IPC (프로세스 간 통신)
IPC(Inter Process Communication, 프로세스 간 통신)는 서로 독립된 프로세스끼리 데이터를 주고받을 수 있게 운영체제(커널)가 마련해 둔 방법들입니다. 파이프, 메시지 큐, 공유 메모리, 메모리 맵, 소켓 등이 있습니다.
한눈에 보기
- 프로세스는 독립적으로 실행됩니다. 각자 자기만의 메모리 공간을 갖고 있어서, 다른 프로세스의 영향을 받지 않습니다. (반면 스레드는 한 프로세스 안에서 자원을 공유하므로 서로 영향을 받습니다. 프로세스와 스레드 참고)
- 그런데 이렇게 독립된 프로세스끼리도 협력해야 할 때가 있습니다. 예를 들어 터미널에서
ls | grep txt를 치면ls프로세스의 출력이grep프로세스의 입력으로 넘어가야 합니다. 이것을 가능하게 해 주는 것이 IPC입니다. - 프로세스는 남의 메모리에 직접 손댈 수 없으므로, 커널이 제공하는 IPC 설비를 이용해 통신합니다.
- 커널: 운영체제의 핵심 부분으로, 다른 모든 부분에 여러 기본 서비스를 제공합니다. (운영체제란 참고)
- 비유: 프로세스들은 각자 다른 집에 사는 이웃입니다. 남의 집에 들어갈 수는 없으니, 관리사무소(커널)가 마련해 둔 방법으로 소통합니다. 한 방향 우편함(파이프), 동 게시판(메시지 큐), 함께 쓰는 공용 창고(공유 메모리), 전화(소켓)처럼요.
- IPC 설비는 종류가 여러 가지이고, 상황에 맞게 골라 써야 합니다.
┌───────────────┐ ┌───────────────┐
│ 프로세스 A │ │ 프로세스 B │
│ (자기 메모리) │ │ (자기 메모리) │
└───────┬───────┘ └───────▲───────┘
│ ① 보내기 ② 받기 │
════════▼════════════════════════════════════════╪════════
│ 커널 (IPC 설비) │
│ 파이프 · 메시지 큐 · 공유 메모리 · 소켓 ... │
══════════════════════════════════════════════════════════먼저 통신 방향을 부르는 용어 세 가지를 알아 두면 아래 내용이 쉬워집니다.
| 용어 | 뜻 | 비유 |
|---|---|---|
| 단방향 (Simplex) | 한쪽은 보내기만, 다른 쪽은 받기만 | 라디오 방송 |
| 반이중 (Half-duplex) | 양쪽 다 보낼 수 있지만 한 번에 한쪽만 | 무전기 ("오버") |
| 전이중 (Full-duplex) | 양쪽이 동시에 주고받음 | 전화 통화 |
예제는 Python으로 작성했고 macOS에서 실행해 결과를 확인했습니다.
1. 익명 파이프 (Anonymous Pipe)
- 파이프는 두 프로세스를 연결하는 통로입니다. 한 프로세스는 데이터를 쓰기만, 다른 프로세스는 읽기만 할 수 있습니다.
- 즉, 한쪽 방향으로만 흐르는 단방향 통신입니다. 그래서 양쪽으로 모두 주고받고 싶다면 파이프를 2개 만들어야 합니다.
- 이름이 없어서 파이프를 만든 프로세스와, 그 프로세스가
fork로 만든 부모-자식 사이처럼 서로를 명확히 아는 관계에서만 쓸 수 있습니다. (시스템 콜 참고) - 장점: 사용법이 매우 간단합니다. 데이터가 한 방향으로 단순하게 흐를 때는 파이프가 효율적입니다.
- 단점: 양방향 통신을 위해 파이프 2개를 만들어야 할 때는 구현이 복잡해집니다.
- 터미널의
ls | grep txt의|가 바로 익명 파이프입니다.
아래 그림은 셀에서 프로그램 세 개를 파이프로 이었을 때입니다. 앞 프로그램의 출력(stdout)이 파이프를 타고 다음 프로그램의 입력(stdin)으로 한 방향으로만 흘러갑니다. 에러 출력(stderr)은 파이프를 타지 않고 화면으로 바로 나갑니다.

