프로세스 간 통신 IPC란 무엇인가요?
- 예상 시간
- 7분
프로세스는 기본적으로 각자 독립된 address space를 가지므로 메모리를 직접 공유하기 어렵습니다. IPC는 이런 프로세스들이 데이터를 주고받거나 작업을 조율하기 위한 방법입니다.
대표적인 IPC에는 pipe, socket, shared memory, message queue, signal, RPC 등이 있습니다. 선택 기준은 데이터 크기, 지연 시간, 같은 머신인지 다른 머신인지, 동기화 필요성입니다.
역설적이게도, 가상 메모리가 프로세스를 격리시켰기 때문에 IPC가 필요해졌습니다. 가상 메모리 이전에는 모든 프로세스가 같은 물리 메모리를 공유했기 때문에 데이터를 주고받는 게 그냥 메모리 주소를 쓰면 됐습니다. 가상 메모리로 각 프로세스에 독립된 주소 공간이 생기면서, 격리의 대가로 프로세스 간 통신을 위한 별도의 메커니즘이 필요해졌습니다.
IPC 방식마다 커널이 개입하는 정도가 다릅니다. pipe, socket, message queue는 커널을 경유해 데이터를 전달합니다. shared memory는 두 프로세스의 page table이 같은 물리 페이지를 가리키게 OS가 설정해서, 이후에는 커널을 거치지 않고 직접 읽고 씁니다. 커널 개입이 적을수록 빠르지만 동기화를 직접 해야 합니다.
꼬리질문
조금 더 깊게 물어본다면
답변 뒤에 이어질 수 있는 질문들을 하나씩 열어볼 수 있어요.
Pipe는 어떤 IPC인가요?
한 프로세스의 출력 stream을 다른 프로세스의 입력 stream으로 연결하는 단방향 통신 방식입니다.
터미널에서 쓰는 | 연산자가 바로 pipe입니다. ls | grep foo를 실행하면 ls의 stdout이 커널 버퍼를 통해 grep의 stdin으로 흘러갑니다. 두 프로세스가 직접 통신하는 게 아니라 커널이 중간에서 버퍼 역할을 합니다. 커널 버퍼는 보통 64KB입니다.
이 방식은 anonymous pipe로, 부모-자식처럼 관련 있는 프로세스 사이에서만 씁니다. 관련 없는 프로세스 사이에서 쓰려면 named pipe(FIFO)를 씁니다. mkfifo 명령어로 파일시스템에 이름을 가진 pipe를 만들고, 어떤 프로세스든 그 경로로 열어 통신할 수 있습니다.
Shared memory의 장단점은?
복사 비용이 적어 빠르지만, 여러 프로세스가 동시에 접근하므로 mutex나 semaphore 같은 동기화가 필요합니다.
shared memory가 빠른 이유는 커널을 경유하지 않기 때문입니다. pipe나 socket은 데이터를 커널 버퍼로 복사했다가 다시 꺼내오는 과정이 있습니다. shared memory는 두 프로세스의 page table이 같은 물리 페이지를 가리키게 OS가 설정해두면, 이후 한 프로세스가 쓴 내용을 다른 프로세스가 메모리를 읽듯 바로 읽을 수 있습니다. 복사가 없습니다.
단점은 커널이 데이터 접근 순서를 관리해주지 않는다는 점입니다. 두 프로세스가 동시에 같은 위치에 쓰면 데이터가 섞입니다. mutex나 semaphore로 접근을 제어해야 합니다. POSIX shared memory는 shm_open으로 공유 메모리 영역을 만들고 mmap으로 프로세스 주소 공간에 붙이는 방식으로 씁니다.
Message queue는 언제 적합한가요?
생산자와 소비자의 속도 차이를 흡수하거나, 비동기적으로 작업을 전달하고 재시도·buffering이 필요할 때 적합합니다.
message queue가 필요한 배경은 producer(데이터를 만드는 쪽)와 consumer(데이터를 처리하는 쪽)의 속도 차이입니다. 주문이 초당 1000개 들어오는데 결제 처리는 초당 100개밖에 못 하면, 두 프로세스를 직접 연결하면 처리 못 한 요청이 다 날아갑니다. message queue가 중간 버퍼 역할을 합니다. 주문 프로세스는 queue에 넣고 바로 다음 요청을 받고, 결제 프로세스는 자기 속도에 맞춰 queue에서 꺼내 처리합니다.
pipe와 결정적으로 다른 점은 연결이 끊겨도 메시지가 보존된다는 것입니다. pipe는 두 프로세스가 동시에 연결된 상태여야 하고 한쪽이 죽으면 데이터가 날아갑니다. message queue는 커널이 들고 있기 때문에 consumer 프로세스가 잠깐 죽어도 재시작하면 queue에 남은 메시지부터 이어서 처리할 수 있습니다.
OS의 message queue는 같은 머신 내 프로세스 간에만 씁니다. Redis나 RabbitMQ 같은 외부 메시지 브로커는 같은 개념을 네트워크를 통해 여러 머신에 걸쳐 쓸 수 있게 확장한 것입니다.
Socket도 IPC인가요?
같은 machine 안에서 Unix domain socket으로 process 간 통신을 할 수 있으므로 IPC입니다. TCP socket처럼 network를 통한 process 간 통신에도 사용할 수 있습니다.
Unix domain socket이 TCP socket보다 빠른 이유는 네트워크 스택을 거치지 않기 때문입니다. TCP는 IP 헤더 구성, 체크섬, 네트워크 인터페이스를 거치는데 Unix domain socket은 커널 안에서 바로 데이터를 전달합니다.
실무에서 Unix domain socket은 생각보다 자주 쓰입니다. Nginx와 PHP-FPM이 같은 서버에서 동작할 때 둘 사이의 통신이 Unix domain socket으로 이루어집니다. Docker CLI가 Docker daemon과 통신하는 /var/run/docker.sock도 Unix domain socket입니다. TCP처럼 port 번호 없이 파일 경로로 연결합니다.
Shared memory에서 동기화는 어떻게 하나요?
여러 프로세스가 동시에 같은 메모리 영역에 쓰면 데이터 경합이 발생합니다. mutex, semaphore, spinlock 같은 동기화 기법을 써서 한 번에 하나의 프로세스만 접근하도록 제어합니다.
mutex는 임계 구역의 잠금장치입니다. shared memory의 특정 영역에 들어가면 잠그고, 나올 때 열어줍니다. 잠근 프로세스만 열 수 있다는 소유권이 있습니다. 비유하자면 화장실 칸이 임계 구역이고, 잠금장치가 mutex입니다. 들어간 사람이 안에서 잠그고, 다른 사람은 밖에서 기다리며, 나올 때 반드시 들어간 사람 본인이 열고 나옵니다.
semaphore는 어원이 철도 신호기입니다. 기차가 진입해도 되는지 알려주는 팔 신호에서 왔습니다. 카운터를 관리합니다. 자원이 N개면 N에서 시작해 쓸 때마다 줄이고, 반납하면 올립니다. 0이 되면 대기합니다. mutex와 달리 소유권이 없어서 A가 줄인 카운터를 B가 올릴 수 있습니다. 생산자가 데이터를 넣으면 소비자에게 "가세요" 신호를 보내는 패턴에 씁니다.
spinlock은 lock을 얻을 때까지 CPU를 점유하면서 계속 확인합니다. 대기 시간이 매우 짧을 때는 context switch 비용 없이 빠르지만, 대기가 길어지면 CPU를 낭비합니다. 커널 내부나 실시간 시스템처럼 대기가 거의 없는 환경에서 씁니다.
Pipe와 socket의 실질적 차이는?
Pipe는 같은 호스트의 관련 프로세스(부모-자식 등) 사이에서 단방향 스트림 통신에 적합합니다. Socket은 양방향이고 Unix domain socket뿐 아니라 네트워크를 통한 다른 호스트와의 통신도 지원합니다. 유연성과 이식성은 socket이 높고, 단순한 로컬 단방향 스트림은 pipe가 간단합니다.
pipe는 한 번 생성하면 파일 디스크립터로만 접근하고 파일시스템에 이름이 없습니다. named pipe(FIFO)는 파일시스템 경로로 누구나 열 수 있어 관련 없는 프로세스 간에도 쓸 수 있지만, 여전히 단방향이고 같은 머신에 한정됩니다.
socket은 accept/connect 모델로 하나의 서버가 여러 클라이언트를 동시에 처리할 수 있습니다. pipe는 1:1 연결만 됩니다. 그래서 여러 프로세스가 하나의 서비스와 통신해야 하는 구조에서는 socket이 필수입니다.
Docker 컨테이너 간 통신에 IPC가 쓰이나요?
기본적으로 컨테이너는 network를 통해 통신합니다. Docker는 같은 호스트의 컨테이너끼리 bridge network나 host network를 씁니다. 성능이 중요한 경우 --ipc=host나 shared memory volume을 통해 컨테이너 간 IPC를 허용하기도 하지만 격리가 줄어드는 trade-off가 있습니다.
/dev/shm이 shared memory를 위한 tmpfs 마운트 포인트입니다. 기본적으로 컨테이너마다 독립된 /dev/shm이 있습니다. --ipc=host를 쓰면 컨테이너가 호스트의 /dev/shm을 공유하는데, 다른 컨테이너나 호스트 프로세스의 shared memory에도 접근 가능해져 격리가 깨집니다.
ML 추론 서버에서 이 패턴이 종종 등장합니다. 전처리 컨테이너가 대용량 텐서 데이터를 shared memory에 올려두고, 추론 컨테이너가 네트워크 복사 없이 직접 읽는 방식입니다. 네트워크로 수백 MB 텐서를 주고받는 것보다 훨씬 빠릅니다.
부가 설명
Shared memory는 빠르지만 동기화 문제가 생기기 쉽고, socket은 로컬과 네트워크 통신을 비슷한 모델로 다룰 수 있습니다.
프로세스는 메모리가 분리되어 있으므로 별도의 통신 메커니즘이 필요합니다.
한 줄 정리
IPC는 서로 다른 프로세스가 데이터를 주고받거나 동기화하기 위한 운영체제 수준의 통신 방법입니다.