프로세스(Process)는 실행 중인 프로그램이라고 생각할 수 있습니다.
대부분 시스템에서는 프로세스가 작업의 단위가 됩니다.
프로세스에 대해서 간단히 정리하고 들어갑시다.
1. 기본 정의
프로세스(Process)는 컴퓨터에서 실행중인 프로그램의 인스턴스입니다. 프로세스는 운영 체제에 의해 관리되며, 자체적인 메모리 공간, 데이터, 프로세서 상태(레지스터) 등을 갖습니다.
2. 구성 요소
3. 프로세스 상태
4. 프로세스 제어 블록(PCB)
5. 멀티태스킹과 컨택스트 스위칭
6. 프로세스 간 통신(IPC)
초창기 컴퓨터는 작업(job)을 실행하는 일괄처리 시스템이었고, 사용자 프로그램 또는 태스크(task)를 실행하는 시분할 시스템
이 뒤를 이었습니다.
프로세스의 현재 활동의 상태는 프로그램 카운터 값과 프로세서 레지스터의 내용으로 나타냅니다.
프로세스 메모리 배치는 다음과 같은 섹션으로 구분되어 있습니다.
여기서, 텍스트 섹션과 데이터 섹션은 크기가 고정되어 있습니다.
하지만, 스택 섹션과 힙 섹션은 크기가 동적으로 바뀔 수 있습니다.
함수가 호출될 때 마다 스텍에는 다음과 같은 요소들이 푸쉬 됩니다.
스택 푸쉬 요소
JAVA에서는 활성화 레코드를 스택 프레임이라고도 부릅니다.
스택 프레임 - 로컬 변수 배열, 오퍼랜드 스택, 프레임 데이터
메모리가 동적으로 바뀔 수 있기 때문에 운영체제는 서로의 메모리에 충돌이 발생하지 않도록 관리를 해줘야 합니다.
프로세스는 실행되면서 그 상태가 변합니다. 프로세스는 다음 상태 중 하나에 있게 됩니다.
어느 한순간에 한 처리기 코어에서는 오직 하나의 프로세스만이 실행된다는 것을 인식하는 것이 중요합니다.
각 프로세스는 운영체제에서 프로세스 제어 블록(process control block, PCB)(태스크 제어 블록이라고도 불린다)에 의해 표현됩니다.
new
, ready
, running
, waiting
, halted
⚡️ 레지스터 ⚡️
누산기 (Accumulator):
- 대부분의 연산의 결과를 임시로 저장하는데 사용
- CPU의 성능 향상을 위해, 누산기를 통한 연산은 일반 메모리 접근보다 훨씬 빠르다.
인덱스 레지스터 (Index Register):
- 인덱스 레지스터는 배열, 문자열 등의 데이터 구조에서 위치나 오프셋(offset)을 나타내는 데 사용
- 예를 들어, 반복문에서 배열의 각 요소에 접근할 때 사용
- 인덱스 레지스터는 특정 메모리 주소에 대한 계산을 단순화하고 빠르게 만든다.
스택 레지스터 (Stack Pointer):
- 스택 레지스터는 스택의 최상단을 가리키는 데 사용
범용 레지스터 (General Purpose Register):
- 다양한 목적으로 사용될 수 있는 레지스터
- 임시 데이터 저장, 중간 계산 결과 보관, 메모리 주소 지정 등 다양한 용도로 사용
- 이 레지스터들은 프로그래밍에서 가장 자주 사용되며, 각각의 용도는 CPu 설계와 명령어 세트에 따라 다를 수 있습니다.
상태 코드 레지스터 (Status Register):
- CPU의 현재 상태를 나타내는 플래그(flag)들을 포함
- 연산의 결과(예: 제로, 오버플로우, 캐리, 사인 등)에 대한 정보를 제공
- 예를 들어, 산술 연산 후 결과가 0이면 '제로 플래그'가 설정
- 이 정보는 조건부 분기, 오류 검출 등에서 사용
대부분의 현대 운영체제는 프로세스 개념을 확장하여 한 프로세스가 다수의 실행 스레드를 가질 수 있도록 허용합니다.
이러한 특성은 특히 다중 처리기 시스템에서 이익을 얻을 수 있는데, 여러 스레드가 병렬로 실행될 수 있습니다.
다중 프로그래밍의 목적은 CPU 이용을 최대화하기 위하여 항상 어떤 프로세스가 실행되도록 하는 데 있습니다.
시분할의 목적은 각 프로그램이 실행되는 동안 사용자가 상호 작용 할 수 있도록 프로세스들 사이에서 CPU 코어를 빈번하게 교체하는 것입니다.
프로세스 스케줄러는 이 목적을 달성하기 위해서 여러 실행 가능한 프로세스 중에서 하나의 프로세스를 선택해야 합니다.
일반적으로 대부분의 프로세스는 2가지로 설명할 수 있습니다.
프로세스가 준비 큐에 들어가게 되면 ready 상태가 된다. 그리고 CPU 코어에 의해서 running 상태로 전환되기를 기다립니다.
큐는 일반적으로 "연결 리스트"로 저장됩니다.
프로세스가 인터럽트 되거나 I/O 요청의 완료와 같은 특정 이벤트가 발생할 때까지 기다린다고 해봅시다.
장치들의 속도는 프로세서보다 상당히 느리게 실행되므로 프로세스는 준비 큐에서 나와 대기 큐(Wait queue)에 삽입됩니다.
프로세스 스케줄링의 일반적인 표현은 큐잉 다이어그램입니다. 아래 그림을 참고하시면 됩니다.
CPU 스케줄러의 역할은 준비 큐에 있는 프로세스 중에서 선택된 하나의 프로세스에 CPU 코어를 할당하는 것입니다.
CPU 스케줄러는 일반적으로 훨씬 더 자주 실행되지만 적어도 100ms마다 한번씩 실행됩니다.
스와핑은 필요에 따라 프로세스를 메모리에서 적재하거나 제거하는 것을 의미합니다. 메모리에서 디스크로 "스왑 아웃"하고 현재 상태를 저장하고, 디스크에서 메모리로 "스왑 인"하여 상태를 복원할 수 있습니다.
스와핑은 일반적으로 메모리가 초과 사용되어 가용공간을 확보해야 할 떄만 필요합니다.
현대의 자원은 높은 성능을 자랑하고, 스와핑의 경우 성능을 낮추는 동작이기 때문에 요즘에는 자주 발생하지 않는다고 설명되고 있습니다.
문맥은 프로세스의 PCB에 표현됩니다.
일반적으로 커널 모드이건 사용자 모드이건 CPU의 현재 상태를 저장하는 작업을 수행(state save)하고, 나중에 연산을 재개하기 위하여 상태 복구 작업을 수행(state restore)합니다.
문맥 교환(Context Switching)은 CPU 코어를 다른 프로세스로 교환하는 작업을 의미하고, 이 과정에서 이전의 프로세스의 상태를 보관하고 새로운 프로세스의 보관된 상태를 복구하는 작업이 요구됩니다.
여기서 말하는 상태는 PCB라고 보면 됩니다.
문맥 교환이 진행될 동안 시스템은 아무런 유용한 일을 할 수 없기 때문에 순수한 오버헤드입니다.
문맥 교환이 발생할 때 메인 메모리(RAM) 혹은 2차 메모리(SSD)와 같은 정보들은 변경이 일어날 필요가 없겠지만, 하나의 CPU 코어가 사용하는 명령어 파이프라인, 레지스터등과 같은 자원들은 공유해서 사용하기 때문에 교체되는 작업이 필요합니다.
새로운 프로세스의 메모리 주소 공간으로 CPU의 접근을 재맵핑 하는 작업은 메모리 관리 유닛(MMU)를 통해 이루어집니다.
여기서는 프로세스의 생성 및 종료를 위한 기법에 대해서 알아보고 UNIX와 Window 시스템에서의 프로세스 연산에 대해서 설명할 것입니다.
프로세스는 부모 프로세스와 자식 프로세스로 구분될 수 있는데, 이것은 프로세스의 트리를 형성합니다.
대부분의 현대 운영체제들은 유일한 프로세스 식별자(pid)를 사용하여 프로세스를 구분하는데 이 식별자는 보통 정수입니다.
이 식별자를 통하여 커널이 유지하고 있는 프로세스의 다양한 속성에 접근하기 위한 찾아보기(index)로 사용됩니다.
pid 1인
systemd
프로세스(부트 프로그램)가 모든 사용자의 프로세스의 루프 부모 프로세스 역할을 수행하고 시스템이 부트될 때 생성되는 사용자 프로세스입니다.
자식 프로세스가 생성될 때 자원 할당과 관련하여 여러 가지를 고려해볼 수 있는데,
또한, 부모 프로세스는 자식 프로세스와 메모리를 공유해서 사용할 수도 있을 것입니다.
부모 프로세스 자원의 일부분만 사용하도록 자식 프로세스가 쓸 수 있게 제한하며, 자식 프로세스들을 많이 생성하여 시스템을 과부화 상태로 만드는 프로세스를 방지할 수 있습니다.
프로세스가 새로운 프로세스를 생성할 때, 두 프로세스를 실행시키는 데 두 가지 가능한 방법이 존재합니다.
새로운 프로세스들의 주소 공간 측면에서 볼 떄 다음과 같은 두 가지 가능성이 있습니다.
프로세스의 복사는
fork()
명령어를 생각하면 되고, 부모 프로세스와 자식 프로세스는 pid를 통해 연결되어 있다고 생각하면 됩니다.
fork()
명령어를 통해 프로세스가 생성되고 exec()
시스템 콜을 호출하여 이진 파일을 메모리로 적재(load)하여 프로그램을 실행시킬 수 있습니다.
만약, 부모 프로세스가 먼저 끝나서 자식 프로세스를 기다리는 상황이라면 wait()
시스템 콜을 통해 준비 큐에서 자신을 제거합니다.
좀 더 이해를 돕기 위해서 프로세스를 생성하는 코드의 이미지를 두개 남겨두겠습니다.
위의 코드는 fork()
시스템 콜을 활용하여 자식이 부모의 복사본을 실행하는 병행 실행 프로세스입니다.
매개 변수에 대해서 간단하게 설명을 적어보자면,
STARTUPINFO
: 새로운 프로세스의 특징PROCESS_INFOMATION
: 새로 생성된 프로세스와 스레드에 대한 핸들과 식별자들을 포함pi.hProcess
: 자식 프로세스의 핸들exit
시스템 콜을 사용하여 종료를 할 때 프로세는 자신을 기다리고 있는 부모 프로세스에(wait 시스템 콜을 통해) 상태 값(통상 정수값)을 반환할 수 있습니다.
그리고 할당 받은 모든 자원이 해제되고 운영체제로 반납됩니다.
부모는 다음과 같이 여러 가지 이유로 인하여 자식 중 하나의 실행을 종료할 수 있습니다.
특히, 마지막 경우는 연쇄식 종료(cascading termination)이라고 부르며 이 작업은 운영체제가 시행합니다.
다음은 wait()
시스템 콜을 사용하는 코드입니다.
pid_t pid;
int status;
pid = wait(&status); // 자식 프로세스를 구별할 수 있게 해줍니다.
프로세스가 종료하면 사용하던 자원은 운영체제가 되찾아 갑니다. 그러나 프로세스의 종료 상태가 저장되는 프로세스 테이블의 해당 항목은 부모 프로세스가 wait()
를 호출할 때까지 남아 있게 됩니다.
종료 되었지만 부모 프로세스가 wait()
호출을 하지 않은 프로세스를 좀비(zombie) 프로세스라고 합니다.
부모 프로세스가 wait()
를 호출하는 대신 종료한다면 해당 프로세스의 자식 프로세스는 고아(orphan) 프로세스가 됩니다.
UNIX 는 고아 프로세스의 새로운 부모 프로세스로 init 프로세스를 지정함으로써 문제를 해결합니다.
init 프로세스는 주기적으로 wait()
를 호출하여 고아 프로세스의 종료 상태를 수집하고 프로세스 식별자와 프로세스 테이블 항목을 반환합니다.
Android는 자원 제약 때문에 제한된 시스템 자원을 회수하기 위해 기존 프로세스를 종료해야 할 수도 있습니다. 따라서, 프로세스의 중요도 계층을 식별했습니다.
우선순위 별로 정리를 해보겠습니다.
운영체제 내에서 실행되는 병행 프로세스들은 독립적이거나 협력적인 프로세스들일 수 있습니다.
프로세스 협력을 허용하는 환경을 제공하는 데는 몇 가지 이유가 있습니다.
협력적 프로세스들은 데이터를 교환할 수 있는 프로세스 간 통신(interprocess communication, IPC) 기법이 필요합니다.
프로세스 간 통신에는 기본적으로 공유 메모리(shared memory)와 메시지 전달(message passing)의 두 가지 모델이 있습니다.
분산 시스템 환경에서는 메시지 전달이 구현하기 더 쉽고 유용합니다. 하지만 시스템 콜을 이용하여 부가적인 시간 소비 작업이 필요하기 때문에 속도적인 측면에서는 공유 메모리가 더 빠릅니다.
공유 메모리 영역이 구축되면 모든 접근은 일반적인 메모리 접근으로 취급되어 커널의 도움이 필요 없습니다.
특징들을 정리하자면 다음과 같습니다.
메시지 전달
공유 메모리
통상 공유 메모리 영역은 공유 메모리 세그먼트를 생성하는 프로세스의 주소 공간에 위치합니다.
일반적으로는 서로 다른 프로세스들 간의 메모리 영역 칩입을 허용하지 않지만, 공유 메모리는 둘 이상의 프로세스가 이 제약 조건을 제거하는 것에 동의를 하여 사용됩니다.
운영체제가 메모리의 영역을 할당해주는 것은 맞지만, 데이터의 형식과 위치는 프로세스에 의해 결정되며, 책임은 프로세스들한테 있습니다.
이 말은 곧 공유 메모리의 데이터에 대한 처리는 개발자의 몫이라는 의미입니다.
아주 일반적인 개념으로는 생산자-소비자 문제를 고려해볼 수 있습니다.
생산자 프로세스는 공유 메모리에 데이터에 대해서 쓰기 동작을 수행하며, 소비자 프로세스는 공유 메모리에 있는 데이터에 대해서 읽기만 수행하는 것입니다.
이렇게 단순한 경우일지라도, 두 프로세스 간에 동기화가 되어 있지 않다면 존재하지 않는 데이터들을 소비자가 소비하려고 계속 시도할 수 있습니다.
여기서 버퍼(Queue)에 대한 개념이 등장하는데,
생산자가 버퍼에 work를 넣어주고, 소비자는 해당 버퍼에서 하나씩 work를 꺼내가면서 동작을 수행할 수 있습니다. 이것은 다음 코드를 보면 이해할 수 있습니다.
생산자는 버퍼가 가득 차있을 경우에는 item을 넣지 않고, 넣을 수 있는 상황에서만 item을 넣습니다. 동시에 소비자는 item을 계속해서 소비하고 소비할 item이 없다면 아무것도 하지 않습니다.
위에서 정리한 것이 헷갈릴 수 있습니다.
그냥 공유 메모리에 저장해두고 프로세스들이 그것에 접근해가며 일반 데이터 접근과 같이 사용하면 되는 것 아닌가? 라는 생각이 들 수 있습니다.
하지만, 이러한 접근은 잘못된 접근입니다.
여기서는 몇 가지 개념을 추가적으로 정리를 할 필요가 있습니다.
헷갈릴 수 있는 이유는 멀티스레드 상황에서 여러 스레드들이 하나의 공유된 데이터에 접근하는 것과는 조금 다르기 때문입니다. 해당 과정에서는 복사본을 만들지 않죠.
복사본을 만드는 이유는 데이터의 일관성과 안전한 사용을 위한 일반적인 방법입니다.
이것은 공유 메모리 영역에 대한 의존도를 줄이고, 데이터 접근 속도를 향상시킬 수 있습니다.
하나씩 풀어가면서 설명해 봅시다.
우선, 공유 메모리에 데이터를 생성하는 과정부터 언급을 해봅시다.
해당 공유 메모리에 데이터를 생성하려고 여러 프로세스들(생성자)이 접근할 수 있습니다.
데이터 Write 과정을 성공적으로 마쳤다고 가정하고, 이후 소비자 프로세스들에게 데이터를 Write를 했다고 알릴 수 있어야 합니다.
각 소비자 프로세스들이 Read하는 것도 성공했다고 합시다. 이 과정에서 각자의 프로세들은 해당 공유된 데이터의 주소값을 이용해서 그대로 사용하는 것이 아니라 자신들의 프로세스 메모리 영역에 복사하여 사용합니다.
각자의 프로세스들은 사용 목적에 맞게 자신들의 특정 메모리 영역에 저장을 하여서 사용할 것이고, 이것은 해당 프로세스의 여러 스레드들이 공유할 수도 있으니 이 경우에도 동기화 작업이 별도로 수행될 수 있습니다.
마지막으로, 생산자 프로세스가 해당 공유된 데이터를 업데이트해야 되는 상황이 발생했다고 가정해봅시다.
메시지 전달 방식은 동일한 주소 공간을 공유하지 않고도 프로세스들이 통신을 하고, 그들의 동작을 동기화할 수 있도록 허용하는 기법을 제공합니다.
메시지 전달 시스템은 최소한 두 가지 연산을 제공합니다.
send(message)
receive(message)
통신을 하기 위해서는 통신 연결(communication link)가 설정되어 있어야 하고, 이것은 논리적인 연결을 의미합니다.
하나의 링크와 send()/receive()
연산ㅇ르 논리적으로 구현하는 다수의 방법은 다음과 같습니다.
직접 통신하에서, 통신을 원하는 각 프로세스는 통신의 수신자 또는 송신자의 이름을 명시해야 합니다.
이 기법에서 통신 연결은 다음의 특성을 가집니다.
해당 방식은 대칭성을 보이는데, 비대칭성을 사용할 수도 있습니다.
두 방식의 단점은 프로세스를 지정하는 방식 때문에 모듈성을 제한한다는 것입니다.
간접 통신에서 메시지들은 메일박스(mailbox) 또는 포트(port)로 송신되고, 그것으로부터 수신됩니다.
각 메일박스는 고유한 id의 정수값을 가지고 그것을 이용하여 통신을 합니다.
특징은 다음과 같다.
메시지 전달은 봉쇄형(blocking)이거나 비봉쇄형(nonblocking) 방식으로 전달됩니다. 이 두 방식은 각각 동기식, 비동기식으로 알려져 있습니다.
만약, 송신자와 수신자 모두 동기식 방식으로 구현을 했다면 랑데부(rendezvous)가 발생하여, 동기화는 해결되지만 성능적인 측면에서 문제가 될 것입니다.
메시지가 들어가게 되는 큐를 구현하는 방식은 세 가지가 있습니다.
간단하게 이해를 돕기위한 차원에서 정리를 하겠습니다.
POSIX 공유 메모리는 메모리-사상 파일을 사용하여 구현합니다.
fd = shm_open(name, O_CREAT | O_RDWR, 0666); // 공유 메모리 객체
ftruncate(fd, 4096); // 객체의 크기 설정
마지막으로 nmap()
함수를 사용하여, 공유 메모리 객체를 포함하는 메모리-사상 파일을 구축합니다.
nmap()
함수는 파일의 포인터를 반환합니다.
공유 메모리 제거는 shm_unlink()
함수를 호출하여 제거할 수 있습니다.
코드는 생략하겠습니다.
Mach 커널은 프로세스와 유사하지만 제어 스레드가 많고 관련 자원이 적은 다중 태스크의 생성 및 제거를 지원합니다.
통신은 메시지로 수행되며, 포트(port)라고 하는 메일박스로 메시지를 주고 받습니다.
포트의 크기는 정해져 있고 단방향입니다.
Mach는 포트를 사용하여 자원을 나타내며, 메시지 전달은 객체 지향 접근 방식을 제공합니다.
각 포트에는 그 포트와 상호 작용하는 데 필요한 자격을 식별하는 포트 권한 집합이 연관됩니다.
즉, 태스크가 다른 태스크로 메시지를 전달하려고 할 때 태스크가 소유하고 있는 포트 권한이 필요합니다.
포트 권한은 태스크(프로세스) 단위로, 동일한 태스크에 속하는 모든 스레드가 동일한 포트 권한을 공유합니다. 따라서 동일한 태스크에 속하는 두 개의 스레드는 각 스레드와 관련된 스레드-별 포트를 통해 메시지를 교환하여 쉽게 통신할 수 있습니다.
태스크가 생성되면 두 개의 특별한 포트도 생성됩니다.
다음 함수는 새 포트를 작성하고, 메시지 큐를 위한 공간을 할당하며 포트에 대한 권한을 식별합니다.
mach_port_t port; // 포트 권한의 이름
mach_port_allocate(
mach_task_self(), // a task referring to itself
MACH_PORT_RIGHT_RECEIVE, // the right for this port
&port); // the name of the port right
각 태스크는 부트스트랩 포트에 액세스 할 수 있어서 태스크가 생성한 포트를 시스템 전체의 부트스트랩 서버에 등록할 수 있습니다.
해당 서버의 레지스트리에서 포트 검색을 수행할 수 있습니다.
각 포트와 관련된 큐는 크기가 제한되어 있으며 처음에는 비어 있습니다. 메시지가 포트로 전송되면 큐에 복사됩니다. 모든 메시지는 안정적으로 전달되며 동일한 우선순위를 가집니다.
여기서 살펴봐야되는 것은 메시지가 고정 크기인지 가변 길이인지를 살펴봐야 합니다.
메시지는 단순하거나 복잡할 수 있습니다.
메시지가 복잡하다면,
이와 같은 방법이 유용합니다.
여기서는 코드를 넣어놓도록 하겠습니다. 메시지 전달을 하기위해 어떠한 매개변수들이 필요한지 확인할 수 있습니다.
만약 포트의 큐가 가득찬 경우 송신자는 mach_msg()
의 매개변수를 통해 특정 동작을 취할 수 있습니다.
마지막 옵션은 서버 태스크를 위한 것입니다.
메시지 시스템 주요 문제점은 일반적으로 송신자 포트에서 수신자의 포트로 메시지를 복사해야 하므로 발생하는 성능 저하입니다.
Mach 메시지 시스템은 가상 메모리 기술을 사용하여 복사 연산을 피하려고 합니다. 기본적으로 Mach는 송신자의 메시지가 포함된 주소 공간을 수신자의 주소 공간에 매핑합니다. 따라서 송신자와 수신자 모두 동일한 메모리에 액세스 하므로 메시지 자체는 실제로 복사되지 않습니다.
이 기술은 시스템 내 메시지에만 작동합니다.
Windows 운영체제는 모듈화를 이용하여 기능을 향상시키고 새로운 기능을 구현하는 시간을 감소시킨 최신 설계의 예입니다.
Windows는 다중 운영 환경 또는 서브 시스템을 지원하며, 응용 프로그램은 메시지 전달 기법을 통해 이들과 통신합니다.
Windows의 메시지 전달 설비는 고급 로컬 프로시저 호출 설비(advanced local procedure call facility, ALPC)라고 불립니다.
ALPC는 동일 기계상에 있는 두 프로세스 간의 통신에 사용됩니다. 이것은 널리 사용되는 RPC 기법과 같으나, Windows에 맞게 특별히 최적화되었습니다.
Mach와 유사하게, 포트를 사용하고 연결 포트(connection port)와 통신 포트(communication port)의 두 가지 유형의 포트를 사용합니다.
이 둘의 사용은 필자의 생각으로는 TCP Connection 매커니즘과 매우 유사합니다.
클라이언트는 서브시스템으로부터 서비스를 원할 경우에, 먼저 연결 포트 객체에 대한 핸들을 열고 연결 요청을 보냅니다. -> 환영 소켓으로 연결 요청
서버는 채널을 생성하고 핸들을 클라이언트에게 반환합니다. -> 통신 소켓 준비
채널은 한 쌍의 사적인 통신 포트로 구성되고, 메시지를 주고 받습니다. -> 메시지 송수신
ALPC 채널이 생성되면 다음 3가지 중 하나의 메시지 전달 기법이 선택됩니다.
Windows의 고급 로컬 프로시저 호출 설비는 Windows API의 부분이 아닙니다.
따라서 프로그래머가 사용할 수 없고 내부적으로 외부 통신은 RPC를 내부 통신은 ALPC을 사용합니다.
파이프는 두 프로세스가 통신할 수 있게 하는 전달자로서 동작합니다.
일반적으로 같은 시스템에서 다른 프로세스끼리 통신을 할 때 사용됩니다.
일반 파이프는 생산자-소비자 형태로 두 프로세스 간의 통신을 허용합니다.
생산자는 파이프의 한 종단(쓰기 종단)에 쓰고, 소비자는 다른 종단(읽기 종단)에서 읽습니다. 단방향 통신을 하게 됩니다.
만약 양방향 통신이 필요하다면 두 개의 파이프를 사용해야 합니다.
UNIX 시스템에서 일반 파이프는 다음 함수를 사용합니다.
pipe(int fd[])
이 함수는 fd[] 파일 설명자를 통해 접근되는 파이프를 생성합니다. UNIX는 파이프를 파일의 특수한 유형으로 취급합니다. 따라서 파이프는 일반적인 read()
와 write()
시스템 콜을 사용하여 접근할 수 있습니다.
앞서 fork()
로 생성한 자식 프로세스는 열린 파일을 부모 프로세스로부터 상속받는다고 했는데, 파이프는 파일의 특수한 유형이기 때문에 자식 프로세스는 부모로부터파이프를 상속받습니다.
이러한 특성 때문에 일반 파이프는 부모 프로세스와 fork()
로 생성한 자식 프로세스와 통신하기 위해 사용됩니다.
Windows 시스템의 일반 파이프는 익명 파이프(anonymous pipe)라고 불리며 UNIX에 대응되는 파이프와 유사하게 동작합니다.
차이점은 UNIX의 경우 자식 프로세스가 부모 프로세스가 생성한 파이프를 자동으로 상속받는 것에 비해 Windows는 프로그래머가 어던 속성을 상속받는지 명시해야 한다는 점입니다.
일반 파이프는 오로지 프로세스들이 서로 통신하는 동안에만 존재합니다.
지명 파이프(named pipes)는 좀 더 강력한 통신 도구를 제공합니다.
여기서는 클라이언트 서버에서 사용할 수 있는 두 가지 다른 통신 전략에 대해서 설명을 합니다.
소켓(socket)은 통신의 극적(endpoint)를 뜻합니다.
소켓을 사용한 통신 같은 경우 Network에서 많이 다루었기 때문에 자세한 정리는 하지 않겠습니다.
1024 미만의 well-known 포트를 이용하여 연결을 하고 이후 다른 포트를 이용하여 통신을 합니다.
식별자로 사용하는 것은 송수신자의 IP 주소와, 포트 번호를 사용하여 4개의 값으로 연결을 구분합니다.
소켓을 이용한 통신은 분산된 프로세스 간에 널리 사용되고 효율적이기는 하지만 너무 낮은 수준입니다.
소켓은 스레드 간에 구조화 되지 않은 바이트 스트림만을 통신하도록 하기 때문입니다.
이러한 원시적인 바이트 스트림 데이터를 구조화하여 해석하는 것은 클라이언트 서버의 책임입니다.
원격 서비스와 관련한 가장 보편적인 형태 중 하나는 RPC 패러다임으로서, 네트워크에 연결된 두 시스템 사이의 통신에 사용하기 위하여 프로시저 호출 기법을 추상화 하는 방법으로 설계되었습니다.
IPC 방식과는 달리 RPC 통신에서 전달되는 메시지는 구조화되어 있고, 따라서 데이터 패킷 수준을 넘어서게 된다.
RPC 시스템은 클라이언트 쪽에 스텁(stub)을 제공하여 통신을 하는데 필요한 자세한 사항들을 숨겨 줍니다.
이러한 방식으로 동작하기 때문에 RPC는 클라이언트가 원격 프로시저 호출을 마치 자기의 프로시저 호출하는 것처럼 해줍니다.
RPC의 경우 지역 프로시저 호출과는 다르게 네트워크 오류 때문에 실패할 수 있고, 메시지가 중복되어 여러 번 실행될 수 있습니다.
이 문제를 해결하는 방법은 운영체제가 메시지가 최대 한번 실행 되는 것이 아니라 정확히 한번 처리되도록 보장하는 것입니다.
"최대 한 번"의 의미는 각 메시지에 타임 스탬프를 매기는 것으로 보장할 수 있다.
"정확히 한 번"은 ARQ 방식을 통해 보장할 수 있다.
마지막으로 사용 포트의 결정은 처음에 고정된 포트 번호를 활용하여 연결 포트의 정보를 주고 받으면서 해결될 수 있다. (3-way handshake와 유사)
RPC에서는 RPC포트를 통해 랑데부용 디먼을 제공한다고 한다. 그러면 클라이언트가 자신이 실행하기를 원하는 RPC 이름을 담고 있는 메시지를 랑데부 디먼에게 보내서, RPC 이름에 대응하는 포트 번호가 무엇인지 알려달라고 요청합니다.
포트 번호가 클라이언트에게 반환되고, 클라이언트는 그 포트 번호로 RPC 요청을 계속 보냅니다.
RPC는 분산 파일 시스템(distributed file system, DFS)을 구현하는 데 유용하다고 합니다.
외부 서버에 데이터에 대한 특정 동작을 맡기고, 응답을 결과로 받아오는 것이죠.