컴퓨터 구조 운영체제 네트워크 자료구조 알고리즘 데이터베이스 내 학습
os · 14

프로세스 간 통신(IPC)

공유메모리파이프IPC

프로세스는 서로 독립된 주소 공간을 가지므로 한 프로세스는 다른 프로세스의 메모리를 마음대로 읽거나 쓸 수 없습니다. 이 격리는 안정성과 보안에 좋지만, 프로세스끼리 데이터를 주고받거나 서로의 작업을 맞춰야 할 때는 걸림돌이 됩니다. 그래서 운영체제는 프로세스 사이의 통신을 위한 방법을 제공하는데, 이를 IPC(Inter-Process Communication, 프로세스 간 통신)라고 부릅니다. IPC는 크게 커널이 중개하는 메시지 전달 방식(파이프, 메시지 큐, 소켓)과, 프로세스들이 메모리 영역을 함께 쓰는 공유 메모리 방식으로 나뉩니다.

방식마다 통신 범위, 데이터 형태, 속도, 동기화 책임이 다릅니다. 파이프는 셸에서 명령어를 | 로 이을 때 쓰는 단방향 통로이고, 공유 메모리는 같은 메모리를 직접 읽고 써서 가장 빠른 편이지만 동기화를 직접 해야 하며, 소켓은 다른 컴퓨터의 프로세스와도 통신할 수 있습니다. 이 토픽에서는 파이프가 커널 버퍼를 거치며 동작하는 과정, 공유 메모리와 파이프의 비용 차이, 여러 IPC 방식의 범위와 특징을 봅니다.

핵심 개념

파이프

한쪽 끝에서 쓰고 다른 쪽 끝에서 읽는 단방향 바이트 통로입니다. 보통 fork로 만든 부모와 자식처럼 관련된 프로세스가 파이프의 끝을 물려받아 씁니다. 버퍼가 차면 쓰는 쪽이, 비면 읽는 쪽이 기다립니다.

핵심 개념

이름 있는 파이프(FIFO)

파일 시스템에 이름을 가진 파이프입니다. 이름으로 열 수 있어서 부모와 자식 관계가 아닌 프로세스도 서로 통신할 수 있습니다.

핵심 개념

메시지 큐

커널이 관리하는 큐에 메시지를 단위로 넣고 꺼내는 방식입니다. 바이트 스트림이 아니라 메시지 경계가 유지되며, 보내는 쪽과 받는 쪽이 동시에 실행 중이지 않아도 큐에 쌓아 둘 수 있습니다.

핵심 개념

공유 메모리

여러 프로세스가 같은 메모리 영역을 각자의 주소 공간에 연결해 쓰는 방식입니다. 연결한 뒤에는 접근마다 시스템 콜 없이 직접 읽고 쓰므로 빠르지만, 동시에 쓰는 문제를 막는 동기화는 직접 해야 합니다.

핵심 개념

소켓

네트워크 통신에 쓰이는 인터페이스이며 IP 주소와 포트로 다른 컴퓨터의 프로세스와도 통신합니다. 같은 컴퓨터 안에서는 유닉스 도메인 소켓으로 같은 인터페이스를 쓸 수 있습니다.

핵심 개념

시그널

프로세스에 사건을 알리는 짧은 신호입니다. 종료 요청이나 오류 같은 알림에 쓰이고, 큰 데이터를 주고받는 용도가 아닙니다.

격리된 프로세스 사이의 통로입니다 — 프로세스끼리는 메모리가 격리되어 있어 운영체제가 마련한 통로를 거쳐야 데이터를 주고받습니다.
커널 중개는 편하고 공유 메모리는 빠릅니다 — 메시지 전달은 커널이 순서와 대기를 처리해 주지만 복사와 시스템 콜 비용이 들고, 공유 메모리는 이 비용이 작은 대신 동기화를 직접 챙겨야 합니다.
통신 범위를 먼저 봅니다 — 다른 컴퓨터와 통신해야 하면 소켓이고, 같은 컴퓨터라면 파이프, 큐, 공유 메모리 등 여러 선택지가 있습니다.
동기화는 공유 메모리의 몫입니다 — 두 프로세스가 같은 메모리를 동시에 고치면 값이 어긋나므로 세마포어나 뮤텍스로 접근 순서를 맞춰야 합니다.
이 시각화에서 확인할 것 — "파이프" 탭에서 버퍼가 차고 비며 쓰는 쪽이 기다리는 과정을, "공유 메모리 vs 파이프" 탭에서 시스템 콜과 복사 횟수의 차이를, "방식 비교" 탭에서 방식별 통신 범위를 STEP으로 확인하세요.

IPC의 배경

유닉스는 작은 프로그램을 파이프로 이어 큰 일을 하게 하는 철학으로 알려져 있고, 셸의 | 연산자가 그 예입니다. 이후 시스템 V 계열의 메시지 큐, 세마포어, 공유 메모리와 POSIX의 같은 기능, 그리고 BSD에서 온 소켓이 더해져 오늘날 유닉스 계열 운영체제의 IPC 수단이 되었습니다. 윈도우에도 이름 있는 파이프, 공유 메모리(파일 매핑), 소켓 같은 대응 수단이 있습니다.

숫자로 확인하기

