Skip to content

IPC (프로세스 간 통신) ​

IPC(Inter Process Communication, 프로세스 간 통신)는 서로 독립된 프로세스끼리 데이터를 주고받을 수 있게 운영체제(커널)가 마련해 둔 방법들입니다. 파이프, 메시지 큐, 공유 메모리, 메모리 맵, 소켓 등이 있습니다.

한눈에 보기 ​

  • 프로세스는 독립적으로 실행됩니다. 각자 자기만의 메모리 공간을 갖고 있어서, 다른 프로세스의 영향을 받지 않습니다. (반면 스레드는 한 프로세스 안에서 자원을 공유하므로 서로 영향을 받습니다. 프로세스와 스레드 참고)
  • 그런데 이렇게 독립된 프로세스끼리도 협력해야 할 때가 있습니다. 예를 들어 터미널에서 ls | grep txt를 치면 ls 프로세스의 출력이 grep 프로세스의 입력으로 넘어가야 합니다. 이것을 가능하게 해 주는 것이 IPC입니다.
  • 프로세스는 남의 메모리에 직접 손댈 수 없으므로, 커널이 제공하는 IPC 설비를 이용해 통신합니다.
    • 커널: 운영체제의 핵심 부분으로, 다른 모든 부분에 여러 기본 서비스를 제공합니다. (운영체제란 참고)
  • 비유: 프로세스들은 각자 다른 집에 사는 이웃입니다. 남의 집에 들어갈 수는 없으니, 관리사무소(커널)가 마련해 둔 방법으로 소통합니다. 한 방향 우편함(파이프), 동 게시판(메시지 큐), 함께 쓰는 공용 창고(공유 메모리), 전화(소켓)처럼요.
  • IPC 설비는 종류가 여러 가지이고, 상황에 맞게 골라 써야 합니다.
text
 ┌───────────────┐                        ┌───────────────┐
 │  프로세스 A    │                        │  프로세스 B    │
 │ (자기 메모리)  │                        │ (자기 메모리)  │
 └───────┬───────┘                        └───────▲───────┘
         │  ① 보내기                    ② 받기   │
 ════════▼════════════════════════════════════════╪════════
 │                 커널 (IPC 설비)                         │
 │   파이프 · 메시지 큐 · 공유 메모리 · 소켓 ...             │
 ══════════════════════════════════════════════════════════

먼저 통신 방향을 부르는 용어 세 가지를 알아 두면 아래 내용이 쉬워집니다.

용어뜻비유
단방향 (Simplex)한쪽은 보내기만, 다른 쪽은 받기만라디오 방송
반이중 (Half-duplex)양쪽 다 보낼 수 있지만 한 번에 한쪽만무전기 ("오버")
전이중 (Full-duplex)양쪽이 동시에 주고받음전화 통화

예제는 Python으로 작성했고 macOS에서 실행해 결과를 확인했습니다.

1. 익명 파이프 (Anonymous Pipe) ​

  • 파이프는 두 프로세스를 연결하는 통로입니다. 한 프로세스는 데이터를 쓰기만, 다른 프로세스는 읽기만 할 수 있습니다.
  • 즉, 한쪽 방향으로만 흐르는 단방향 통신입니다. 그래서 양쪽으로 모두 주고받고 싶다면 파이프를 2개 만들어야 합니다.
  • 이름이 없어서 파이프를 만든 프로세스와, 그 프로세스가 fork로 만든 부모-자식 사이처럼 서로를 명확히 아는 관계에서만 쓸 수 있습니다. (시스템 콜 참고)
  • 장점: 사용법이 매우 간단합니다. 데이터가 한 방향으로 단순하게 흐를 때는 파이프가 효율적입니다.
  • 단점: 양방향 통신을 위해 파이프 2개를 만들어야 할 때는 구현이 복잡해집니다.
  • 터미널의 ls | grep txt의 |가 바로 익명 파이프입니다.

아래 그림은 셀에서 프로그램 세 개를 파이프로 이었을 때입니다. 앞 프로그램의 출력(stdout)이 파이프를 타고 다음 프로그램의 입력(stdin)으로 한 방향으로만 흘러갑니다. 에러 출력(stderr)은 파이프를 타지 않고 화면으로 바로 나갑니다.

키보드 입력이 Program 1, 2, 3을 파이프로 차례로 거쳐 화면에 출력되는 흐름

그림 출처: Wikimedia Commons (XcepticZP, 퍼블릭 도메인)

python
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}")

실행 결과:

text
부모가 받은 메시지: 부모님, 안녕하세요

