프로세스간 통신, IPC
카테고리: OSMarch 18, 2021
여러 개의 프로세스가 동시에 실행되는 시스템에서, 프로세스를 크게 아래와 같이 두 분류로 나눌 수 있습니다:
- 독립 프로세스(independent process): 다른 프로세스와 데이터를 공유하지 않는 프로세스입니다.
- 협력 프로세스(cooperating process): 다른 프로세스에 의해 영향을 받거나, 다른 프로세스에 영향을 줄 수 있는 프로세스입니다.
이때, 아래와 같은 이유로 프로세스끼리 서로 협력할 수 있는 환경을 조성해줘야 합니다:
- 정보 공유(Information sharing): 여러 애플리케이션에서 동일한 데이터에 접근하고자 하는 경우가 있을 수 있습니다.
- 동작 속도 향상(Computation speedup): 어떤 작업을 더 빠르게 수행하기 위해 작업을 세부 작업으로 나눠 병렬 처리 함으로써 성능을 증대할 수 있습니다.
- 모듈성(Modularity): 시스템을 설계할 때 하나의 거대한 프로그램으로 구성하는 것이 아니라, 여러 개의 모듈로 나눠 구성함으로써 개발 효율을 향상하고 유지 보수를 좀 더 쉽게 할 수 있습니다.
이처럼 협력 프로세스들끼리 서로 통신하기 위해 프로세스간 통신 (InterProcess Communication, IPC)이라는 메커니즘이 존재하는데, IPC를 하는 방법에는 두 가지 모델이 존재합니다:
- 공유 메모리(shared memory): 협력 프로세스들끼리 메모리를 공유하여 사용하는 방식으로, 공유 메모리 영역에 읽기·쓰기 작업을 수행하여 데이터를 공유합니다. 중개자 없이 프로세스가 메모리에 직접 접근하기 때문에 속도가 빠르다는 장점이 있지만, 동기화(synchronization) 문제가 발생할 수 있어 이에 대한 처리가 필요합니다.
- 메시지 전달(message passing): 메시지를 주고받는 방식으로 통신하는 방식입니다. 운영체제가 커널 내부에 메시지를 기록할 수 있는 공간(message queue)을 마련해 두고, 프로세스들이 적절한 시스템 콜을 이용해 메시지 송·수신을 운영체제에 요청하여 사용할 수 있도록 합니다.
대부분의 운영체제는 위 두 방식을 모두 구현하고 있습니다. 이를 살펴봅시다.
공유 메모리 방식
공유 메모리(Shared Memory)를 이용하여 프로세스 간에 통신하는 방식은 여러 프로세스가 공유하여 사용하는 메모리 영역에 읽기·쓰기 작업을 수행하여 데이터를 주고받습니다. 공유 메모리는 방식은 프로세스가 커널의 중개 없이 직접 공유 메모리 영역에 접근할 수 있기 때문에 빠르다는 장점이 있습니다.
메세지 전달 방식
메시지 전달 방식은 주소 공간을 공유하지 않고 협력 프로세스끼리 통신할 수 있도록 하는 방식으로, 통신하는 프로세스들이 서로 다른 컴퓨터에 존재하는 분산형 시스템에 특히 적합한 방식입니다.
메시지 전달 방식엔 최소한 메시지를 전송하는 동작 (send(message)
)과 수신하는 동작 (receive(message)
)이 제공되는데, 이때 주고받는 메시지의 크기는 고정 길이일 수도, 가변 길이일 수도 있습니다.
두 프로세스 A, B가 서로 통신하고자 할 때, 두 프로세스 간에는 반드시 통신 링크(communication link)가 존재해야 합니다. 통신 링크의 논리적인 구현 방법으로는 아래와 같은 방법들이 있습니다.
- 직접(Direct) 혹은 간접(Indirect) 통신.
- 동기(Synchronous) 혹은 비동기(Asynchronous) 통신.
- 자동(Automatic) 혹은 명시적(Explicit) 버퍼링.
이와 관련된 내용들을 하나씩 살펴봅시다.
네이밍(Naming)
직접 통신 방법을 사용하는 경우, 각 프로세스는 반드시 상대방의 이름을 명시해야 합니다. 이 방식에서 send()
와 receive()
는 아래와 같이 정의됩니다:
send(A, message)
: 프로세스 A에 메시지 전송.receive(B, message)
: 프로세스 B로부터 메시지 수신.
네이밍 방식에서 통신 링크는 아래와 같은 특성을 보입니다:
- 통신을 원하는 모든 프로세스 쌍(pair) 사이에 자동으로 링크가 형성됩니다. 프로세스는 통신하고자 하는 상대방의 신원(identity)만 알면 됩니다.
- 하나의 링크는 오직 두 프로세스만을 연결합니다. 또한 각 프로세스 쌍 사이에는 오직 하나의 링크만 존재합니다.
이 기법은 addressing 방식에서 대칭성(symmetry)을 보입니다. 즉, 통신하기 위해선 전송자와 수신자 모두 상대방의 이름을 명시해야만 하죠. 반대로, 비대칭적(asymmetry)인 addressing 방식을 사용하는 기법의 경우 전송자만 상대방의 이름을 명시하고, 수신자는 상대방의 이름을 명시하지 않아도 됩니다. 이러한 방식에선 send()
와 receive()
는 아래와 같이 정의됩니다:
send(A, message)
: 프로세스 A에게 메시지 전송.receive(id, message)
: 아무 프로세스로부터 메시지를 수신. 변수id
는 통신을 발생시킨 프로세스의 이름으로 설정됨.
하지만 이러한 방식(대칭·비대칭 모두)은 모듈성이 제한된다는 단점이 있습니다. 예를 들어 프로세스의 이름을 바꾸는 경우, 다른 프로세스에 정의된 이전의 이름을 모두 찾아서 바꿔야만 합니다. 이처럼 식별자를 하드 코딩 방식은 곧이어 살펴볼 간접 통신 방법에 비해 그리 바람직하진 않습니다.
간접 통신 방식에선 메일 박스 혹은 포트를 통해 메시지를 주고받습니다. 이때 메일 박스란 추상적으로 프로세스가 메시지를 저장하고 지우는 객체라고 볼 수 있습니다. 각 메일 박스엔 고유 id가 있으며, 한 프로세스는 여러 개의 메일 박스를 통해 다른 프로세스와 통신할 수 있지만, 어떤 두 프로세스가 통신할 땐 두 프로세스가 공유하는 메일 박스를 통해서만 이뤄집니다.
이 방식에서 send()
와 receive()
는 아래와 같이 정의됩니다:
send(A, message)
:A
메일 박스에 메시지 전송.receive(A, message)
:A
메일 박스로부터 메시지 수신.
메일 박스 방식에서 통신 링크는 아래와 같은 특성을 보입니다:
- 통신하고자 하는 프로세스들이 특정 메일 박스를 공유하는 경우에만 링크가 형성됩니다.
- 한 링크는 두 개 이상의 프로세스에 연결될 수 있습니다.
- 한 프로세스 쌍 사이에는 여러 개의 서로 다른 링크가 존재할 수 있으며, 각 링크는 하나의 메일 박스에 대응됩니다.
프로세스 P1
, P2
, P3
가 메일 박스 A
를 공유한다고 해봅시다. P1
이 메일 박스 A
에 메시지를 전송하고, P2
, P3
가 A
에 대해 receive()
를 호출했다면, P2
와 P3
중 어느 프로세스가 메시지를 수신하도록 해야 할까요? 이 문제는 아래와 같은 방법으로 해결할 수 있습니다:
- 한 링크에 최대 두 개의 프로세스가 연결되도록 하여 문제를 해결.
- 한 번에 단 하나의 프로세스만
receive()
를 수행할 수 있도록 하여 문제를 해결. - 시스템이 라운드 로빈 같은 알고리즘을 이용하여 어느 프로세스가 수신할지를 결정. 이때 전송자 누가 메시지를 수신했는지 알림.
메일 박스는 프로세스 혹은 운영 체제가 소유할 수 있습니다.
- 프로세스가 메일 박스를 소유하는 경우(즉, 메일 박스가 프로세스의 주소 공간에 포함되는 경우), 오직 해당 메일 박스를 통해 수신만 할 수 있는 소유자와 해당 메일 박스로 전송만 할 수 있는 사용자를 구분하게 됩니다. 이 상황에선 각 메일 박스마다 고유의 소유자가 있기 때문에 어느 프로세스가 메시지를 수신해야 할지를 결정할 필요가 없습니다. 그리고 메일 박스를 소유하는 프로세스가 종료되면 메일 박스도 같이 사라지는데, 이때 다른 프로세스에도 해당 메일 박스가 사라졌다는 사실을 알려줘야 합니다.
-
반대로 운영 체제가 메일 박스를 소유하는 경우, 해당 메일 박스는 어느 프로세스에도 속하지 않으며 운영 체제는 반드시 프로세스에게 아래의 기능을 제공해야 합니다:
- 새로운 메일 박스를 생성하는 기능.
- 메일 박스로 메시지를 전송하는 기능과 메일 박스를 통해 메시지를 수신하는 기능.
- 메일 박스를 삭제하는 기능.
운영 체제가 제공하는 기능을 통해 메일 박스를 생성한 프로세스는 자연스레 해당 메일 박스의 소유자가 됩니다. 처음엔 해당 프로세스만 메일 박스를 통해 메시지를 수신할 수 있지만, 이후 시스템 콜을 통해 다른 프로세스에게 소유권이 넘어가도록 할 수도 있습니다.
동기화(Synchronization)
메시지 전달 방식은 동기 혹은 비동기로 동작할 수 있습니다:
-
blocking 방식은 동기적인 방식으로,
- blocking send는 수신자가 메시지를 수신할 때까지 전송자의 상태를 blocked로 유지합니다.
- blocking receive는 수신할 메시지가 있을 때까지 수신자의 상태를 blocked로 유지합니다.
-
non-blocking 방식은 동기적인 방식으로,
- non-blocking send는 전송자가 메시지를 보낸 뒤 하던 작업을 이어서 수행합니다.
- non-blocking receive는 수신자가 유효한 메시지를 받도록 하거나, 받을 메시지가 없으면
null
을 받도록 합니다.
이때, 전송자와 수신자 모두 동기식으로 동작하는 경우 이를 랑데뷰(rendezvous) 방식이라고 하는데, 이는 아래의 버퍼링 섹션에서 다시 살펴보겠습니다.
버퍼링(Buffering)
직접 통신이든 간접 통신이든 상관없이, 두 프로세스가 주고받는 메시지들은 임시 큐(버퍼)에 저장됩니다. 이때 큐는 세 가지 방식으로 구현될 수 있습니다:
- 큐의 크기가 0인 경우: 큐의 크기가 0이므로 메시지를 보관할 수 없습니다. 따라서 전송자는 수신자가 메시지를 받을 때까지 반드시 blocked 상태를 유지해야 합니다 (즉, 전송자가 동기식으로 동작해야 합니다). 앞서 말한 랑데뷰 방식이 이에 해당합니다.
- 큐의 크기가 n인 경우: 큐의 크기가 n이라는 것은 최대 n개의 메시지를 보관할 수 있다는 말이므로, 새로운 메시지가 도착했을 때 큐가 꽉 차지 않았다면 전송자는 비동기식으로 동작할 수 있습니다. 반대로 큐가 꽉 찬 경우 전송자는 큐에 빈 공간이 날 때까지 동기적으로 동작해야 합니다.
- 큐의 크기가 무한대인 경우: 큐의 크기가 무한이므로 전송자는 항상 비동기식으로 동작할 수 있습니다.