파이프 버퍼 — 이 시각화는 버퍼를 4칸으로 그립니다. 칸 수는 설명용이며, 리눅스의 파이프 용량은 보통 65,536바이트(4KiB 페이지 16개)이고 설정으로 바꿀 수 있습니다.
메시지 5개 전달(단순화한 값) — 파이프는 메시지마다 write와 read 시스템 콜 2번과 복사 2번이 들어 시스템 콜 10번·복사 10번입니다. 공유 메모리는 연결 때 시스템 콜 2번을 쓰고 이후 추가 복사가 없습니다.
동기화가 없을 때 — 두 프로세스가 같은 카운터를 각자 1씩 올리려고 둘 다 0을 읽고 1을 쓰면 최종 값이 2가 아니라 1입니다.

자주 하는 오해

파이프는 양방향이다 — 기본 파이프는 한 방향입니다. 양방향이 필요하면 파이프를 두 개 쓰거나 소켓을 씁니다.
공유 메모리는 동기화가 필요 없다 — 오히려 커널이 순서를 보장하지 않으므로 사용자가 동기화해야 합니다.
소켓은 네트워크 전용이다 — 같은 컴퓨터의 프로세스끼리도 유닉스 도메인 소켓으로 통신할 수 있습니다.
스레드와 프로세스의 통신은 같다 — 같은 프로세스의 스레드는 메모리를 이미 공유하므로 IPC 없이 변수로 통신합니다. 이때는 동기화가 핵심입니다.

면접에서 이렇게 물어요

면접 질문

IPC 방식에는 어떤 것이 있나요?

파이프, 이름 있는 파이프(FIFO), 메시지 큐, 공유 메모리, 소켓, 시그널이 있습니다. 커널이 중개하는 메시지 전달 방식과 메모리를 함께 쓰는 공유 메모리 방식으로 나눠 설명합니다.

면접 질문

공유 메모리가 빠른 이유는?

연결을 마친 뒤에는 접근할 때마다 시스템 콜을 거치지 않고 같은 메모리를 직접 읽고 써서, 메시지 전달에서 생기는 시스템 콜과 데이터 복사가 줄기 때문입니다. 대신 동기화는 직접 해야 합니다.

면접 질문

파이프와 FIFO의 차이는?

일반 파이프는 이름이 없어 부모와 자식처럼 파이프를 물려받은 프로세스끼리 쓰고, FIFO는 파일 시스템 이름이 있어 관련 없는 프로세스도 이름으로 열어 통신할 수 있습니다.

면접 질문

다른 컴퓨터의 프로세스와 통신하려면?

소켓을 씁니다. IP 주소와 포트로 상대를 지정해 네트워크를 거쳐 통신하며, 유닉스의 파이프와 공유 메모리는 같은 컴퓨터 안에서만 쓸 수 있습니다.

실제 시스템에서는

셸의 파이프 — ls | grep txt처럼 | 로 이은 명령어들은 한 명령어의 표준 출력을 다음 명령어의 표준 입력에 파이프로 연결합니다.
데이터베이스 — PostgreSQL은 접속마다 서버 프로세스를 두고, 이 프로세스들이 공유 메모리의 공유 버퍼를 함께 사용합니다.
리눅스 문서 — pipe(7)는 파이프의 용량과 가득 차거나 비었을 때의 블로킹 동작을, shm_overview(7)는 POSIX 공유 메모리의 사용 순서를 설명합니다.
파이썬 — multiprocessing 모듈은 Pipe와 Queue를 제공하고, multiprocessing.shared_memory는 프로세스 사이에 공유 메모리 블록을 만들어 줍니다.

함께 보면 좋은 토픽

프로세스 — 독립된 주소 공간을 가진 프로세스가 무엇인지 봅니다.
프로세스 동기화 — 공유 메모리에서 필요한 세마포어와 뮤텍스를 봅니다.
시스템 콜 — 파이프와 공유 메모리가 시스템 콜로 열리는 과정을 봅니다.
메모리 관리 — 공유 메모리가 같은 물리 메모리에 매핑되는 바탕을 봅니다.

참고 자료

교재 — Silberschatz 외, "Operating System Concepts"의 프로세스 간 통신 절, W. Richard Stevens, "Advanced Programming in the UNIX Environment"

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)의 특징으로 옳은 것은?

셸에서 명령어를 | 로 연결하던 것을 떠올려 보세요.
일반 파이프는 한쪽에서 쓰고 다른 쪽에서 읽는 단방향 바이트 스트림이며, 보통 fork로 만든 부모-자식처럼 관련된 프로세스 간에 사용합니다. 관련 없는 프로세스는 이름 있는 파이프(FIFO)를 씁니다.

Q2

공유 메모리(Shared Memory) 방식이 IPC 중 가장 빠른 편인 이유는?

대신 동시에 쓰지 않도록 동기화는 직접 해야 합니다.
공유 메모리는 초기 설정 이후에는 커널을 거치지 않고 같은 메모리 영역을 직접 읽고 쓰므로 빠릅니다. 대신 동시 접근에 대한 동기화(세마포어, 뮤텍스 등)는 개발자가 책임져야 합니다.

Q3

서로 다른 컴퓨터에서 실행 중인 프로세스끼리도 통신할 수 있는 IPC 방식은?

네트워크 통신에 쓰이는 바로 그 API입니다.
소켓은 IP 주소와 포트를 이용해 네트워크를 통해 통신하므로 서로 다른 컴퓨터의 프로세스끼리도 통신할 수 있습니다. 파이프와 공유 메모리는 같은 컴퓨터 안에서만 사용합니다.