2. 이름 있는 파이프 (Named Pipe, FIFO) ​

  • 익명 파이프는 부모-자식처럼 통신할 상대를 명확히 알 때 씁니다.
  • 이름 있는 파이프는 서로 전혀 모르는 프로세스 사이의 통신에 씁니다. 익명 파이프를 확장한 것으로, 부모 프로세스와 무관한 다른 프로세스도 통신할 수 있습니다.
  • 통신을 위해 파일 시스템에 이름 있는 특수 파일을 만들고, 그 경로를 아는 프로세스라면 누구나 열어서 쓰거나 읽을 수 있습니다. 먼저 넣은 데이터가 먼저 나와서 FIFO(First In, First Out)라고도 부릅니다.
  • 하지만 이름 있는 파이프도 한 방향으로만 흐릅니다. 양방향 통신을 하려면 익명 파이프처럼 2개를 만들어야 합니다.
python
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)

실행 결과:

text
받은 메시지: 이름으로 찾아온 메시지

예제에서는 편의상 fork를 썼지만, 터미널 두 개를 열어 서로 다른 프로그램에서 같은 경로를 열어도 똑같이 통신됩니다.

3. 메시지 큐 (Message Queue) ​

  • 파이프처럼 한쪽이 넣고 다른 쪽이 꺼내는 방식이지만, 파이프가 끊김 없는 바이트의 흐름이라면 메시지 큐는 커널 안에 있는 메시지 보관 공간(메모리) 입니다.
  • 데이터를 메시지 단위로 넣고 꺼내므로, 보낸 메시지 하나가 그대로 하나로 받아집니다. (파이프는 경계가 없어서 두 번 보낸 데이터가 한 번에 붙어서 읽힐 수 있습니다.)
  • 메시지마다 번호(타입) 를 붙일 수 있어서, 받는 쪽이 원하는 번호의 메시지만 골라 꺼낼 수 있습니다. 그래서 여러 프로세스가 동시에 데이터를 쉽게 다룰 수 있습니다.
  • 비유: 아파트 동 게시판에 "[주문] 아메리카노"처럼 머리말을 붙인 쪽지를 붙여 두면, 필요한 사람이 자기에게 해당하는 쪽지만 떼어 가는 것과 같습니다.
python
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()

실행 결과:

text
주문: 아메리카노
주문: 라떼

Python의 multiprocessing.Queue는 운영체제의 메시지 큐를 그대로 쓰는 것은 아니고, 내부적으로 파이프 위에 메시지 단위 주고받기를 구현한 것입니다. 메시지를 하나씩 넣고 꺼낸다는 개념을 보여 주는 예제로 봐 주세요.

4. 공유 메모리 (Shared Memory) ​

  • 파이프와 메시지 큐가 데이터를 보내고 받는(통신하는) 설비라면, 공유 메모리는 데이터 자체를 함께 쓰도록 해 주는 설비입니다.
  • 원래 프로세스의 메모리 영역은 독립적이고, 다른 프로세스가 접근하지 못하도록 반드시 보호되어야 합니다. 하지만 다른 프로세스와 데이터를 같이 써야 하는 상황도 있습니다. 파이프로 데이터를 보낼 수도 있지만, 스레드처럼 메모리를 함께 쓸 수 있다면 더 편하겠죠.
  • 공유 메모리는 여러 프로세스가 같은 메모리 영역을 함께 쓰도록 허용해 줍니다.
    1. 한 프로세스가 커널에 공유 메모리를 요청합니다.
    2. 커널이 메모리 공간을 만들어 줍니다.
    3. 이후 허락받은 프로세스들은 그 영역을 자기 메모리처럼 바로 읽고 씁니다.
  • 중개자(커널) 없이 곧바로 메모리에 접근하므로, IPC 중에서 가장 빠릅니다. 데이터를 복사해서 보낼 필요가 없기 때문입니다.
  • 대신 여러 프로세스가 동시에 같은 곳을 고치면 값이 꼬일 수 있어서, 동기화를 직접 챙겨야 합니다. (경쟁 상태 참고)
python
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}")

실행 결과:

text
최종 값: 300

프로세스 3개가 각자 100씩 더했는데, 모두 같은 메모리를 고쳤기 때문에 부모가 300을 봅니다. fork로 복사된 변수는 자식이 고쳐도 부모 쪽이 그대로였던 것(시스템 콜 참고)과 비교해 보세요. get_lock()으로 잠그는 이유는 아래 "동기화" 부분에서 설명합니다.

