프로세스 간 통신(IPC)
프로세스는 서로 독립된 주소 공간을 가지므로 한 프로세스는 다른 프로세스의 메모리를 마음대로 읽거나 쓸 수 없습니다. 이 격리는 안정성과 보안에 좋지만, 프로세스끼리 데이터를 주고받거나 서로의 작업을 맞춰야 할 때는 걸림돌이 됩니다. 그래서 운영체제는 프로세스 사이의 통신을 위한 방법을 제공하는데, 이를 IPC(Inter-Process Communication, 프로세스 간 통신)라고 부릅니다. IPC는 크게 커널이 중개하는 메시지 전달 방식(파이프, 메시지 큐, 소켓)과, 프로세스들이 메모리 영역을 함께 쓰는 공유 메모리 방식으로 나뉩니다.
방식마다 통신 범위, 데이터 형태, 속도, 동기화 책임이 다릅니다. 파이프는 셸에서 명령어를 | 로 이을 때 쓰는 단방향 통로이고, 공유 메모리는 같은 메모리를 직접 읽고 써서 가장 빠른 편이지만 동기화를 직접 해야 하며, 소켓은 다른 컴퓨터의 프로세스와도 통신할 수 있습니다. 이 토픽에서는 파이프가 커널 버퍼를 거치며 동작하는 과정, 공유 메모리와 파이프의 비용 차이, 여러 IPC 방식의 범위와 특징을 봅니다.
핵심 개념
파이프
한쪽 끝에서 쓰고 다른 쪽 끝에서 읽는 단방향 바이트 통로입니다. 보통 fork로 만든 부모와 자식처럼 관련된 프로세스가 파이프의 끝을 물려받아 씁니다. 버퍼가 차면 쓰는 쪽이, 비면 읽는 쪽이 기다립니다.
핵심 개념
이름 있는 파이프(FIFO)
파일 시스템에 이름을 가진 파이프입니다. 이름으로 열 수 있어서 부모와 자식 관계가 아닌 프로세스도 서로 통신할 수 있습니다.
핵심 개념
메시지 큐
커널이 관리하는 큐에 메시지를 단위로 넣고 꺼내는 방식입니다. 바이트 스트림이 아니라 메시지 경계가 유지되며, 보내는 쪽과 받는 쪽이 동시에 실행 중이지 않아도 큐에 쌓아 둘 수 있습니다.
핵심 개념
공유 메모리
여러 프로세스가 같은 메모리 영역을 각자의 주소 공간에 연결해 쓰는 방식입니다. 연결한 뒤에는 접근마다 시스템 콜 없이 직접 읽고 쓰므로 빠르지만, 동시에 쓰는 문제를 막는 동기화는 직접 해야 합니다.
핵심 개념
소켓
네트워크 통신에 쓰이는 인터페이스이며 IP 주소와 포트로 다른 컴퓨터의 프로세스와도 통신합니다. 같은 컴퓨터 안에서는 유닉스 도메인 소켓으로 같은 인터페이스를 쓸 수 있습니다.
핵심 개념
시그널
프로세스에 사건을 알리는 짧은 신호입니다. 종료 요청이나 오류 같은 알림에 쓰이고, 큰 데이터를 주고받는 용도가 아닙니다.
IPC의 배경
유닉스는 작은 프로그램을 파이프로 이어 큰 일을 하게 하는 철학으로 알려져 있고, 셸의 | 연산자가 그 예입니다. 이후 시스템 V 계열의 메시지 큐, 세마포어, 공유 메모리와 POSIX의 같은 기능, 그리고 BSD에서 온 소켓이 더해져 오늘날 유닉스 계열 운영체제의 IPC 수단이 되었습니다. 윈도우에도 이름 있는 파이프, 공유 메모리(파일 매핑), 소켓 같은 대응 수단이 있습니다.
숫자로 확인하기
자주 하는 오해
면접에서 이렇게 물어요
면접 질문
IPC 방식에는 어떤 것이 있나요?
파이프, 이름 있는 파이프(FIFO), 메시지 큐, 공유 메모리, 소켓, 시그널이 있습니다. 커널이 중개하는 메시지 전달 방식과 메모리를 함께 쓰는 공유 메모리 방식으로 나눠 설명합니다.
면접 질문
공유 메모리가 빠른 이유는?
연결을 마친 뒤에는 접근할 때마다 시스템 콜을 거치지 않고 같은 메모리를 직접 읽고 써서, 메시지 전달에서 생기는 시스템 콜과 데이터 복사가 줄기 때문입니다. 대신 동기화는 직접 해야 합니다.
면접 질문
파이프와 FIFO의 차이는?
일반 파이프는 이름이 없어 부모와 자식처럼 파이프를 물려받은 프로세스끼리 쓰고, FIFO는 파일 시스템 이름이 있어 관련 없는 프로세스도 이름으로 열어 통신할 수 있습니다.
면접 질문
다른 컴퓨터의 프로세스와 통신하려면?
소켓을 씁니다. IP 주소와 포트로 상대를 지정해 네트워크를 거쳐 통신하며, 유닉스의 파이프와 공유 메모리는 같은 컴퓨터 안에서만 쓸 수 있습니다.
실제 시스템에서는
함께 보면 좋은 토픽
참고 자료
C — 파이프로 부모가 자식에게 보내기
#include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main(void) { int fd[2]; pipe(fd); /* fd[0]=읽기 끝, fd[1]=쓰기 끝 */ if (fork() == 0) { /* 자식: 읽는 쪽 */ close(fd[1]); char buf[64]; ssize_t n = read(fd[0], buf, sizeof(buf) - 1); if (n < 0) return 1; buf[n] = 0; printf("child got: %s\n", buf); return 0; } close(fd[0]); /* 부모: 쓰는 쪽 */ write(fd[1], "hello", 5); close(fd[1]); wait(NULL); }
pipe는 파일 디스크립터 두 개를 만듭니다. fd[0]은 읽기 끝, fd[1]은 쓰기 끝이며, fork하면 자식도 이 둘을 물려받아 부모와 자식이 같은 파이프를 사이에 두게 됩니다. 한 방향으로만 쓰도록 각자 쓰지 않는 끝을 닫습니다. 파이프가 비어 있는데 쓰는 끝이 열려 있으면 read는 데이터가 올 때까지 기다리고, 쓰는 끝이 모두 닫히면 0을 돌려줍니다. 양방향 통신이 필요하면 파이프를 하나 더 만들거나 소켓을 사용합니다.
C — POSIX 공유 메모리 (쓰는 쪽과 읽는 쪽)
/* 쓰는 프로세스 */ int fd = shm_open("/demo", O_CREAT | O_RDWR, 0600); ftruncate(fd, 4096); char *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0); strcpy(p, "hello"); /* 메모리에 쓰기만 하면 됨 */ /* 읽는 프로세스 (같은 이름으로 열어서 같은 영역을 매핑) */ int fd2 = shm_open("/demo", O_RDWR, 0); char *q = mmap(NULL, 4096, PROT_READ, MAP_SHARED, fd2, 0); printf("%s\n", q); shm_unlink("/demo"); /* 더 쓰지 않으면 이름 제거 */
shm_open이 이름 있는 공유 메모리 객체를 열고, ftruncate로 크기를 정한 뒤 mmap으로 각 프로세스의 주소 공간에 연결합니다. 연결 이후에는 일반 메모리처럼 포인터로 읽고 써서 시스템 콜 없이 데이터를 주고받습니다. 다만 쓰는 쪽이 다 쓰기 전에 읽는 쪽이 읽을 수 있으므로, 실제 코드에서는 세마포어나 프로세스 간 공유 뮤텍스로 순서를 맞춰야 합니다. 위 코드는 매핑 방법만 보여 주는 최소 예시입니다. 더 쓰지 않을 때 shm_unlink로 이름을 지웁니다. glibc 2.33 이하에서는 -lrt로 링크해야 하고, 2.34부터는 libc에 들어 있어 필요 없습니다(man 페이지에는 여전히 -lrt가 적혀 있습니다).
셸 · Python — FIFO와 multiprocessing
# 셸: 이름 있는 파이프 (관련 없는 두 프로세스가 이름으로 연결) mkfifo /tmp/demo.fifo cat /tmp/demo.fifo & # 읽는 쪽: 데이터가 올 때까지 대기 echo hello > /tmp/demo.fifo # 쓰는 쪽 # Python: 프로세스 사이에 파이프 / 공유 메모리 from multiprocessing import Process, Pipe, shared_memory def child(conn): conn.send("hi") if __name__ == "__main__": parent, c = Pipe() p = Process(target=child, args=(c,)); p.start() print(parent.recv()); p.join() shm = shared_memory.SharedMemory(create=True, size=16) shm.buf[:2] = b"ok" shm.close(); shm.unlink()
mkfifo가 파일 시스템에 이름 있는 파이프를 만들면 cat과 echo처럼 서로 관련 없는 프로세스도 같은 이름으로 열어 통신합니다. 읽는 쪽이 열릴 때까지 쓰는 쪽이 기다리는 식으로 한쪽이 상대를 기다립니다. 파이썬의 multiprocessing.Pipe는 두 프로세스 사이에 객체를 보내는 연결을 만들고(기본은 양방향이라 운영체제 파이프와는 다른 추상화입니다), 3.8에서 추가된 shared_memory.SharedMemory는 이름 있는 공유 메모리 블록을 만들어 buf로 직접 읽고 쓰게 합니다. 공유 메모리 블록은 다 쓴 뒤 close로 자기 쪽 연결을 닫고, 만든 쪽이 unlink로 블록 자체를 없애야 시스템에 남지 않습니다.
Q1
파이프(pipe)의 특징으로 옳은 것은?
Q2
공유 메모리(Shared Memory) 방식이 IPC 중 가장 빠른 편인 이유는?
Q3
서로 다른 컴퓨터에서 실행 중인 프로세스끼리도 통신할 수 있는 IPC 방식은?