그림 출처: Wikimedia Commons (XcepticZP, 퍼블릭 도메인)
import os
r, w = os.pipe() # 읽는 쪽(r), 쓰는 쪽(w) 두 개의 끝이 생김
rc = os.fork()
if rc == 0: # 자식: 쓰기만 한다
os.close(r)
os.write(w, "부모님, 안녕하세요".encode())
os.close(w)
os._exit(0)
else: # 부모: 읽기만 한다
os.close(w)
data = os.read(r, 1024).decode()
os.close(r)
os.wait()
print(f"부모가 받은 메시지: {data}")실행 결과:
부모가 받은 메시지: 부모님, 안녕하세요2. 이름 있는 파이프 (Named Pipe, FIFO)
- 익명 파이프는 부모-자식처럼 통신할 상대를 명확히 알 때 씁니다.
- 이름 있는 파이프는 서로 전혀 모르는 프로세스 사이의 통신에 씁니다. 익명 파이프를 확장한 것으로, 부모 프로세스와 무관한 다른 프로세스도 통신할 수 있습니다.
- 통신을 위해 파일 시스템에 이름 있는 특수 파일을 만들고, 그 경로를 아는 프로세스라면 누구나 열어서 쓰거나 읽을 수 있습니다. 먼저 넣은 데이터가 먼저 나와서 FIFO(First In, First Out)라고도 부릅니다.
- 하지만 이름 있는 파이프도 한 방향으로만 흐릅니다. 양방향 통신을 하려면 익명 파이프처럼 2개를 만들어야 합니다.
import os, tempfile
path = os.path.join(tempfile.mkdtemp(), "mailbox")
os.mkfifo(path) # 파일 시스템에 '이름 있는 파이프'를 만든다
if os.fork() == 0: # 보내는 쪽 (아무 프로세스나 이 경로를 알면 됨)
with open(path, "w") as f:
f.write("이름으로 찾아온 메시지")
os._exit(0)
else: # 받는 쪽
with open(path) as f:
print(f"받은 메시지: {f.read()}")
os.wait()
os.remove(path)실행 결과:
받은 메시지: 이름으로 찾아온 메시지예제에서는 편의상 fork를 썼지만, 터미널 두 개를 열어 서로 다른 프로그램에서 같은 경로를 열어도 똑같이 통신됩니다.
3. 메시지 큐 (Message Queue)
- 파이프처럼 한쪽이 넣고 다른 쪽이 꺼내는 방식이지만, 파이프가 끊김 없는 바이트의 흐름이라면 메시지 큐는 커널 안에 있는 메시지 보관 공간(메모리) 입니다.
- 데이터를 메시지 단위로 넣고 꺼내므로, 보낸 메시지 하나가 그대로 하나로 받아집니다. (파이프는 경계가 없어서 두 번 보낸 데이터가 한 번에 붙어서 읽힐 수 있습니다.)
- 메시지마다 번호(타입) 를 붙일 수 있어서, 받는 쪽이 원하는 번호의 메시지만 골라 꺼낼 수 있습니다. 그래서 여러 프로세스가 동시에 데이터를 쉽게 다룰 수 있습니다.
- 비유: 아파트 동 게시판에 "[주문] 아메리카노"처럼 머리말을 붙인 쪽지를 붙여 두면, 필요한 사람이 자기에게 해당하는 쪽지만 떼어 가는 것과 같습니다.
from multiprocessing import Process, Queue
def worker(q):
q.put(("주문", "아메리카노"))
q.put(("주문", "라떼"))
q.put(("끝", None))
if __name__ == "__main__":
q = Queue()
p = Process(target=worker, args=(q,))
p.start()
while True:
kind, item = q.get() # 넣은 순서대로, 메시지 단위로 꺼냄
if kind == "끝":
break
print(f"{kind}: {item}")
p.join()실행 결과:
주문: 아메리카노
주문: 라떼Python의
multiprocessing.Queue는 운영체제의 메시지 큐를 그대로 쓰는 것은 아니고, 내부적으로 파이프 위에 메시지 단위 주고받기를 구현한 것입니다. 메시지를 하나씩 넣고 꺼낸다는 개념을 보여 주는 예제로 봐 주세요.
4. 공유 메모리 (Shared Memory)
- 파이프와 메시지 큐가 데이터를 보내고 받는(통신하는) 설비라면, 공유 메모리는 데이터 자체를 함께 쓰도록 해 주는 설비입니다.
- 원래 프로세스의 메모리 영역은 독립적이고, 다른 프로세스가 접근하지 못하도록 반드시 보호되어야 합니다. 하지만 다른 프로세스와 데이터를 같이 써야 하는 상황도 있습니다. 파이프로 데이터를 보낼 수도 있지만, 스레드처럼 메모리를 함께 쓸 수 있다면 더 편하겠죠.
- 공유 메모리는 여러 프로세스가 같은 메모리 영역을 함께 쓰도록 허용해 줍니다.
- 한 프로세스가 커널에 공유 메모리를 요청합니다.
- 커널이 메모리 공간을 만들어 줍니다.
- 이후 허락받은 프로세스들은 그 영역을 자기 메모리처럼 바로 읽고 씁니다.
- 중개자(커널) 없이 곧바로 메모리에 접근하므로, IPC 중에서 가장 빠릅니다. 데이터를 복사해서 보낼 필요가 없기 때문입니다.
- 대신 여러 프로세스가 동시에 같은 곳을 고치면 값이 꼬일 수 있어서, 동기화를 직접 챙겨야 합니다. (경쟁 상태 참고)
from multiprocessing import Process, Value
def worker(counter):
with counter.get_lock(): # 동시에 고치지 않도록 잠금
counter.value += 100 # 복사 없이 같은 메모리를 바로 고침
if __name__ == "__main__":
counter = Value("i", 0) # 여러 프로세스가 같이 보는 정수 하나
workers = [Process(target=worker, args=(counter,)) for _ in range(3)]
for p in workers:
p.start()
for p in workers:
p.join()
print(f"최종 값: {counter.value}")실행 결과:
최종 값: 300프로세스 3개가 각자 100씩 더했는데, 모두 같은 메모리를 고쳤기 때문에 부모가 300을 봅니다. fork로 복사된 변수는 자식이 고쳐도 부모 쪽이 그대로였던 것(시스템 콜 참고)과 비교해 보세요. get_lock()으로 잠그는 이유는 아래 "동기화" 부분에서 설명합니다.
5. 메모리 맵 (Memory-Mapped File)
- 공유 메모리처럼 메모리를 함께 씁니다. 다만 열린 파일을 메모리에 연결(매핑)해서 공유하는 방식입니다. 즉, 공유하는 매개체가 파일 + 메모리입니다.
- 메모리를 고치면 그 내용이 파일에도 반영되고, 같은 파일을 매핑한 다른 프로세스도 바뀐 내용을 봅니다.
- 주로 파일로 된 대용량 데이터를 공유해야 할 때 씁니다. 파일을 일일이 읽고 쓰는 대신 메모리처럼 다룰 수 있어 편하고 빠릅니다.
import mmap, os, tempfile
path = os.path.join(tempfile.mkdtemp(), "shared.bin")
with open(path, "wb") as f:
f.write(b"before")
with open(path, "r+b") as f:
mm = mmap.mmap(f.fileno(), 0) # 파일 내용을 메모리에 그대로 비춘다
if os.fork() == 0:
mm[:6] = b"after!" # 자식이 메모리를 고치면
mm.flush()
os._exit(0)
os.wait()
print(f"메모리에서 읽기: {mm[:6].decode()}")
with open(path, "rb") as f:
print(f"파일에서 읽기: {f.read().decode()}") # 파일에도 반영됨실행 결과:
메모리에서 읽기: after!
파일에서 읽기: after!자식이 메모리를 고쳤는데 부모의 메모리에도, 디스크의 파일에도 반영되었습니다.
6. 소켓 (Socket)
- 네트워크 소켓 통신으로 데이터를 주고받습니다. 웹 브라우저와 웹 서버가 통신하는 바로 그 방식입니다.
- 클라이언트와 서버가 소켓을 통해 통신하는 구조로, 다른 컴퓨터(원격)에 있는 프로세스와도 데이터를 주고받을 수 있습니다. 같은 컴퓨터 안의 프로세스끼리도 쓸 수 있습니다(유닉스 도메인 소켓).
- 하나의 연결로 양쪽이 동시에 주고받는 전이중 통신이 가능합니다.
- 서버와 클라이언트가 쓰는 함수는 다음과 같습니다.
| 쪽 | 순서 | 하는 일 |
|---|---|---|
| 서버 | socket → bind | 소켓을 만들고 주소(IP, 포트)를 붙임 |
| 서버 | listen | 연결 요청을 받을 준비 |
| 서버 | accept | 들어온 연결 요청을 수락 |
| 클라이언트 | socket → connect | 소켓을 만들고 서버에 연결 요청 |
| 둘 다 | send / recv | 데이터 주고받기 |
아래는 이미 서로 연결된 소켓 한 쌍(socketpair)으로, 하나의 연결에서 보내고 답장까지 받는 모습입니다.
import os, socket
parent_sock, child_sock = socket.socketpair() # 서로 연결된 소켓 한 쌍
if os.fork() == 0:
parent_sock.close()
msg = child_sock.recv(1024).decode()
child_sock.sendall(f"'{msg}' 잘 받았어요".encode()) # 같은 소켓으로 답장
os._exit(0)
else:
child_sock.close()
parent_sock.sendall("안녕".encode())
print(parent_sock.recv(1024).decode())
os.wait()실행 결과:
'안녕' 잘 받았어요파이프와 달리 소켓 하나로 보내기와 받기를 모두 했습니다.
방식별 비교
| 방식 | 방향 | 통신 상대 | 특징 |
|---|---|---|---|
| 익명 파이프 | 단방향 | 부모-자식처럼 관계가 있는 프로세스 | 가장 간단. 양방향이면 2개 필요 |
| 이름 있는 파이프 | 단방향 | 경로만 알면 아무 프로세스나 | 파일 이름으로 찾아옴 |
| 메시지 큐 | 단방향 (큐 하나 기준) | 여러 프로세스 | 메시지 단위, 번호로 골라 받기 |
| 공유 메모리 | 양방향 | 허락받은 프로세스 | 가장 빠름. 동기화는 직접 챙겨야 함 |
| 메모리 맵 | 양방향 | 같은 파일을 연 프로세스 | 대용량 파일 공유에 유리 |
| 소켓 | 전이중 | 같은 컴퓨터 또는 원격 | 네트워크 너머까지 통신 가능 |
IPC와 동기화
여러 프로세스가 같은 자원(공유 메모리, 파일 등)을 함께 쓰면, 동시에 고치다가 값이 꼬일 수 있습니다. 그래서 IPC에서 프로세스 간 데이터를 동기화하고 보호하기 위해 세마포어(Semaphore)와 뮤텍스(Mutex) 를 사용합니다. 공유된 자원에 한 번에 하나의 프로세스만 접근하게 만드는 도구입니다. 위 공유 메모리 예제의 get_lock()이 바로 이 역할을 합니다.
자세한 내용은 경쟁 상태와 세마포어와 뮤텍스 글에서 다루고, 잠금을 잘못 쓰면 생기는 문제는 데드락 글을 참고하세요.
정리
| 개념 | 비전공자 버전 요약 |
|---|---|
| IPC | 서로 남의 집에 못 들어가는 프로세스들이 관리사무소(커널)를 통해 소통하는 방법 |
| 익명 파이프 | 부모와 자식 사이의 한 방향 우편함 |
| 이름 있는 파이프 (FIFO) | 주소(파일 이름)만 알면 누구나 쓸 수 있는 한 방향 우편함 |
| 메시지 큐 | 머리말 붙은 쪽지를 붙이고 골라 떼어 가는 게시판 |
| 공유 메모리 | 여러 집이 함께 쓰는 공용 창고. 가장 빠르지만 순서 정하기가 필요 |
| 메모리 맵 | 파일을 메모리에 비춰서 함께 보는 방식. 큰 파일 공유에 좋음 |
| 소켓 | 멀리 있는 컴퓨터와도 양쪽으로 동시에 대화하는 전화 |
| 세마포어 / 뮤텍스 | 공용 자원에 한 번에 한 명만 들어가게 하는 열쇠 |