5. 메모리 맵 (Memory-Mapped File) ​

  • 공유 메모리처럼 메모리를 함께 씁니다. 다만 열린 파일을 메모리에 연결(매핑)해서 공유하는 방식입니다. 즉, 공유하는 매개체가 파일 + 메모리입니다.
  • 메모리를 고치면 그 내용이 파일에도 반영되고, 같은 파일을 매핑한 다른 프로세스도 바뀐 내용을 봅니다.
  • 주로 파일로 된 대용량 데이터를 공유해야 할 때 씁니다. 파일을 일일이 읽고 쓰는 대신 메모리처럼 다룰 수 있어 편하고 빠릅니다.
python
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()}")   # 파일에도 반영됨

실행 결과:

text
메모리에서 읽기: after!
파일에서 읽기:   after!

자식이 메모리를 고쳤는데 부모의 메모리에도, 디스크의 파일에도 반영되었습니다.

6. 소켓 (Socket) ​

  • 네트워크 소켓 통신으로 데이터를 주고받습니다. 웹 브라우저와 웹 서버가 통신하는 바로 그 방식입니다.
  • 클라이언트와 서버가 소켓을 통해 통신하는 구조로, 다른 컴퓨터(원격)에 있는 프로세스와도 데이터를 주고받을 수 있습니다. 같은 컴퓨터 안의 프로세스끼리도 쓸 수 있습니다(유닉스 도메인 소켓).
  • 하나의 연결로 양쪽이 동시에 주고받는 전이중 통신이 가능합니다.
  • 서버와 클라이언트가 쓰는 함수는 다음과 같습니다.
쪽순서하는 일
서버socket → bind소켓을 만들고 주소(IP, 포트)를 붙임
서버listen연결 요청을 받을 준비
서버accept들어온 연결 요청을 수락
클라이언트socket → connect소켓을 만들고 서버에 연결 요청
둘 다send / recv데이터 주고받기

아래는 이미 서로 연결된 소켓 한 쌍(socketpair)으로, 하나의 연결에서 보내고 답장까지 받는 모습입니다.

python
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()

실행 결과:

text
'안녕' 잘 받았어요

파이프와 달리 소켓 하나로 보내기와 받기를 모두 했습니다.

방식별 비교 ​

방식방향통신 상대특징
익명 파이프단방향부모-자식처럼 관계가 있는 프로세스가장 간단. 양방향이면 2개 필요
이름 있는 파이프단방향경로만 알면 아무 프로세스나파일 이름으로 찾아옴
메시지 큐단방향 (큐 하나 기준)여러 프로세스메시지 단위, 번호로 골라 받기
공유 메모리양방향허락받은 프로세스가장 빠름. 동기화는 직접 챙겨야 함
메모리 맵양방향같은 파일을 연 프로세스대용량 파일 공유에 유리
소켓전이중같은 컴퓨터 또는 원격네트워크 너머까지 통신 가능

IPC와 동기화 ​

여러 프로세스가 같은 자원(공유 메모리, 파일 등)을 함께 쓰면, 동시에 고치다가 값이 꼬일 수 있습니다. 그래서 IPC에서 프로세스 간 데이터를 동기화하고 보호하기 위해 세마포어(Semaphore)와 뮤텍스(Mutex) 를 사용합니다. 공유된 자원에 한 번에 하나의 프로세스만 접근하게 만드는 도구입니다. 위 공유 메모리 예제의 get_lock()이 바로 이 역할을 합니다.

자세한 내용은 경쟁 상태와 세마포어와 뮤텍스 글에서 다루고, 잠금을 잘못 쓰면 생기는 문제는 데드락 글을 참고하세요.

정리 ​

개념비전공자 버전 요약
IPC서로 남의 집에 못 들어가는 프로세스들이 관리사무소(커널)를 통해 소통하는 방법
익명 파이프부모와 자식 사이의 한 방향 우편함
이름 있는 파이프 (FIFO)주소(파일 이름)만 알면 누구나 쓸 수 있는 한 방향 우편함
메시지 큐머리말 붙은 쪽지를 붙이고 골라 떼어 가는 게시판
공유 메모리여러 집이 함께 쓰는 공용 창고. 가장 빠르지만 순서 정하기가 필요
메모리 맵파일을 메모리에 비춰서 함께 보는 방식. 큰 파일 공유에 좋음
소켓멀리 있는 컴퓨터와도 양쪽으로 동시에 대화하는 전화
세마포어 / 뮤텍스공용 자원에 한 번에 한 명만 들어가게 하는 열쇠