CS — ipc
답변 흐름 — 왜 IPC가 필요한가(주소 공간 격리) → 분류축 3개 → 파이프·메시지 큐·공유 메모리·동기화 객체·시그널·소켓·RPC/COM → 비용·보안 트레이드오프 → 언리얼 적용
프로세스끼리 통신할 수 있는 방법(IPC, Inter-Process Communication)에 대해 설명해 주세요
모의면접 주제: “프로세스끼리 통신할 수 있는 방법(IPC, Inter-Process Communication)에 대해 설명해 주세요” 왜 IPC가 필요한가(주소 공간 격리·커널 모드 경유) → 분류축 3개(데이터 vs 신호 / 커널 경유 vs 사용자 경유 / 같은 머신 vs 네트워크) → 파이프(익명·명명) → 메시지 큐(POSIX·Windows PostMessage/Mailslot) → 공유 메모리·메모리 맵 파일 → 세마포어·뮤텍스·Event(커널 객체) → 시그널·Windows Event Object → 소켓(UNIX domain·TCP/UDP) → RPC / COM → 보안·동기화 트레이드오프 → 언리얼(
FPlatformProcess·MessageBus) → 꼬리질문 경로
학습 영역 — 프로세스 vs 스레드(19)·컨텍스트 스위칭(21)에서 파생된 OS·동시성 회귀
프로세스 vs 스레드(19)에서 프로세스/스레드의 메모리 구조를 다루며 “스레드는 같은 주소 공간을 공유하지만 프로세스는 격리돼 있다”는 핵심 차이를 봤고, 컨텍스트 스위칭(21)에서 그 격리를 지키며 실행 주체를 갈아끼우는 컨텍스트 스위칭의 비용 구조를 봤습니다. 22번은 그 격리를 깨지 않으면서 프로세스끼리 데이터를 주고받는 메커니즘, 즉 IPC가 본 주제입니다.
1
2
3
4
5
6
7
8
01번 메모리 4영역 (Code/Data/Heap/Stack) ← 주소 공간의 기초
─────────────────────────────────────────────
19번 프로세스 vs 스레드 ← 주소 공간 격리·PCB·핸들 테이블
20번 Stack Overflow (스레드별 독립 스택) ← 스레드별 메모리 분리
21번 Context Switching (★) ← 커널 모드 진입·모드 스위치
─────────────────────────────────────────────
22번 IPC (★) ← 본 주제 — 격리된 프로세스 간 통신
이후 가상 메모리·페이징 / 시스템 콜 ABI / 네트워크 소켓
IPC는 OS가 강제하는 주소 공간 격리(address space isolation) 위에서 동작합니다. 프로세스 A가 자기 메모리에 적은 값을 프로세스 B가 그냥 읽을 수 없다는 게 OS의 기본 약속입니다(그렇지 않으면 한 프로세스의 버그가 다른 프로세스를 망가뜨립니다). 그 약속을 깨지 않으면서 데이터를 옮기려면 반드시 커널이 중재해야 하고, 그래서 모든 IPC는 결국 시스템 콜과 커널 자료구조 위에 만들어집니다. 예외가 공유 메모리인데, 이것조차도 “메모리를 공유하자”는 합의는 커널이 한 번 세팅해주고 그 뒤로만 사용자 모드에서 직접 접근합니다.
Windows는 특히 CreateNamedPipe·CreateFileMapping·PostMessage·Mailslot·RPC·COM까지 풍부한 IPC API 계층을 가지고 있어서, 같은 “데이터를 옮긴다”는 작업이 수십 ns(공유 메모리 한 번 읽기) 부터 수십 μs(RPC marshalling) 까지 4~5자릿수 비용 차이를 만듭니다. 그래서 IPC 선택은 컨텍스트 스위칭(21)의 동기화 객체 선택과 똑같은 의사결정 구조를 가집니다 — 얼마나 빈번한가, 데이터 크기는 얼마인가, 격리/보안이 얼마나 중요한가.
모의면접 답변
IPC(Inter-Process Communication, 프로세스 간 통신)는 OS가 강제하는 주소 공간 격리를 깨지 않으면서 서로 다른 프로세스끼리 데이터를 주고받거나 동기화 신호를 교환하는 메커니즘입니다. 프로세스 vs 스레드(19)에서 봤듯이 같은 프로세스 안의 스레드들은 코드·데이터·힙을 공유하므로 그냥 전역 변수에 써도 통신이 되지만, 프로세스끼리는 가상 주소 공간이 다르고 페이지 테이블도 다르므로 같은 가상 주소가 서로 다른 물리 메모리를 가리킵니다. 그래서 통신하려면 반드시 커널이 한 번 중재해야 하고, IPC는 그 중재 방식의 카탈로그입니다.
IPC 방식은 세 가지 축으로 분류할 수 있습니다. 이걸 먼저 정리하면 개별 메커니즘을 비교하기 쉽습니다.
- 데이터 전송 vs 신호 전송 — 파이프·메시지 큐·공유 메모리·소켓은 데이터를 옮기는 채널, 시그널·Event·세마포어는 “어떤 일이 일어났다”는 신호를 알리는 도구입니다.
- 커널 경유 vs 사용자 모드 직접 — 파이프·메시지 큐·소켓·시그널은 매 송수신마다 커널을 거치고, 공유 메모리는 초기 세팅만 커널에서 하고 그 뒤로는 사용자 모드에서 직접 읽고 씁니다.
- 같은 머신 vs 네트워크 가능 — 파이프(익명)·공유 메모리·시그널은 같은 머신만, 명명 파이프·소켓·RPC는 같은 머신과 네트워크 모두 가능합니다.
각 메커니즘을 비용 순으로 보면 큰 그림이 보입니다.
- 공유 메모리(shared memory) 는 가장 빠릅니다. 두 프로세스가 같은 물리 페이지를 자기 가상 주소 공간에 매핑해두면, 그 뒤로는 일반 메모리 접근(수 ns)과 똑같은 속도로 데이터를 공유할 수 있습니다. Windows에선
CreateFileMapping+MapViewOfFile, POSIX에선shm_open+mmap이 표준입니다. 단점은 데이터 무결성을 OS가 보장하지 않는다는 점입니다 — race condition을 직접 막아야 하므로 세마포어나 뮤텍스 같은 동기화 객체와 거의 항상 같이 씁니다. - 메모리 맵 파일(memory-mapped file) 은 공유 메모리의 일반화로, 파일 시스템의 파일을 메모리에 매핑해 여러 프로세스가 같은 파일을 동시에 메모리로 다루는 방식입니다. Windows에서는
CreateFileMapping에 실제 파일 핸들을 주거나,INVALID_HANDLE_VALUE를 주어 페이징 파일 기반의 익명 공유 메모리로 사용합니다. - 파이프(pipe) 는 단방향 바이트 스트림입니다. 익명 파이프(
CreatePipe)는 부모-자식 프로세스 사이에서만 쓰고, 명명 파이프(CreateNamedPipe,\\.\pipe\name)는 이름으로 찾을 수 있어 무관한 프로세스 간이나 네트워크에서도 사용 가능합니다. 송수신마다 커널 버퍼를 거치므로 시스템 콜 2회 + 데이터 복사 2회(사용자→커널→사용자)가 발생합니다. - 메시지 큐 — POSIX에선
mq_open/mq_send/mq_receive로 우선순위가 있는 고정 크기 메시지 큐를 쓰고, Windows에선 같은 머신·같은 데스크톱 안 스레드 메시지 큐에 보내는PostMessage(GUI 중심), 또는 작은 데이터를 비신뢰 브로드캐스트하는Mailslot(\\.\mailslot\name)이 있습니다. 메시지 경계가 보존된다는 게 파이프와의 핵심 차이입니다. - 소켓(socket) 은 가장 범용적입니다. UNIX domain socket(
AF_UNIX)·Windows의 named pipe over network·TCP/UDP 모두 같은 BSD socket API로 다룹니다. 같은 머신에선 loopback이 빠르고, 네트워크로 확장하면 그대로 분산 통신이 됩니다. 단점은 매 송수신 시스템 콜과 프로토콜 스택 처리 비용입니다. - 세마포어·뮤텍스·Event — 데이터를 옮기는 채널이 아니라 동기화 신호입니다. Windows의 커널 객체 Mutex·Semaphore·Event는 이름을 줘서 프로세스 간 공유 가능합니다. 보통 공유 메모리와 짝지어 “공유 메모리는 데이터, 세마포어/Event는 동기화”로 분업합니다.
- 시그널(signal) — POSIX의 가장 오래된 IPC.
kill(pid, SIGUSR1)처럼 신호 번호를 보내면 대상 프로세스에 등록된 시그널 핸들러가 실행됩니다. 비동기적이고 비트마스크 한 개라 데이터 전달엔 부적합하지만 “중단해라”·”설정을 다시 읽어라” 같은 명령에 적합합니다. Windows에는 직접 대응이 없고, 대신 Event 객체와WaitForSingleObject가 동등한 역할을 합니다. - RPC / COM — 함수 호출처럼 보이지만 그 호출이 다른 프로세스에서 실행되는 고수준 추상화입니다. Windows의 MS-RPC 는 IDL로 인터페이스를 정의하고 marshalling/unmarshalling 코드를 자동 생성하며, 그 위에 얹힌 COM(Component Object Model) 은 객체 단위로 IPC를 추상화합니다. 비용은 가장 비싸지만(수십 μs) 추상화 수준이 가장 높습니다.
선택 기준을 한 줄로 정리하면 “데이터 크기와 빈도, 그리고 격리·보안 요구”입니다. 매 프레임마다 큰 데이터(렌더 명령 버퍼 등)를 옮긴다면 공유 메모리 + 세마포어가 답이고, 가끔 작은 명령을 주고받는다면 명명 파이프나 소켓이 충분합니다. 함수 호출처럼 다뤄야 한다면 RPC/COM이지만 성능을 포기하게 됩니다. Chrome의 멀티프로세스 아키텍처 가 좋은 예시인데, 탭 간 격리(보안)를 위해 프로세스를 분리했지만 그만큼 IPC 비용을 감수하고, 핵심 경로(렌더링 명령)는 공유 메모리 + 세마포어로, 제어 신호는 메시지 패싱으로 분리해 비용을 최적화합니다. 결국 IPC는 컨텍스트 스위칭(21)과 같은 원리입니다 — 메커니즘은 OS가 정해주지만, 빈도와 비용을 조절하는 게 엔지니어링입니다.
핵심 개념
| 분류 | 키워드 | 한 줄 정의 |
|---|---|---|
| 정의 | IPC (Inter-Process Communication, 프로세스 간 통신) | OS의 주소 공간 격리를 깨지 않으면서 프로세스 간 데이터·신호를 주고받는 메커니즘의 총칭 |
| 주소 공간 격리 (Address Space Isolation) | 각 프로세스가 자기 페이지 테이블·가상 주소 공간을 가져, 같은 가상 주소가 서로 다른 물리 메모리를 가리키는 OS 보호 모델 | |
| 커널 중재 (Kernel Mediation) | 격리된 두 프로세스가 안전하게 통신하기 위해 커널이 시스템 콜·커널 객체로 데이터 이동을 매개하는 원칙 | |
| 분류축 | 데이터 vs 신호 | 데이터 전송: pipe·shm·socket·message queue / 신호 전송: signal·event·semaphore |
| 커널 경유 vs 사용자 모드 | 매번 커널 경유: pipe·socket·signal / 초기 1회만: shared memory (이후 user-mode) | |
| 로컬 vs 네트워크 | 로컬 전용: anonymous pipe·shm·signal / 네트워크 가능: named pipe·socket·RPC | |
| 파이프 | 익명 파이프 (Anonymous Pipe) | 부모-자식 사이 단방향 바이트 스트림. Windows CreatePipe, POSIX pipe() |
| 명명 파이프 (Named Pipe) | 이름으로 찾을 수 있는 양방향 파이프. Windows CreateNamedPipe(\\.\pipe\name), POSIX mkfifo() | |
| 파이프 버퍼 | 커널이 관리하는 FIFO 큐. 가득 차면 송신자 블록, 비면 수신자 블록 | |
| 바이트 스트림 (vs 메시지) | 파이프는 메시지 경계 없음 — 4바이트 쓰고 4바이트 쓰면 수신자가 8바이트로 한 번에 읽을 수 있음 | |
| 메시지 큐 | 메시지 큐 (Message Queue) | 메시지 경계가 보존되는 큐. POSIX mq_open/mq_send/mq_receive |
| POSIX mq 우선순위 | 메시지마다 우선순위, 같은 큐 안에서 우선순위 높은 게 먼저 빠짐 | |
PostMessage/SendMessage | Windows GUI 스레드 메시지 큐에 메시지 전송. 같은 데스크톱·같은 머신 한정 | |
Mailslot | Windows의 신뢰성 없는 단방향 브로드캐스트(작은 메시지, 같은 도메인). \\.\mailslot\name | |
| 공유 메모리 | 공유 메모리 (Shared Memory) | 두 프로세스가 같은 물리 페이지를 자기 가상 주소에 매핑 → 일반 메모리 접근 속도 |
CreateFileMapping (Windows) | 페이징 파일 또는 실제 파일을 메모리 매핑 객체로 만듦 | |
MapViewOfFile (Windows) | 매핑 객체를 자기 프로세스 가상 주소에 매핑 | |
shm_open + mmap (POSIX) | POSIX 표준 공유 메모리 — /dev/shm 아래 파일로 표현 | |
| 공유 메모리의 함정 | OS가 동기화 안 함 → race condition·구조체 정렬·포인터 의존 모두 직접 처리 필요 | |
| 메모리 맵 파일 | 메모리 맵 파일 (Memory-Mapped File) | 디스크 파일을 메모리에 매핑해 read/write 대신 메모리 접근으로 다룸. 페이지 캐시 자동 활용 |
| 익명 메모리 맵 | 파일 없이 페이징 파일 기반 — 공유 메모리와 동일 (Windows: INVALID_HANDLE_VALUE) | |
| 동기화 객체 | Mutex (커널 객체, Windows) | 이름 부여 시 프로세스 간 공유 가능. CreateMutex(NULL, FALSE, "Name") |
| Semaphore (커널 객체) | 카운터 기반 락. 공유 메모리에서 produce/consume 동기화에 쓰임 | |
| Event 객체 (Windows) | 시그널 상태를 가지는 객체. POSIX 시그널과 비슷한 알림용 | |
WaitForSingleObject | 모든 커널 객체 대기 진입점 — Mutex·Semaphore·Event 모두 동일 API | |
PulseEvent/SetEvent/ResetEvent | Event 객체 상태 제어 | |
| 시그널 | 시그널 (Signal, POSIX) | 비동기 알림. kill(pid, SIGUSR1) → 대상 프로세스 핸들러 실행 |
| 표준 시그널 | SIGINT(Ctrl+C), SIGTERM(종료 요청), SIGKILL(강제 종료), SIGUSR1/2(사용자 정의), SIGPIPE(파이프 끊김) | |
signal()/sigaction() | 시그널 핸들러 등록. sigaction이 더 안전 | |
| 시그널 핸들러의 제약 | async-signal-safe 함수만 호출 가능 (write·_exit·signal 등 한정) | |
| Windows 대응 | Event 객체 + WaitForSingleObject 또는 SetConsoleCtrlHandler(콘솔 한정) | |
| 소켓 | 소켓 (Socket) | BSD socket API 기반 양방향 통신. 같은 머신·네트워크 모두 |
UNIX domain socket (AF_UNIX) | 같은 머신 전용. 파일 시스템 경로로 식별. TCP보다 빠름 | |
TCP socket (AF_INET/SOCK_STREAM) | 신뢰성 있는 바이트 스트림. 네트워크 가능 | |
UDP socket (AF_INET/SOCK_DGRAM) | 비신뢰 데이터그램. 메시지 경계 보존 | |
| loopback (127.0.0.1) | 같은 머신 내 TCP/UDP. 커널이 NIC 우회 | |
| Winsock | Windows의 BSD socket 구현. WSAStartup/socket/bind/listen | |
| RPC/COM | RPC (Remote Procedure Call) | 함수 호출처럼 다른 프로세스의 함수를 호출하는 추상화 |
| MS-RPC (Microsoft RPC) | Windows의 RPC 구현. IDL로 인터페이스 정의 → marshalling 코드 자동 생성 | |
| marshalling/unmarshalling | 함수 인자를 바이트로 직렬화(marshal) → 전송 → 역직렬화(unmarshal)해 다른 프로세스에서 호출 | |
| COM (Component Object Model) | 객체 단위 IPC + 컴포넌트 모델. CoCreateInstance·IUnknown·인터페이스 | |
| out-of-proc COM | COM 서버가 다른 프로세스(또는 다른 머신)에 있는 경우 — 내부적으로 RPC | |
| 비용 | 공유 메모리 비용 | 초기 세팅 1회 시스템 콜, 이후 메모리 접근 속도(수 ns) |
| 파이프/소켓 비용 | 매 송수신 시스템 콜 + 커널 버퍼 복사 2회 (수 μs~수십 μs) | |
| 시그널 비용 | 시스템 콜 + 인터럽트 처리 (수 μs) | |
| RPC/COM 비용 | marshalling + 프로세스 전환 + unmarshalling (수십~수백 μs) | |
| 보안 | 권한/ACL | 명명된 IPC 객체(Named Pipe·Mutex·Mailslot)는 보안 디스크립터로 접근 제어 |
| integrity level (Windows) | Low/Medium/High — 낮은 IL 프로세스가 높은 IL 객체에 접근 제한 | |
| input validation | 신뢰 경계를 넘는 데이터는 항상 검증 — IPC는 attack surface | |
| 언리얼 | FPlatformProcess | 플랫폼 추상화 — 프로세스 생성·파이프·실행 |
FPlatformProcess::CreatePipe | 익명 파이프 생성 | |
FMessageBus / IMessageBus | 언리얼 자체 메시지 패싱 시스템 (같은 머신·같은 인스턴스 안의 모듈 간) | |
| Live Coding / Hot Reload | 에디터-컴파일러 IPC (named pipe 기반) |
목차
- 핵심 요약 카드
- 한 줄 정의 — IPC란 무엇이고 왜 필요한가
- 분류축 3가지 — 데이터/신호·커널 경유/사용자·로컬/네트워크
- 파이프 — 익명 파이프와 명명 파이프
- 메시지 큐 — POSIX
mq·WindowsPostMessage·Mailslot - 공유 메모리 — 가장 빠른 IPC
- 메모리 맵 파일 —
CreateFileMapping과 페이지 캐시 - 세마포어·뮤텍스·Event — 동기화 객체로서의 IPC
- 시그널 — POSIX와 Windows Event 대응
- 소켓 — UNIX domain·TCP·UDP·Winsock
- RPC와 COM — 함수 호출처럼 보이는 IPC
- 비용 스펙트럼 정리 — 어느 방식이 얼마나 빠른가
- 트레이드오프 — 속도 vs 안전성 vs 동기화 복잡도
- 보안 — 신뢰 경계와 ACL
- 언리얼에서의 IPC —
FPlatformProcess·MessageBus·Hot Reload - 꼬리질문 예상 경로
- 핵심 요약 카드 — 확장판
- 회귀 다리 — 다른 CS 파일 연결
1. 핵심 요약 카드
30초 답변
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
IPC = OS의 주소 공간 격리를 깨지 않으면서 프로세스 간 데이터·신호를 교환하는 메커니즘.
각 프로세스는 자기 페이지 테이블을 가져 가상 주소가 달라 → 반드시 커널이 중재.
분류축 3개:
① 데이터 vs 신호 — pipe·shm·socket / signal·event·semaphore
② 커널 경유 vs 사용자 — 매번 syscall / 초기 1회만 (shared memory)
③ 로컬 vs 네트워크 — anonymous pipe·shm / named pipe·socket·RPC
주요 메커니즘 (빠름 → 느림):
① 공유 메모리 (CreateFileMapping/MapViewOfFile, shm_open+mmap)
- 가장 빠름 (수 ns, 메모리 접근 속도)
- 동기화 직접 처리 필요 → 세마포어/Event와 짝지어 씀
② 메모리 맵 파일 (CreateFileMapping with real file)
- 디스크 파일을 메모리처럼 — 페이지 캐시 자동 활용
③ 파이프 (CreatePipe / CreateNamedPipe / pipe / mkfifo)
- 단방향 바이트 스트림 (메시지 경계 없음)
- 익명: 부모-자식만 / 명명: 무관 프로세스·네트워크 가능
④ 메시지 큐 (POSIX mq_*, Windows PostMessage/Mailslot)
- 메시지 경계 보존, 우선순위 (POSIX)
⑤ 세마포어·뮤텍스·Event (Windows 커널 객체)
- 데이터 X, 동기화 신호 O. 프로세스 간 공유 가능
⑥ 시그널 (POSIX kill/signal/sigaction)
- 비동기 알림. 핸들러는 async-signal-safe 함수만
- Windows에는 Event 객체로 대응
⑦ 소켓 (UNIX domain / TCP / UDP)
- 가장 범용. 네트워크로 확장 가능
⑧ RPC / COM (MS-RPC, CoCreateInstance)
- 함수 호출처럼 — marshalling 비용 큼
비용 스펙트럼 (낮음 → 높음):
공유 메모리 접근 수 ns
Mutex/Event signal+wait 1~3 μs
Anonymous Pipe send/recv 수 μs
Named Pipe send/recv 수 μs (loopback)
UNIX domain socket 수 μs
TCP loopback 수~수십 μs
PostMessage 수 μs
Mailslot 수십 μs
MS-RPC 호출 수십~수백 μs
선택 가이드:
큰 데이터·고빈도 → 공유 메모리 + 세마포어
메시지 경계 필요 → 메시지 큐 / Mailslot / UDP
네트워크 확장 가능성 → 소켓 / 명명 파이프
함수 호출 추상화 → RPC / COM
단순 신호 → Event / 시그널
꼬리질문 연결 맵
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
IPC
├── 왜 필요한가
│ ├── 주소 공간 격리 (페이지 테이블·CR3 — 프로세스 vs 스레드(19)·컨텍스트 스위칭(21) 회귀)
│ ├── 커널 모드 중재 (시스템 콜·모드 스위치 — 컨텍스트 스위칭(21) 회귀)
│ └── 한 프로세스 버그가 다른 프로세스로 전파 안 되게 (보안·안정성)
├── 분류축
│ ├── 데이터 vs 신호
│ ├── 커널 경유 vs 사용자 모드
│ └── 로컬 vs 네트워크
├── 데이터 전송
│ ├── 파이프 (단방향, 바이트 스트림)
│ │ ├── 익명 (CreatePipe / pipe) — 부모-자식만
│ │ └── 명명 (CreateNamedPipe / mkfifo) — 이름·네트워크
│ ├── 메시지 큐 (메시지 경계 보존)
│ │ ├── POSIX mq_open/send/receive — 우선순위
│ │ ├── Windows PostMessage — GUI 한정
│ │ └── Windows Mailslot — 신뢰성 없는 브로드캐스트
│ ├── 공유 메모리 (가장 빠름)
│ │ ├── Windows CreateFileMapping / MapViewOfFile
│ │ ├── POSIX shm_open / mmap
│ │ └── 동기화는 세마포어/Event와 함께
│ ├── 메모리 맵 파일 (디스크 파일을 메모리로)
│ │ └── 페이지 캐시 자동 활용
│ └── 소켓 (가장 범용)
│ ├── UNIX domain (AF_UNIX) — 로컬, 빠름
│ ├── TCP/UDP (AF_INET) — 네트워크
│ └── Winsock — Windows의 BSD 호환
├── 동기화 신호
│ ├── 세마포어 (카운터)
│ ├── 뮤텍스 (배타 락)
│ ├── Event 객체 (Windows, 상태 시그널)
│ └── 시그널 (POSIX, 비동기 핸들러)
├── 고수준 추상화
│ ├── RPC (함수 호출처럼)
│ │ ├── IDL로 인터페이스 정의
│ │ ├── marshalling/unmarshalling
│ │ └── MS-RPC (Windows)
│ └── COM (객체 단위)
│ ├── CoCreateInstance
│ ├── IUnknown / 인터페이스
│ └── out-of-proc COM (내부 RPC)
├── 비용 스펙트럼
│ ├── 공유 메모리 — 수 ns
│ ├── 동기화 객체 — 1~3 μs
│ ├── 파이프/소켓 — 수~수십 μs
│ └── RPC/COM — 수십~수백 μs
├── 트레이드오프
│ ├── 속도: 공유 메모리 ≫ 파이프 ≫ RPC
│ ├── 안전성: 메시지 큐·소켓 (격리 강함) > 공유 메모리 (직접 동기화)
│ └── 추상화: RPC/COM > 메시지 큐 > 파이프 > 공유 메모리
├── 보안
│ ├── ACL (Named 객체)
│ ├── Integrity Level (Windows)
│ └── 입력 검증 (신뢰 경계)
└── 언리얼
├── FPlatformProcess::CreatePipe (익명 파이프)
├── FPlatformProcess::CreateProc (자식 프로세스)
├── IMessageBus / MessageBus 플러그인
├── Live Coding (named pipe)
└── 외부 빌드 도구와의 통신
2. 한 줄 정의 — IPC란 무엇이고 왜 필요한가
핵심 한 문장
IPC(Inter-Process Communication, 프로세스 간 통신)는 OS가 강제하는 주소 공간 격리를 깨지 않으면서 서로 다른 프로세스끼리 데이터를 옮기거나 동기화 신호를 교환하는 메커니즘의 총칭입니다.
왜 그냥 메모리에 쓰면 안 되나 — 주소 공간 격리
프로세스 vs 스레드(19)와 컨텍스트 스위칭(21)에서 정리한 그대로지만, IPC의 출발점이라 다시 짚습니다.
1
2
3
4
프로세스 P1: 가상 주소 0x12345678 → 페이지 테이블 P1 → 물리 주소 0xAAAA
프로세스 P2: 가상 주소 0x12345678 → 페이지 테이블 P2 → 물리 주소 0xBBBB
↑ 같은 가상 주소가
다른 물리 메모리!
P1이 자기 가상 주소 0x12345678에 42를 써도 P2가 같은 주소를 읽으면 전혀 다른 데이터를 봅니다. 이게 주소 공간 격리(address space isolation) 입니다. OS가 강제하고 MMU(Memory Management Unit)가 페이지 테이블로 구현합니다.
격리가 필요한 이유는 세 가지입니다.
- 안정성 — 한 프로세스의 버그(use-after-free·wild pointer·스택 오버플로)가 다른 프로세스의 메모리를 망가뜨리지 않게.
- 보안 — 악의적 프로세스가 다른 프로세스의 비밀 데이터(암호화 키·세션 토큰)를 읽지 못하게.
- 자원 격리 — 한 프로세스가 죽어도 다른 프로세스는 살아 있게 (Chrome의 탭별 프로세스가 대표 예).
격리가 있으니 프로세스끼리 데이터를 옮기려면 그 격리를 한 번 우회하거나, 통신 채널을 따로 만들거나 해야 합니다. 그게 IPC가 풀어야 하는 문제입니다.
어떻게 가능한가 — 커널이 중재
격리는 사용자 모드에서 깨지지 않습니다. 사용자 모드 코드는 자기 페이지 테이블에 매핑되지 않은 주소를 만지면 즉시 page fault → segfault로 죽습니다. 따라서 격리를 합법적으로 우회하려면 커널 모드를 거쳐야 하고, 그게 시스템 콜입니다.
1
2
3
4
5
6
7
8
9
10
11
12
프로세스 P1 (사용자 모드)
↓ write(pipe_fd, data, n) — 시스템 콜
사용자 → 커널 모드 (모드 스위치, 컨텍스트 스위칭(21))
↓
커널: 자기 메모리에 있는 파이프 버퍼에 data 복사 (이건 합법)
↓ 커널 → 사용자 모드
프로세스 P2 (사용자 모드)
↓ read(pipe_fd, buf, n)
사용자 → 커널 모드
↓
커널: 파이프 버퍼에서 buf로 복사
↓ 커널 → 사용자 모드
P1이 P2의 메모리를 직접 만지는 게 아니라, 둘 다 커널 안의 공용 자료구조(파이프 버퍼·메시지 큐·소켓 버퍼) 에 자기 데이터를 복사하거나 거기서 데이터를 가져갑니다. 커널은 양쪽 프로세스의 페이지 테이블을 다 알므로 안전하게 복사할 수 있습니다.
예외가 공유 메모리입니다. 공유 메모리는 처음 한 번 커널이 “이 물리 페이지를 P1·P2 둘 다의 페이지 테이블에 매핑하라”고 세팅해주면, 그 뒤로 P1·P2는 사용자 모드에서 자기 가상 주소로 그냥 메모리 접근만 하면 됩니다. 그래서 가장 빠릅니다. 대신 그 메모리에서 race condition을 막는 건 사용자 코드의 책임입니다 — 커널은 매핑만 도와주고 동기화는 모릅니다.
흐름 한눈에
1
2
3
4
5
6
7
8
9
10
11
12
프로세스 P1 커널 프로세스 P2
───────── ──── ─────────
write/send/post ─────syscall────→ [버퍼/큐/객체]
│
│ (혹은 공유 페이지 매핑)
↓
[페이지 테이블 P2에 매핑]
↓
──────syscall─────→ read/recv/get
│
↓
데이터 수신
왜 IPC를 잘 골라야 하는가
같은 “데이터를 옮긴다”는 작업이라도 메커니즘에 따라 수십 ns(공유 메모리 한 줄 읽기) 에서 수십 μs(RPC 한 번 호출) 까지 1000배 이상 비용 차이가 납니다. 컨텍스트 스위칭(21)에서 본 동기화 객체 선택과 똑같은 의사결정 구조입니다 — 얼마나 빈번한가, 얼마나 큰가, 격리·보안이 얼마나 중요한가.
3. 분류축 3가지 — 데이터/신호·커널 경유/사용자·로컬/네트워크
IPC 메커니즘이 너무 많아 보이지만, 사실 세 개의 축으로 분류하면 차이가 선명해집니다.
분류축 1 — 데이터 전송 vs 신호 전송
| 유형 | 무엇을 옮기나 | 메커니즘 |
|---|---|---|
| 데이터 전송 (data-carrying) | 임의 크기의 바이트 | 파이프·메시지 큐·공유 메모리·소켓·메모리 맵 파일 |
| 신호 전송 (signaling) | 이벤트 발생 사실 | 시그널·Event 객체·세마포어·뮤텍스 |
데이터 전송 채널은 무엇을 보냈는가가 핵심이고, 신호 전송은 언제 무슨 일이 일어났는가가 핵심입니다. 실무에선 둘을 짝지어 씁니다 — 공유 메모리에 데이터를 쓰고 세마포어로 “다 썼다”고 알리거나, 메일슬롯에 작은 명령을 넣고 받는 쪽이 Event 객체로 깨어나는 식입니다.
분류축 2 — 커널 경유 vs 사용자 모드 직접
| 유형 | 매 동작마다 커널 진입? | 메커니즘 |
|---|---|---|
| 매번 커널 경유 | O (시스템 콜 1~2회) | 파이프·메시지 큐·소켓·시그널·RPC |
| 초기 1회만 커널 | X (이후 user-mode 접근) | 공유 메모리·메모리 맵 파일 |
매번 커널 경유는 모드 스위치 비용(컨텍스트 스위칭(21)) 이 매 송수신마다 발생합니다 — 보통 100~500 ns × 2회 + 커널 안에서 버퍼 복사. 그래서 빈도가 높으면 비용이 누적됩니다.
공유 메모리는 처음 CreateFileMapping/MapViewOfFile 호출 시에만 커널을 거치고, 그 뒤로는 일반 메모리 접근입니다 — 매 접근 비용이 수 nanosecond. 단점은 동기화·메시지 경계·생산자/소비자 페이싱을 직접 해결해야 한다는 점입니다.
분류축 3 — 같은 머신 vs 네트워크 가능
| 유형 | 어디서 동작 | 메커니즘 |
|---|---|---|
| 같은 머신 전용 | 한 OS 인스턴스 안 | 익명 파이프·공유 메모리·시그널·Mailslot·UNIX domain socket |
| 네트워크 가능 | 같은 머신 + 다른 머신 | 명명 파이프(Windows SMB 경유)·TCP/UDP 소켓·MS-RPC |
같은 머신 전용은 보통 더 빠르고 단순합니다. 네트워크 가능 메커니즘은 같은 머신에서도 쓸 수 있지만 약간의 추가 오버헤드를 감수합니다(loopback 인터페이스라도 프로토콜 스택을 통과).
Windows의 named pipe는 같은 머신에선 직접 IPC로 동작하지만,
\\server\pipe\name형식으로 다른 머신을 가리키면 SMB 프로토콜 위로 자동 전환되어 네트워크 IPC가 됩니다. 호출 코드는 같습니다.
세 축의 조합 표
| 메커니즘 | 데이터/신호 | 커널 경유 | 네트워크 |
|---|---|---|---|
| 익명 파이프 | 데이터 | 매번 | 같은 머신 |
| 명명 파이프 | 데이터 | 매번 | O (SMB) |
| POSIX mq | 데이터 | 매번 | 같은 머신 |
| PostMessage | 데이터 | 매번 | 같은 머신·같은 데스크톱 |
| Mailslot | 데이터 | 매번 | O (도메인 브로드캐스트) |
| 공유 메모리 | 데이터 | 초기만 | 같은 머신 |
| 메모리 맵 파일 | 데이터 | 초기만 | 같은 머신 |
| Mutex/Semaphore/Event | 신호 | 매번 (Wait) | 같은 머신 |
| 시그널 (POSIX) | 신호 | 매번 | 같은 머신 |
| UNIX domain socket | 데이터 | 매번 | 같은 머신 |
| TCP/UDP 소켓 | 데이터 | 매번 | O |
| MS-RPC / COM | 데이터 (함수 호출) | 매번 | O |
이 표만 외워도 면접에서 IPC 카탈로그를 한 번에 답할 수 있습니다.
4. 파이프 — 익명 파이프와 명명 파이프
4.1 익명 파이프 (Anonymous Pipe)
부모-자식 사이에서만 사용 가능한 단방향 바이트 스트림입니다. 가장 단순하고 가장 오래된 IPC.
Windows — CreatePipe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <windows.h>
HANDLE hRead, hWrite;
SECURITY_ATTRIBUTES sa = { sizeof(sa), NULL, TRUE }; // bInheritHandle = TRUE
CreatePipe(&hRead, &hWrite, &sa, 0); // 마지막 0은 기본 버퍼 크기
// 자식 프로세스 생성 시 핸들 상속
PROCESS_INFORMATION pi;
STARTUPINFO si = { sizeof(si) };
si.dwFlags = STARTF_USESTDHANDLES;
si.hStdInput = hRead;
si.hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CreateProcess(NULL, L"child.exe", NULL, NULL, TRUE, 0, NULL, NULL, &si, &pi);
// 부모는 쓰기만
CloseHandle(hRead);
DWORD written;
WriteFile(hWrite, "hello", 5, &written, NULL);
CloseHandle(hWrite);
POSIX — pipe()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <unistd.h>
int fds[2];
pipe(fds); // fds[0]: 읽기, fds[1]: 쓰기
pid_t pid = fork();
if (pid == 0) { // 자식
close(fds[1]);
char buf[64];
read(fds[0], buf, sizeof(buf));
close(fds[0]);
} else { // 부모
close(fds[0]);
write(fds[1], "hello", 5);
close(fds[1]);
}
동작 원리
1
2
3
4
5
6
7
8
9
10
부모 프로세스 커널 자식 프로세스
───── ── ─────
write(hWrite, data) ──syscall─→ [파이프 버퍼 (FIFO)]
│
│ (페이지 캐시 비슷한 커널 메모리)
↓
←─syscall── read(hRead, buf)
│
↓
buf에 복사
핵심은 커널이 FIFO 버퍼(보통 4KB~64KB)를 가지고, 양쪽이 그 버퍼에 시스템 콜로 접근하는 구조입니다. 버퍼가 가득 차면 WriteFile이 블록(또는 EAGAIN), 비면 ReadFile이 블록.
익명 파이프의 한계
- 부모-자식 한정 — 핸들 상속으로 공유하므로 무관한 프로세스에선 못 씀
- 단방향 — 양방향이 필요하면 파이프 2개 필요
- 메시지 경계 없음 — 4바이트 두 번 써도 8바이트로 한 번에 읽힐 수 있음 (바이트 스트림)
- 로컬 한정 — 네트워크 불가
4.2 명명 파이프 (Named Pipe)
이름으로 찾을 수 있는 양방향 파이프입니다. 부모-자식 관계가 없는 프로세스끼리도 통신 가능하고, Windows에선 네트워크 IPC(다른 머신)도 가능합니다.
Windows — CreateNamedPipe
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 서버 측
HANDLE hPipe = CreateNamedPipe(
L"\\\\.\\pipe\\MyService", // 이름 (네트워크: \\server\pipe\name)
PIPE_ACCESS_DUPLEX, // 양방향
PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT, // 메시지 모드!
PIPE_UNLIMITED_INSTANCES,
4096, 4096, // out/in 버퍼 크기
0, // 기본 타임아웃
NULL // 기본 보안
);
ConnectNamedPipe(hPipe, NULL); // 클라이언트 연결 대기 → 컨텍스트 스위치
DWORD bytesRead;
char buf[4096];
ReadFile(hPipe, buf, sizeof(buf), &bytesRead, NULL);
WriteFile(hPipe, "response", 8, &bytesWritten, NULL);
DisconnectNamedPipe(hPipe);
CloseHandle(hPipe);
1
2
3
4
5
6
7
8
9
10
// 클라이언트 측
HANDLE hPipe = CreateFile(
L"\\\\.\\pipe\\MyService",
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, 0, NULL
);
WriteFile(hPipe, "request", 7, &bytesWritten, NULL);
ReadFile(hPipe, buf, sizeof(buf), &bytesRead, NULL);
CloseHandle(hPipe);
POSIX — mkfifo()
1
2
3
4
5
6
7
8
9
10
11
#include <sys/stat.h>
mkfifo("/tmp/myfifo", 0666); // 파일 시스템에 FIFO 노드 생성
// 프로세스 A
int fd = open("/tmp/myfifo", O_WRONLY);
write(fd, "hello", 5);
// 프로세스 B (별도 실행)
int fd = open("/tmp/myfifo", O_RDONLY);
read(fd, buf, sizeof(buf));
POSIX의 FIFO는 파일 시스템에 노드를 만들지만 데이터는 디스크에 쓰지 않습니다 — 커널 메모리의 버퍼만 씁니다.
Windows 명명 파이프의 핵심 기능
- 양방향(
PIPE_ACCESS_DUPLEX) 또는 단방향(PIPE_ACCESS_INBOUND/OUTBOUND) - 메시지 모드(
PIPE_TYPE_MESSAGE) — 메시지 경계 보존! 한 번WriteFile한 만큼 한 번에ReadFile로 받음 - 바이트 모드(
PIPE_TYPE_BYTE) — 익명 파이프와 같은 바이트 스트림 - 네트워크 가능 —
\\server\pipe\name으로 다른 머신 접근 (SMB 경유) - 인스턴스 제한 —
PIPE_UNLIMITED_INSTANCES로 다중 클라이언트 - ACL 적용 — 보안 디스크립터로 접근 제어
익명 vs 명명 파이프 비교
| 항목 | 익명 파이프 | 명명 파이프 |
|---|---|---|
| 사용 범위 | 부모-자식 한정 | 무관 프로세스·네트워크 |
| 방향 | 단방향 | 양방향 가능 |
| 메시지 경계 | 없음 (바이트 스트림) | 모드 선택 (메시지/바이트) |
| 이름 | 없음 (핸들 상속) | \\.\pipe\name |
| 보안 | 핸들 상속 가시성 | ACL 적용 |
| 네트워크 | X | O (SMB) |
| 사용처 | 표준 입출력 리다이렉트, 자식 프로세스 통신 | 서비스-클라이언트, 디버거-프로세스, 빌드 도구 |
사용 사례
1
2
3
4
5
6
7
8
9
10
Windows 익명 파이프:
- cmd 리다이렉트 (cmd | grep)
- Visual Studio가 자식 컴파일러와 통신 (출력 캡처)
- 자식 프로세스의 stdout/stderr 가로채기
Windows 명명 파이프:
- SQL Server 클라이언트-서버 (TCP 외 옵션)
- Docker Desktop의 docker.exe ↔ engine
- .NET의 NamedPipeServerStream / NamedPipeClientStream
- 언리얼 Live Coding의 에디터 ↔ 컴파일러 통신
5. 메시지 큐 — POSIX mq·Windows PostMessage·Mailslot
파이프와 가장 큰 차이는 메시지 경계가 보존된다는 점입니다. 4바이트 메시지 + 8바이트 메시지를 보내면 받는 쪽도 4바이트 + 8바이트로 받습니다(파이프는 12바이트 한 번에 받을 수 있음).
5.1 POSIX 메시지 큐 — mq_*
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <mqueue.h>
struct mq_attr attr = {
.mq_flags = 0,
.mq_maxmsg = 10, // 최대 10개 메시지
.mq_msgsize = 256, // 메시지당 최대 256바이트
.mq_curmsgs = 0
};
// 송신 측
mqd_t mq = mq_open("/myqueue", O_CREAT | O_WRONLY, 0644, &attr);
mq_send(mq, "hello", 5, 1); // 마지막 1은 priority
mq_close(mq);
// 수신 측
mqd_t mq = mq_open("/myqueue", O_RDONLY);
char buf[256];
unsigned prio;
mq_receive(mq, buf, sizeof(buf), &prio);
mq_close(mq);
mq_unlink("/myqueue"); // 큐 자체 제거
특징:
- 우선순위 큐 — 우선순위가 높은 메시지가 먼저 빠짐
- 고정 메시지 크기 제한 —
mq_msgsize초과 시 송신 실패 - 알림(
mq_notify) — 큐에 메시지 도착 시 시그널 또는 스레드 시작 - 같은 머신 전용
System V 계열의 msgsnd/msgrcv도 비슷한 역할이지만 POSIX 방식이 더 모던하고 권장됩니다.
5.2 Windows PostMessage/SendMessage — GUI 스레드 메시지 큐
Windows는 모든 GUI 스레드가 자기 메시지 큐를 가지고 있습니다. 다른 스레드/프로세스가 이 큐에 메시지를 넣을 수 있습니다.
1
2
3
4
5
6
// 다른 프로세스의 윈도우에 메시지 보내기
HWND hwnd = FindWindow(L"Notepad", NULL); // 메모장 찾기
PostMessage(hwnd, WM_USER + 100, wParam, lParam); // 비동기 (즉시 리턴)
// 또는 동기로 (대상이 처리 끝날 때까지 대기)
LRESULT result = SendMessage(hwnd, WM_USER + 100, wParam, lParam);
| 함수 | 의미 |
|---|---|
PostMessage | 큐에 넣고 즉시 리턴 (비동기) |
SendMessage | 대상이 처리 끝날 때까지 대기 (동기) — 대상 프로세스가 응답 안 하면 무한 대기 가능 |
SendMessageTimeout | 타임아웃 있는 동기 호출 |
특징:
- 같은 데스크톱 안의 GUI 스레드만 — 서비스 프로세스나 다른 세션에는 안 됨
WPARAM/LPARAM은 32/64비트 정수로 작은 데이터만 직접 전달 — 큰 데이터는WM_COPYDATA로 구조체 포인터 전달WM_COPYDATA사용 시 OS가 자동으로 데이터를 대상 프로세스 주소 공간으로 복사- GUI 앱끼리의 간단한 명령 전달에 흔히 사용 (예: 두 번째 인스턴스가 첫 번째 인스턴스에 인자 전달)
1
2
3
4
5
COPYDATASTRUCT cds;
cds.dwData = 0xCAFEBABE;
cds.cbData = strlen(msg) + 1;
cds.lpData = (PVOID)msg;
SendMessage(hwnd, WM_COPYDATA, (WPARAM)hThisWnd, (LPARAM)&cds);
5.3 Windows Mailslot — 신뢰성 없는 브로드캐스트
Mailslot은 작은 메시지의 단방향 브로드캐스트를 위한 IPC입니다. 도메인 안의 여러 머신에 같은 메시지를 한 번에 뿌릴 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
// 수신 측 (서버)
HANDLE hSlot = CreateMailslot(
L"\\\\.\\mailslot\\MyMailslot",
0, // 최대 메시지 크기 (0 = 무제한)
MAILSLOT_WAIT_FOREVER,
NULL
);
DWORD bytesRead;
char buf[512];
ReadFile(hSlot, buf, sizeof(buf), &bytesRead, NULL);
1
2
3
4
5
6
7
8
9
10
11
// 송신 측 (클라이언트) — 단방향
HANDLE hMail = CreateFile(
L"\\\\.\\mailslot\\MyMailslot",
GENERIC_WRITE,
FILE_SHARE_READ,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
);
WriteFile(hMail, "broadcast", 9, &written, NULL);
// 도메인 브로드캐스트
HANDLE hMail = CreateFile(L"\\\\*\\mailslot\\MyMailslot", ...);
특징:
- 단방향 (서버 ← 클라이언트만)
- 신뢰성 없음 — 큰 메시지(>424B)는 도메인 브로드캐스트 시 손실 가능
- 메시지 경계 보존
- 도메인 와일드카드 —
\\*\mailslot\name으로 도메인 내 모든 머신 - 보안 발견(discovery), 네트워크 게임의 LAN 광고 같은 용도에 사용
Mailslot은 모던 Windows에선 거의 deprecated 분위기이고, 새 코드에선 잘 안 씁니다. 알아두는 정도로 충분합니다.
5.4 메시지 큐 비교 표
| 항목 | POSIX mq | Windows PostMessage | Windows Mailslot |
|---|---|---|---|
| 방향 | 양방향(별 큐) | 양방향 | 단방향 |
| 메시지 경계 | O | O | O |
| 우선순위 | O | X (FIFO) | X |
| 크기 제한 | mq_msgsize | WPARAM/LPARAM (8B+8B) 또는 WM_COPYDATA | 무제한 (단, 브로드캐스트 시 424B) |
| 네트워크 | X | X | O (도메인 브로드캐스트) |
| 사용처 | UNIX 서비스 큐 | GUI 앱 간 명령 | 네트워크 발견·LAN 광고 |
6. 공유 메모리 — 가장 빠른 IPC
6.1 왜 가장 빠른가
이미 봤듯이 다른 IPC는 매 송수신마다 시스템 콜 + 모드 스위치 + 커널 안 버퍼 복사가 발생합니다. 1KB를 보내면 사용자→커널 복사 + 커널→사용자 복사 = 총 2KB 메모리 복사 + 시스템 콜 2회.
공유 메모리는 초기 세팅 한 번만 시스템 콜이고, 그 뒤로는 두 프로세스가 같은 물리 페이지를 자기 가상 주소에 매핑해두므로 일반 메모리 접근(수 nanosecond)과 같은 속도입니다. 1KB를 “보내는” 게 사실은 그 메모리에 그냥 쓰는 것이라 복사도 없습니다.
1
2
3
4
5
6
7
8
9
10
초기 세팅 (한 번):
P1: CreateFileMapping → 커널이 페이지 객체 생성
P1: MapViewOfFile → P1 페이지 테이블에 매핑 (가상 주소 V1)
P2: OpenFileMapping → 같은 객체 찾기 (이름 기반)
P2: MapViewOfFile → P2 페이지 테이블에 매핑 (가상 주소 V2)
(V1과 V2는 가상 주소만 다를 뿐 가리키는 물리 페이지는 같음)
이후 통신:
P1: *V1 = 42; ← 그냥 메모리 쓰기 (수 ns)
P2: int x = *V2; ← 그냥 메모리 읽기 (수 ns)
6.2 Windows — CreateFileMapping + MapViewOfFile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 프로세스 P1 (생성자)
HANDLE hMap = CreateFileMapping(
INVALID_HANDLE_VALUE, // 실제 파일 없음 → 페이징 파일 기반 익명
NULL, // 기본 보안
PAGE_READWRITE,
0, 4096, // 크기 (4KB)
L"Global\\MySharedMem" // 이름 (다른 프로세스가 찾을 이름)
);
void* pView = MapViewOfFile(
hMap,
FILE_MAP_ALL_ACCESS,
0, 0, // offset
4096 // 매핑 크기
);
strcpy((char*)pView, "Hello from P1");
// ... P2가 읽을 동안 매핑 유지
UnmapViewOfFile(pView);
CloseHandle(hMap);
1
2
3
4
5
6
7
8
9
10
11
12
13
// 프로세스 P2 (소비자)
HANDLE hMap = OpenFileMapping(
FILE_MAP_ALL_ACCESS,
FALSE,
L"Global\\MySharedMem"
);
void* pView = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 4096);
printf("%s\n", (char*)pView); // "Hello from P1"
UnmapViewOfFile(pView);
CloseHandle(hMap);
Global\\ 네임스페이스는 모든 세션에서 보이고, Local\\은 같은 세션 안에서만 보입니다. 서비스 ↔ 데스크톱 앱 통신은 Global\\이 필요.
6.3 POSIX — shm_open + mmap
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <sys/mman.h>
#include <fcntl.h>
// 프로세스 P1
int fd = shm_open("/myshm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);
void* pView = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
strcpy((char*)pView, "Hello from P1");
munmap(pView, 4096);
close(fd);
// shm_unlink("/myshm")로 객체 제거
// 프로세스 P2
int fd = shm_open("/myshm", O_RDWR, 0);
void* pView = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
printf("%s\n", (char*)pView);
munmap(pView, 4096);
close(fd);
POSIX 공유 메모리 객체는 /dev/shm/myshm 같은 파일로 노출되어 디버깅 시 ls /dev/shm으로 확인 가능합니다.
6.4 공유 메모리의 함정 — 동기화는 직접
빠르지만 OS는 데이터 무결성을 보장하지 않습니다. 두 프로세스가 동시에 같은 영역을 쓰면 그냥 race condition.
1
2
3
4
5
6
7
// 잘못된 예 — race condition
*((int*)pView) += 1; // P1·P2가 동시에 하면 결과 불확정
// 올바른 예 — 세마포어/뮤텍스로 동기화
WaitForSingleObject(hMutex, INFINITE);
*((int*)pView) += 1;
ReleaseMutex(hMutex);
그래서 공유 메모리는 거의 항상 이름 있는 Mutex·Semaphore·Event와 짝지어 씁니다. 보통 패턴이 “공유 메모리는 데이터, 세마포어는 슬롯 카운트, Event는 데이터 도착 알림”.
6.5 공유 메모리의 다른 함정 — 포인터 사용 불가
공유 메모리 안에 포인터를 넣으면 안 됩니다. P1의 매핑 주소와 P2의 매핑 주소가 다르므로 P1에서 적은 포인터가 P2에선 잘못된 주소를 가리킵니다.
1
2
3
4
5
6
// 잘못된 예 — P1·P2의 매핑 주소가 다르면 포인터 깨짐
struct Bad { int* p; };
((Bad*)pView)->p = &localVar; // P1의 지역 변수 주소 → P2에선 의미 없음
// 올바른 예 — 오프셋(offset)으로 표현
struct Good { ptrdiff_t offset_to_data; };
공유 메모리 안에서 자료구조를 만들려면 포인터 대신 오프셋, 또는 매핑을 양쪽이 같은 주소에 하도록 강제(MapViewOfFileEx)해야 합니다. 일반적으로 어렵고 오류가 잦아 권장하지 않습니다.
6.6 사용 사례
1
2
3
4
5
- 게임 엔진의 렌더 명령 버퍼 (Game ↔ Render 프로세스 분리 시)
- Chrome의 GPU 프로세스와 렌더러 프로세스 간 텍스처 공유
- 대용량 데이터 분석 도구 간 측정 데이터 공유
- 데이터베이스의 shared buffer pool (워커 프로세스 풀)
- 윈도우 메시지 보드(공유 비디오 메모리)
7. 메모리 맵 파일 — CreateFileMapping과 페이지 캐시
메모리 맵 파일(memory-mapped file)은 공유 메모리의 일반화입니다. 실제 파일을 메모리에 매핑해서 read/write 시스템 콜 없이 메모리 접근으로 파일을 다룹니다.
7.1 Windows — 같은 API, 다른 인자
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
HANDLE hFile = CreateFile(
L"data.bin",
GENERIC_READ | GENERIC_WRITE,
0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL
);
HANDLE hMap = CreateFileMapping(
hFile, // 실제 파일 핸들 (vs INVALID_HANDLE_VALUE)
NULL,
PAGE_READWRITE,
0, 0, // 0,0 = 파일 전체
NULL // 이름 없어도 됨
);
void* pView = MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0);
// 파일이 메모리로 보임 — 일반 메모리 접근
((char*)pView)[0] = 'A'; // 파일 첫 바이트가 'A'로
hFile을 INVALID_HANDLE_VALUE로 주면 페이징 파일 기반 익명 공유 메모리(6장), 실제 파일을 주면 그 파일과 동기화되는 메모리 맵 파일이 됩니다 — API가 같습니다.
7.2 POSIX — mmap
1
2
3
4
5
int fd = open("data.bin", O_RDWR);
void* pView = mmap(NULL, file_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
// pView로 파일 접근
munmap(pView, file_size);
close(fd);
7.3 IPC 측면에서의 가치
여러 프로세스가 같은 파일을 메모리 맵하면 자동으로 공유 메모리가 됩니다. OS의 페이지 캐시(page cache)가 같은 파일의 같은 페이지를 한 번만 RAM에 올리고 여러 프로세스 페이지 테이블에 매핑하기 때문입니다.
1
2
3
4
5
프로세스 P1 ──mmap("data.bin")──→ ┐
│
프로세스 P2 ──mmap("data.bin")──→ ├─ 페이지 캐시 (RAM) ←─ data.bin (디스크)
│
프로세스 P3 ──mmap("data.bin")──→ ┘
읽기·쓰기가 자동으로 디스크와 동기화되어 영속성도 얻습니다.
7.4 페이지 폴트 기반 lazy 로딩
매핑할 때 데이터가 실제로 RAM에 올라오는 게 아닙니다. 처음 그 메모리 페이지를 만질 때 page fault가 발생하고, 그때 OS가 디스크에서 페이지를 읽어옵니다 — 일종의 lazy loading. 그래서 100GB 파일도 메모리 맵으로 다룰 수 있습니다(주소 공간만 잡고 실제 데이터는 접근하는 페이지만).
7.5 사용 사례
1
2
3
4
- DBMS의 데이터 파일 (SQLite mmap mode, MongoDB의 일부 엔진)
- 대용량 로그 파일 분석 (수십 GB)
- 게임의 애셋 패키지 — 프로세스끼리 공유하면서 page cache 활용
- 외부 도구가 만든 결과 파일을 메인 앱이 즉시 메모리로 사용
8. 세마포어·뮤텍스·Event — 동기화 객체로서의 IPC
이 셋은 데이터를 옮기는 채널이 아니라 동기화 신호입니다. 컨텍스트 스위칭(21)에서 같은 프로세스 안의 스레드 동기화로 다뤘는데, IPC 관점에서는 이름을 부여해 프로세스 간 공유할 수 있습니다.
8.1 Named Mutex (Windows)
1
2
3
4
5
6
7
8
9
10
11
12
13
// P1 (먼저 실행)
HANDLE hMutex = CreateMutex(NULL, FALSE, L"Global\\MyMutex");
WaitForSingleObject(hMutex, INFINITE);
// 임계 구역
ReleaseMutex(hMutex);
CloseHandle(hMutex);
// P2 (나중에 실행)
HANDLE hMutex = OpenMutex(SYNCHRONIZE, FALSE, L"Global\\MyMutex");
WaitForSingleObject(hMutex, INFINITE);
// 같은 임계 구역 (P1과 직렬화)
ReleaseMutex(hMutex);
CloseHandle(hMutex);
CreateMutex가 기존에 같은 이름이 있으면 그 핸들을 반환합니다(GetLastError() == ERROR_ALREADY_EXISTS로 구분 가능). 그래서 “먼저 만들기” 패턴이 가능 — 한 인스턴스만 띄우는 single-instance 앱이 흔히 씁니다.
8.2 Named Semaphore (Windows)
1
2
3
4
5
6
7
HANDLE hSem = CreateSemaphore(NULL, 0, 10, L"Global\\MySem"); // 초기 0, 최대 10
// 생산자
ReleaseSemaphore(hSem, 1, NULL); // 카운터 +1 → 대기 중인 소비자 1명 깨움
// 소비자
WaitForSingleObject(hSem, INFINITE); // 카운터 -1, 0이면 대기
전형적인 공유 메모리 + 세마포어 패턴:
1
2
3
4
5
6
7
공유 메모리: 데이터 슬롯 N개
세마포어 'empty' (초기 N): 빈 슬롯 수
세마포어 'full' (초기 0): 채워진 슬롯 수
Mutex 'lock': 슬롯 인덱스 보호
생산자: Wait(empty) → Wait(lock) → 슬롯 채움 → Release(lock) → Release(full)
소비자: Wait(full) → Wait(lock) → 슬롯 비움 → Release(lock) → Release(empty)
8.3 Named Event (Windows)
Event는 시그널 상태를 가지는 객체입니다. 시그널 상태에서 WaitForSingleObject 호출은 즉시 통과, 비시그널 상태에선 대기.
1
2
3
4
5
6
// 종류 두 가지
HANDLE hAuto = CreateEvent(NULL, FALSE, FALSE, L"Global\\AutoEvt"); // auto-reset
HANDLE hManual = CreateEvent(NULL, TRUE, FALSE, L"Global\\ManualEvt"); // manual-reset
// auto-reset: SetEvent로 시그널 → 한 스레드 깨우면 자동으로 reset
// manual-reset: SetEvent로 시그널 → 모든 대기 스레드 깨움, ResetEvent로 직접 reset
| 함수 | 효과 |
|---|---|
SetEvent | 시그널 상태로 변경 → 대기 스레드 깨움 |
ResetEvent | 비시그널 상태로 변경 (manual-reset만 의미) |
PulseEvent | Set 후 즉시 Reset (단발 신호, 잘 안 씀 — 경합 시 누락 가능) |
8.4 동기화 객체의 IPC 분류
| 객체 | 데이터 | 신호 | 프로세스 간 공유 | 사용 패턴 |
|---|---|---|---|---|
| Named Mutex | X | 배타 락 | O (이름) | 자원 보호, single-instance |
| Named Semaphore | X | 카운터 | O (이름) | 생산자-소비자 슬롯 카운팅 |
| Named Event | X | 상태 시그널 | O (이름) | “데이터 도착”, “준비 완료” 알림 |
WaitForMultipleObjects | - | 다중 대기 | O | 여러 이벤트 중 하나라도 시그널이면 깨어남 |
8.5 WaitForMultipleObjects — 다중 대기
1
2
3
4
5
6
7
8
9
10
HANDLE handles[] = { hEvent1, hMutex, hSem };
DWORD ret = WaitForMultipleObjects(3, handles, FALSE, INFINITE);
// FALSE: 하나라도 시그널이면 리턴
// TRUE: 모두 시그널이면 리턴
switch (ret) {
case WAIT_OBJECT_0 + 0: /* hEvent1 시그널 */ break;
case WAIT_OBJECT_0 + 1: /* hMutex 획득 */ break;
case WAIT_OBJECT_0 + 2: /* hSem 카운트 감소 */ break;
}
여러 IPC 객체를 한 스레드에서 통합 대기. 게임 서버나 UI 디스패처가 “메시지 큐 + 종료 신호 + 타이머”를 동시에 기다릴 때 흔히 씁니다.
8.6 POSIX 동등물
1
2
3
4
5
6
7
8
9
10
11
12
// Named Semaphore — POSIX
sem_t* sem = sem_open("/mysem", O_CREAT, 0666, 0);
sem_post(sem); // +1
sem_wait(sem); // -1
sem_close(sem);
sem_unlink("/mysem");
// Mutex/Condition Variable in shared memory — POSIX
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(mutex_in_shm, &attr);
POSIX는 Mutex/Condition Variable를 공유 메모리에 놓고 PTHREAD_PROCESS_SHARED 속성을 줘서 프로세스 간 공유합니다 — Windows의 Named Mutex와 같은 효과지만 더 번거롭습니다.
9. 시그널 — POSIX와 Windows Event 대응
9.1 POSIX 시그널 — 가장 오래된 IPC
시그널은 비동기 알림입니다. 한 프로세스가 다른 프로세스에 “이런 일이 일어났다”는 번호 하나만 보내고, 받는 쪽은 그 번호에 등록해둔 핸들러가 실행됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <signal.h>
void handler(int signo) {
write(1, "received\n", 9); // async-signal-safe 함수만
}
int main() {
signal(SIGUSR1, handler); // 시그널 핸들러 등록
// 또는 더 안전한 sigaction()
struct sigaction sa = { .sa_handler = handler };
sigaction(SIGUSR1, &sa, NULL);
while (1) pause(); // 시그널 대기
}
// 다른 프로세스에서
kill(target_pid, SIGUSR1);
9.2 표준 시그널
| 시그널 | 기본 동작 | 의미 |
|---|---|---|
SIGINT (2) | 종료 | Ctrl+C |
SIGTERM (15) | 종료 | 정상 종료 요청 (catch 가능) |
SIGKILL (9) | 강제 종료 | catch 불가, 무조건 죽음 |
SIGSTOP (19) | 정지 | catch 불가 |
SIGSEGV (11) | core dump | Segmentation Fault |
SIGCHLD (17) | 무시 | 자식 프로세스 종료 |
SIGPIPE (13) | 종료 | 끊어진 파이프에 쓰기 시도 |
SIGUSR1 / SIGUSR2 | 종료 | 사용자 정의 |
SIGHUP (1) | 종료 | 터미널 닫힘 (서비스 재설정 신호로 자주 재정의) |
9.3 시그널 핸들러의 제약 — async-signal-safe
시그널은 언제든 인터럽트로 끼어들어 핸들러가 실행됩니다 — 일반 함수가 락을 잡고 있는 상태에서도. 그래서 핸들러 안에서는 매우 적은 함수만 호출 가능합니다(POSIX async-signal-safe 함수 목록).
1
2
3
4
5
6
7
8
9
10
// 안전
write(1, "msg\n", 4);
_exit(1);
signal(SIGINT, handler);
sigaction(...);
// 위험 (다른 코드가 같은 자원에서 락을 잡고 있으면 데드락)
printf("msg\n"); // stdio 락
malloc(100); // 힙 락
pthread_mutex_lock(&mtx); // 일반 mutex
실무 패턴은 시그널 핸들러에서 플래그만 세팅하고, 메인 루프에서 그 플래그를 검사하는 것입니다:
1
2
3
4
5
6
7
volatile sig_atomic_t g_should_quit = 0;
void handler(int) { g_should_quit = 1; }
while (!g_should_quit) {
// 메인 작업
}
cleanup();
sig_atomic_t는 시그널 안전한 정수 타입입니다.
9.4 시그널의 한계
- 데이터 전달 불가 — 시그널 번호 하나뿐 (다만 POSIX real-time signals —
SIGRTMIN~SIGRTMAX는 작은 정수 데이터 전달 가능,sigqueue/siginfo_t) - 비동기 — 핸들러가 언제 실행될지 모름
- 누락 가능 — 같은 시그널이 핸들러 실행 전에 또 오면 합쳐짐(기본 시그널). real-time signals는 큐잉됨
- async-signal-safe 함수만 사용 가능
- 로컬 한정 — 네트워크 불가
그래서 시그널은 보통 간단한 명령 (“종료해라”, “설정 다시 읽어라”, “통계 출력해라”) 에 적합합니다. 데이터를 옮기려면 다른 IPC를 써야 합니다.
9.5 Windows에는 시그널이 없다 — Event로 대응
Windows는 POSIX 시그널을 직접 지원하지 않습니다. 일부 표준 C 시그널(SIGINT, SIGTERM)은 흉내내지만 동작이 제한적입니다.
Windows에서 같은 효과를 내는 방법:
| POSIX 패턴 | Windows 대응 |
|---|---|
kill(pid, SIGUSR1) → 핸들러 실행 | Named Event + WaitForSingleObject (메인 스레드가 대기, 시그널러가 SetEvent) |
kill(pid, SIGTERM) → 정상 종료 | SetConsoleCtrlHandler (콘솔 앱), 또는 IPC 채널로 종료 메시지 |
| Ctrl+C | SetConsoleCtrlHandler(CTRL_C_EVENT) |
SIGSEGV (access violation) | SEH (__try/__except) 또는 Vectored Exception Handler |
SIGCHLD (자식 종료) | WaitForSingleObject(hProcess) |
1
2
3
4
5
6
7
8
9
// Windows의 Ctrl+C 처리
BOOL WINAPI CtrlHandler(DWORD ctrlType) {
if (ctrlType == CTRL_C_EVENT) {
// 정리
return TRUE;
}
return FALSE;
}
SetConsoleCtrlHandler(CtrlHandler, TRUE);
1
2
3
4
5
6
7
8
9
10
// "다른 프로세스에 알림" 패턴 — Named Event
// 시그널러 측
HANDLE h = OpenEvent(EVENT_MODIFY_STATE, FALSE, L"Global\\QuitEvent");
SetEvent(h);
CloseHandle(h);
// 대상 측 (메인 스레드 또는 워커)
HANDLE h = CreateEvent(NULL, TRUE, FALSE, L"Global\\QuitEvent");
WaitForSingleObject(h, INFINITE);
// 깨어나면 정리
이게 Windows에선 사실상 IPC + 비동기 알림의 표준 패턴입니다.
10. 소켓 — UNIX domain·TCP·UDP·Winsock
소켓은 가장 범용적인 IPC입니다. 같은 BSD socket API로 같은 머신과 네트워크를 모두 다룹니다.
10.1 주요 도메인
| 도메인 | 주소 패밀리 | 범위 | 특성 |
|---|---|---|---|
| UNIX domain | AF_UNIX (POSIX) | 같은 머신 | 파일 시스템 경로로 식별, TCP보다 빠름 |
| TCP | AF_INET + SOCK_STREAM | 네트워크 | 신뢰성 있는 바이트 스트림 |
| UDP | AF_INET + SOCK_DGRAM | 네트워크 | 비신뢰 데이터그램, 메시지 경계 보존 |
| IPv6 TCP/UDP | AF_INET6 | 네트워크 | IPv6 |
Windows에는 직접적인 AF_UNIX 대응이 명명 파이프지만, Windows 10 1803+부터 AF_UNIX 자체도 지원합니다.
10.2 UNIX domain socket — AF_UNIX
같은 머신 안에서만 동작하는 소켓으로, TCP loopback보다 빠릅니다(프로토콜 스택 우회).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <sys/socket.h>
#include <sys/un.h>
// 서버
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr = { .sun_family = AF_UNIX };
strcpy(addr.sun_path, "/tmp/mysock");
bind(sfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sfd, 5);
int cfd = accept(sfd, NULL, NULL);
char buf[1024];
read(cfd, buf, sizeof(buf));
close(cfd);
// 클라이언트
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr = { .sun_family = AF_UNIX };
strcpy(addr.sun_path, "/tmp/mysock");
connect(sfd, (struct sockaddr*)&addr, sizeof(addr));
write(sfd, "hello", 5);
close(sfd);
특징:
- 파일 시스템 경로로 식별 —
/tmp/mysock - 권한 관리 — 파일 시스템 권한 그대로 (
chmod) - stream(
SOCK_STREAM)·datagram(SOCK_DGRAM)·seqpacket(SOCK_SEQPACKET) - fd 전송 가능 —
SCM_RIGHTS로 파일 디스크립터 자체를 다른 프로세스로 넘김 (고급) - TCP loopback보다 30~50% 빠름
Docker socket(/var/run/docker.sock), X11 디스플레이 서버, systemd activation 등이 대표 사용처입니다.
10.3 TCP/UDP — 네트워크 가능
1
2
3
4
5
6
7
8
9
10
// TCP 서버
int sfd = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {
.sin_family = AF_INET,
.sin_port = htons(8080),
.sin_addr.s_addr = INADDR_ANY
};
bind(sfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sfd, 5);
int cfd = accept(sfd, NULL, NULL);
같은 머신 안에서 쓸 때는 127.0.0.1 (loopback)을 씁니다. loopback은 NIC를 거치지 않고 커널이 직접 처리하므로 네트워크 가지 않습니다. 그래도 프로토콜 스택(TCP 헤더·시퀀스 번호·체크섬)은 통과합니다.
| 옵션 | TCP | UDP |
|---|---|---|
| 신뢰성 | O (재전송) | X |
| 순서 보장 | O | X |
| 메시지 경계 | X (바이트 스트림) | O (데이터그램) |
| 연결 지향 | O | X |
| 비용 | 핸드셰이크 + ACK | 단순 송수신 |
10.4 Windows Winsock
Windows는 BSD socket API의 변형인 Winsock을 제공합니다. 거의 같지만 WSAStartup/WSACleanup이 필요하고 closesocket을 씁니다.
1
2
3
4
5
6
7
8
9
10
11
12
#include <winsock2.h>
#include <ws2tcpip.h>
#pragma comment(lib, "ws2_32.lib")
WSADATA wsa;
WSAStartup(MAKEWORD(2, 2), &wsa);
SOCKET s = socket(AF_INET, SOCK_STREAM, 0);
// ... bind/connect/send/recv
closesocket(s);
WSACleanup();
Winsock은 또 확장 함수(WSAAccept, WSAConnect, WSARecv 등)와 IOCP(I/O Completion Port) 같은 비동기 모델을 제공해 고성능 서버를 만들 수 있습니다.
10.5 소켓의 사용 사례 (IPC 관점)
1
2
3
4
5
6
- 마이크로서비스: TCP/UDP로 프로세스 간 통신 (HTTP·gRPC)
- Docker daemon: UNIX domain socket
- X11/Wayland: UNIX domain socket
- 데이터베이스 클라이언트-서버 (Postgres, MySQL — TCP 또는 UNIX socket)
- 게임 클라이언트-서버 (UDP 위주, TCP는 채팅·로비)
- 언리얼의 NetDriver는 UDP socket 기반
11. RPC와 COM — 함수 호출처럼 보이는 IPC
11.1 RPC — Remote Procedure Call
RPC는 다른 프로세스(또는 머신)의 함수를 자기 함수처럼 호출하는 추상화입니다. 클라이언트 코드가 Add(2, 3) 호출하면 그 호출이 다른 프로세스에서 실행되고 결과가 돌아옵니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
클라이언트 (P1) RPC 런타임 서버 (P2)
───── ───── ─────
int r = Add(2, 3);
↓
client stub (자동 생성)
↓ marshalling
[바이트 스트림] ──IPC 채널──→ [바이트 스트림]
(pipe/socket) ↓
server stub
↓ unmarshalling
Add(2, 3) 실제 함수
↓
return 5
↓ marshalling
←──IPC 채널── [바이트 스트림]
↓ unmarshalling
return 5
핵심 기술이 marshalling(인자를 바이트로 직렬화)과 stub 코드 자동 생성입니다.
11.2 MS-RPC — Windows의 RPC
Windows는 OS 수준에서 RPC를 제공합니다. IDL(Interface Definition Language) 로 인터페이스를 정의하고, midl.exe 컴파일러가 stub 코드를 자동 생성합니다.
// math.idl
[uuid(00000000-0000-0000-0000-000000000001), version(1.0)]
interface MathInterface {
int Add([in] int a, [in] int b);
int Subtract([in] int a, [in] int b);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// 서버 측 (자동 생성된 코드 + 실제 구현)
int Add(handle_t h, int a, int b) { return a + b; }
// 등록
RpcServerUseProtseqEp(L"ncalrpc", ...); // 로컬 RPC (LPC, fastest)
// 또는 RpcServerUseProtseq(L"ncacn_ip_tcp", ...) // TCP
RpcServerRegisterIf(MathInterface_v1_0_s_ifspec, NULL, NULL);
RpcServerListen(...);
// 클라이언트 측
RpcStringBindingCompose(NULL, L"ncalrpc", NULL, ..., &binding);
RpcBindingFromStringBinding(binding, &g_hBinding);
int result = Add(g_hBinding, 2, 3); // 마치 로컬 함수처럼
RPC의 전송 프로토콜 옵션:
| 프로토콜 | 용도 |
|---|---|
ncalrpc | Local RPC (LPC) — 같은 머신, 가장 빠름 |
ncacn_ip_tcp | TCP |
ncacn_np | Named Pipe |
ncacn_http | HTTP (firewall 통과) |
11.3 COM — Component Object Model
COM은 RPC를 객체 단위로 추상화한 모델입니다. 인터페이스(IUnknown 기반)를 표준화하고, 어디서든 같은 방식으로 객체를 만들고 호출합니다.
1
2
3
4
5
6
7
8
// 클라이언트
IShellLink* pSL;
CoInitialize(NULL);
CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
IID_IShellLink, (void**)&pSL);
pSL->SetPath(L"C:\\Windows\\notepad.exe");
pSL->Release();
CoUninitialize();
COM 객체는 위치 투명성(location transparency) 을 가집니다:
| CLSCTX | 의미 |
|---|---|
CLSCTX_INPROC_SERVER | 같은 프로세스 안의 DLL — IPC 없음, 직접 호출 |
CLSCTX_LOCAL_SERVER | 다른 프로세스의 EXE — out-of-proc, 내부적으로 LRPC |
CLSCTX_REMOTE_SERVER | 다른 머신 — DCOM, 내부적으로 RPC over TCP |
클라이언트 코드는 똑같습니다 — 어디서 객체가 만들어지는지 COM 런타임이 알아서 결정. out-of-proc COM이 사실상 RPC인 셈입니다.
11.4 marshalling 비용
1
2
3
4
5
6
7
8
9
10
11
함수 호출 한 번에:
① 인자를 바이트 스트림으로 직렬화 (구조체·포인터 따라가기)
② IPC 채널 (LPC pipe/named pipe/TCP)로 전송
③ 서버 프로세스 깨우기 (컨텍스트 스위치)
④ 서버에서 역직렬화
⑤ 실제 함수 호출
⑥ 결과를 직렬화
⑦ IPC 채널로 반환
⑧ 클라이언트에서 역직렬화
총 비용: 수십~수백 μs
같은 머신·간단한 인자라도 수십 μs는 깁니다 — 공유 메모리 접근(수 ns)의 1000~10000배. 그래서 RPC/COM은 호출 빈도가 낮고 추상화가 중요한 곳에만 쓰는 게 원칙입니다.
11.5 사용 사례
1
2
3
4
5
6
- Windows Shell 확장 (탐색기 컨텍스트 메뉴, 썸네일 핸들러) — COM in-proc DLL
- Office 자동화 (Word·Excel 매크로 API) — COM
- DirectX, Direct2D, DirectWrite — COM 인터페이스
- WMI (Windows Management Instrumentation) — DCOM
- gRPC, Thrift, Cap'n Proto — 크로스플랫폼 RPC 프레임워크
- 일반 마이크로서비스 통신
12. 비용 스펙트럼 정리 — 어느 방식이 얼마나 빠른가
컨텍스트 스위칭(21)에서 동기화 객체 비용 스펙트럼을 정리한 것과 같은 패턴입니다.
12.1 비용 표 (대략적 수치, 같은 머신·작은 메시지 기준)
| IPC 메커니즘 | 한 회 비용 | 상대 비용 | 비고 |
|---|---|---|---|
| 공유 메모리 접근 | 수 ns | 1× | 초기 세팅 후, 메모리 접근 그 자체 |
std::atomic (공유 메모리에서) | 수 ns | 1× | CPU 명령 |
| Named Mutex/Event signal+wait | 1~3 μs | ~500× | 컨텍스트 스위칭(21) 그대로 |
| Anonymous Pipe send/recv | 수 μs | ~1000× | 시스템 콜 2회 + 복사 2회 |
| Named Pipe send/recv (로컬) | 수 μs | ~1000× | 비슷 |
| UNIX domain socket send/recv | 수 μs | ~1000× | TCP보다 빠름 |
| POSIX mq send/receive | 수 μs | ~1000× | 메시지 경계 처리 |
| TCP loopback send/recv | 5~30 μs | ~5000× | 프로토콜 스택 비용 |
| PostMessage | 수 μs | ~1000× | GUI 큐 dispatch |
| Mailslot write | 수십 μs | ~5000× | 신뢰성 없음 |
| 시그널 | 수 μs | ~1000× | 인터럽트 처리 |
| MS-RPC 호출 (LRPC) | 수십 μs | ~10000× | marshalling 포함 |
| MS-RPC 호출 (TCP) | 수십~수백 μs | ~50000× | + 프로토콜 스택 |
| COM out-of-proc | 수십~수백 μs | ~50000× | RPC와 비슷 |
| DCOM (네트워크) | 수백 μs~수 ms | ~100000× | + 네트워크 지연 |
12.2 비용 구성 분석
각 방식의 한 회 비용이 어디서 오는지 분해해보면:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
공유 메모리 접근 (수 ns):
└ CPU 메모리 접근 비용 그 자체
파이프/소켓 한 회 (수 μs):
├ 시스템 콜 진입 (모드 스위치, 100~500 ns)
├ 커널 안 버퍼 락 획득 (수십 ns)
├ 사용자 → 커널 데이터 복사 (memcpy, 데이터 크기 비례)
├ 수신자 깨우기 (스케줄러 시그널)
├ (수신자 측) 컨텍스트 스위치 (1~5 μs, 컨텍스트 스위칭(21))
├ 시스템 콜 진입 (수신자)
├ 커널 → 사용자 데이터 복사
└ 시스템 콜 리턴
RPC 한 회 (수십 μs):
├ marshalling (인자 직렬화, 구조체 따라가기)
├ IPC 채널 송신 (위 파이프/소켓 비용)
├ 서버 컨텍스트 스위치
├ unmarshalling
├ 실제 함수 호출
├ 결과 marshalling
├ IPC 채널 반환
└ 클라이언트 unmarshalling
12.3 메시지 크기에 따른 변화
큰 메시지(수 MB)일 때는 비용 구성이 달라집니다.
1
2
3
4
5
6
1MB 데이터 전송:
- 공유 메모리: 수 ns (포인터만 공유, 데이터 자체는 복사 없음)
- 파이프/소켓: 1MB 복사 × 2회 = 수십 ms (메모리 대역폭 한계)
- RPC: marshalling + 1MB 복사 = 수십 ms 이상
→ 큰 데이터는 공유 메모리가 절대적으로 유리
이게 게임 엔진이나 데이터베이스가 공유 메모리를 적극 활용하는 직접적 이유입니다.
12.4 빈도에 따른 누적 비용
1
2
3
4
1초당 10000회 IPC (게임 60fps × 200회):
공유 메모리: 10000 × 수 ns = 수십 μs (무시 가능)
파이프: 10000 × 수 μs = 수십 ms (5% 프레임 예산)
RPC: 10000 × 수십 μs = 수백 ms (프레임 예산 폭발!)
높은 빈도 + 작은 데이터 = 공유 메모리, 낮은 빈도 + 추상화 = RPC/COM 이 큰 그림의 의사결정 축입니다.
13. 트레이드오프 — 속도 vs 안전성 vs 동기화 복잡도
비용만으로는 IPC를 고를 수 없습니다. 안전성·동기화 복잡도·격리 강도가 직교축으로 함께 작용합니다.
13.1 3축 비교 표
| 메커니즘 | 속도 | 안전성/격리 | 동기화 복잡도 | 추상화 |
|---|---|---|---|---|
| 공유 메모리 | ★★★★★ | ★★ (직접 동기화 필요) | ★★★★★ (어렵다) | ★ |
| 메모리 맵 파일 | ★★★★★ | ★★ | ★★★★★ | ★ |
| 명명 파이프 | ★★★★ | ★★★★ | ★★ | ★★ |
| 메시지 큐 | ★★★★ | ★★★★ | ★★ | ★★★ |
| UNIX domain socket | ★★★★ | ★★★★ | ★★ | ★★★ |
| TCP/UDP socket | ★★★ | ★★★★★ | ★★ | ★★★ |
| 시그널/Event | ★★★ | ★★★ | ★★★ (재진입 주의) | ★★ |
| RPC | ★★ | ★★★★★ | ★ (런타임이 처리) | ★★★★★ |
| COM | ★★ | ★★★★★ | ★ | ★★★★★ |
13.2 안전성/격리 — 왜 격리가 깨질까
공유 메모리는 두 프로세스의 가상 주소 공간이 같은 물리 페이지를 공유하므로 격리가 부분적으로 깨집니다. 한 프로세스가 메모리를 잘못 쓰면 다른 프로세스가 그 깨진 데이터를 봅니다. 또 공유 메모리 영역에 포인터를 넣으면 매핑 주소가 달라 깨집니다(6.5절).
반면 파이프·소켓·메시지 큐는 데이터 복사 방식이라 보낸 쪽이 망가져도 받은 쪽의 데이터는 영향 받지 않습니다. 격리가 강합니다. RPC는 한 단계 더 — 인자가 marshalling 과정에서 검증되므로 잘못된 데이터가 들어오면 stub이 거부합니다.
13.3 동기화 복잡도 — 가장 어려운 부분
공유 메모리의 가장 큰 약점입니다. OS가 동기화를 안 해주니까 다음을 모두 직접 처리해야 합니다.
- race condition — 두 프로세스 동시 쓰기 보호
- memory ordering — atomic operations, memory barriers
- 부분 쓰기(torn write) — 8바이트 값을 32비트 머신에서 atomic하게 쓰기 불가
- 데이터 구조의 일관성 — 자료구조 갱신 중간에 다른 프로세스가 보면 깨진 상태가 보임
- 버전 관리 — 생산자가 N번째 데이터를 쓰는 중에 소비자가 N-1번째와 N번째 혼합본을 읽지 않게
1
2
3
4
5
6
7
8
9
10
11
// 잘못된 예 — race condition
struct SharedData {
int x, y;
int magic; // 검증용
};
SharedData* p = (SharedData*)pView;
p->x = 10; // ← 여기서 컨텍스트 스위치
p->y = 20;
p->magic = 0xDEADBEEF;
// 다른 프로세스가 위 시점에 읽으면 x=10, y=이전값, magic=이전값
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 올바른 예 — Mutex로 보호
WaitForSingleObject(hMutex, INFINITE);
p->x = 10;
p->y = 20;
p->magic = 0xDEADBEEF;
ReleaseMutex(hMutex);
// 또는 atomic + 메모리 배리어
std::atomic<bool> ready{false};
p->x = 10;
p->y = 20;
ready.store(true, std::memory_order_release);
// 받는 쪽
if (ready.load(std::memory_order_acquire)) {
// x, y 안전하게 읽기
}
메시지 큐·파이프는 메시지 한 단위의 원자성을 OS가 보장하므로 이런 문제가 없습니다. 그게 추상화의 가치입니다.
13.4 격리 vs 속도 — 실무 의사결정
1
2
3
4
5
6
7
8
신뢰 경계 안 (같은 팀·같은 코드 베이스):
→ 공유 메모리 우선 — 속도 절대적
→ 동기화 직접 관리 가능
신뢰 경계 바깥 (외부 프로세스·서드파티):
→ 메시지 큐·소켓·RPC — 격리 강한 메커니즘
→ 입력 검증 필수
→ 속도는 일부 포기
Chrome의 GPU 프로세스는 렌더러와는 공유 메모리(텍스처), 브라우저 메인 프로세스와는 메시지 패싱으로 분리합니다 — 신뢰도가 다르기 때문입니다. 같은 자기 팀 코드끼리는 빠르게, 외부 노출 가능 코드와는 안전하게.
13.5 동기화 객체 + 데이터 채널의 조합 패턴
실무에서 가장 흔한 패턴이 공유 메모리 + 동기화 객체 조합입니다.
1
2
3
4
공유 메모리: 데이터
Named Mutex: 데이터 보호
Named Event: "새 데이터 있음" 알림
Named Semaphore: 슬롯 카운팅 (큐 모델)
이 조합은 공유 메모리의 속도 + 동기화 객체의 안전성을 함께 얻습니다. 게임 엔진의 프로세스 분리, DBMS의 워커 풀, 비디오 인코더의 GPU 인터페이스 모두 이 패턴.
14. 보안 — 신뢰 경계와 ACL
IPC는 신뢰 경계(trust boundary) 를 넘는 통신이므로 보안 고려가 필수입니다.
14.1 명명된 IPC 객체의 ACL
Windows에서 이름 있는 IPC 객체(Named Pipe·Named Mutex·File Mapping·Mailslot)는 보안 디스크립터(SECURITY_DESCRIPTOR) 로 접근 제어합니다.
1
2
3
4
5
SECURITY_ATTRIBUTES sa = { sizeof(sa), pSD, FALSE };
// pSD는 직접 만들거나 ConvertStringSecurityDescriptorToSecurityDescriptor로
HANDLE hPipe = CreateNamedPipe(L"\\\\.\\pipe\\MyService",
..., ..., ..., ..., ..., ..., &sa);
기본값(NULL)은 현재 사용자의 토큰 기준으로 권한이 매겨집니다 — 같은 사용자의 다른 프로세스만 열 수 있고, 다른 사용자나 서비스는 못 엽니다.
14.2 Integrity Level (Windows)
Vista 이후 Windows는 무결성 수준(Integrity Level) 으로 추가 격리를 합니다.
| 수준 | 사용처 |
|---|---|
| System | OS 코어 |
| High | 관리자 권한 (UAC 승격) |
| Medium | 일반 사용자 프로세스 (기본) |
| Low | Internet Explorer protected mode, Chrome 렌더러 |
| Untrusted | 더 강한 샌드박스 |
낮은 IL 프로세스는 높은 IL이 가진 객체에 쓰기 권한이 제한됩니다. 그래서 샌드박스된 렌더러(Low)가 브라우저 메인 프로세스(Medium)의 객체를 마음대로 못 만집니다. IPC 채널을 통해서만 제한된 명령을 보낼 수 있습니다.
14.3 신뢰 경계와 입력 검증
IPC로 들어오는 데이터는 모두 신뢰할 수 없는 입력으로 취급해야 합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 잘못된 예
struct Command {
int op;
int size;
char data[1024];
};
void HandleCommand(const Command* cmd) {
memcpy(g_buffer, cmd->data, cmd->size); // cmd->size가 1024 초과면? 1MB라면?
}
// 올바른 예
void HandleCommand(const Command* cmd) {
if (cmd->size < 0 || cmd->size > sizeof(cmd->data) || cmd->size > sizeof(g_buffer)) {
// 거부
return;
}
memcpy(g_buffer, cmd->data, cmd->size);
}
IPC가 보안 취약점의 흔한 출처인 이유 — 코드가 같은 팀에서 작성됐다고 가정하고 검증을 빠뜨립니다.
14.4 인증과 임퍼소네이션
Windows의 명명 파이프는 클라이언트 임퍼소네이션(impersonation) 을 지원합니다 — 서버가 클라이언트의 보안 토큰을 빌려 그 권한으로 동작.
1
2
3
4
5
ConnectNamedPipe(hPipe, NULL);
ImpersonateNamedPipeClient(hPipe);
// 이제 이 스레드는 클라이언트의 권한으로 동작
// (파일 접근 등 클라이언트가 가진 권한만 사용 가능)
RevertToSelf();
서비스가 클라이언트 권한으로 자원 접근하는 패턴. 임퍼소네이션 토큰을 잘못 다루면 권한 상승 취약점이 됨.
14.5 보안 측면 트레이드오프
| 메커니즘 | 격리 | 인증 | 입력 검증 부담 |
|---|---|---|---|
| 공유 메모리 | 낮음 (포인터 깨짐 위험) | 객체 ACL | 매우 큼 (구조체 자체 신뢰 불가) |
| 명명 파이프 | 높음 | ACL + 임퍼소네이션 | 보통 (메시지 단위 검증) |
| 메시지 큐 | 높음 | ACL | 보통 |
| 소켓 | 매우 높음 | 외부 인증 추가 가능 | 매우 큼 (네트워크 신뢰 안 됨) |
| RPC/COM | 매우 높음 | 토큰 + 인증 콜백 | 자동 (marshalling이 일부 검증) |
15. 언리얼에서의 IPC — FPlatformProcess·MessageBus·Hot Reload
언리얼 엔진은 IPC를 에디터-외부 도구 통신과 분산 빌드/협업에 활용합니다.
15.1 FPlatformProcess — 플랫폼 추상화
언리얼은 모든 IPC를 FPlatformProcess 인터페이스 뒤에 숨깁니다. Windows에선 Win32 API, Linux/Mac에선 POSIX로 매핑.
1
2
3
4
5
6
7
8
9
10
11
// 자식 프로세스 실행
FProcHandle proc = FPlatformProcess::CreateProc(
*AppPath, // 실행 파일
*Args, // 인자
true, // 분리된 콘솔
false, false, // 숨김·minimize
nullptr, // PID 출력
0, // 우선순위
nullptr, // 작업 디렉터리
nullptr // 표준 입출력 파이프
);
15.2 익명 파이프 — FPlatformProcess::CreatePipe
1
2
3
4
5
6
7
8
9
10
void* ReadPipe = nullptr;
void* WritePipe = nullptr;
FPlatformProcess::CreatePipe(ReadPipe, WritePipe);
// CreateProc에 ReadPipe/WritePipe 전달해 자식 프로세스 stdout 캡처
// 자식 출력 읽기
FString Output = FPlatformProcess::ReadPipe(ReadPipe);
FPlatformProcess::ClosePipe(ReadPipe, WritePipe);
내부적으로 Windows에선 CreatePipe → CreateProcess에 STDIN/STDOUT 핸들 연결.
대표 사용처:
- UnrealBuildTool 실행 후 컴파일러 출력 캡처
- 셰이더 컴파일러 자식 프로세스 모니터링
- 에디터 ↔ Live Coding 컴파일러 통신
15.3 IMessageBus / MessageBus 플러그인 — 같은 인스턴스 내 메시지 패싱
언리얼의 MessageBus 플러그인은 같은 머신·같은 에디터 인스턴스 안에서 모듈 간 메시지 패싱을 추상화합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 메시지 정의
USTRUCT()
struct FMyCommand
{
GENERATED_BODY()
UPROPERTY() FString Action;
};
// 송신
TSharedPtr<IMessageBus> Bus = IMessageBusManager::Get()->GetMessageBus();
TSharedRef<IMessageEndpoint> Sender = FMessageEndpoint::Builder("MySender").Build();
Sender->Publish(new FMyCommand{ "DoStuff" });
// 수신
TSharedRef<IMessageEndpoint> Receiver = FMessageEndpoint::Builder("MyReceiver")
.Handling<FMyCommand>([](const FMyCommand& Msg, const IMessageContextRef& Ctx){
UE_LOG(LogTemp, Log, TEXT("Got: %s"), *Msg.Action);
})
.Build();
Receiver->Subscribe<FMyCommand>();
내부적으로는 같은 프로세스 안의 모듈 간 통신이라 IPC가 아니지만, 분산 버전(UDP Messaging 플러그인) 을 활성화하면 같은 LAN의 다른 언리얼 인스턴스로도 메시지가 전파됩니다 — 진짜 IPC가 됩니다.
15.4 Live Coding — 에디터 ↔ 컴파일러
언리얼 5의 Live Coding은 에디터가 실행 중인 상태에서 코드 변경을 핫리로드하는 기능인데, 에디터 프로세스 ↔ Live++ 컴파일러 프로세스 사이에 명명 파이프로 통신합니다.
1
2
3
4
5
UE5Editor.exe ──[named pipe]──→ Live++.exe (또는 LiveCoding subprocess)
↑ ↓
│ 소스 코드 컴파일
│ ↓
└──[패치 정보 + DLL]───────── patch 결과
에디터 프로세스가 코드 변경을 컴파일러에 알리고, 컴파일러가 결과 DLL을 에디터에 전달하면 에디터가 그걸 핫패치합니다. 정확한 구현은 비공개이지만 패턴은 명명 파이프 + 메시지 직렬화.
15.5 외부 빌드 도구와의 통신
언리얼은 빌드·셰이더 컴파일·라이팅 빌드에 별도 프로세스를 광범위하게 사용합니다.
1
2
3
4
5
6
7
8
9
10
11
UE5Editor
├─ UnrealBuildTool.exe (C# 코드 빌드 오케스트레이션)
├─ ShaderCompileWorker.exe (다수) (셰이더 병렬 컴파일)
├─ SwarmAgent.exe / SwarmCoordinator (라이팅 빌드 분산)
└─ UnrealLightmass.exe (라이트맵 계산 워커)
통신 방식:
- UBT: 자식 프로세스로 실행, stdout 파이프로 진행 캡처
- ShaderCompileWorker: 명명 파이프 또는 임시 파일 (대량 데이터)
- Swarm: TCP 소켓 (네트워크 분산)
- Lightmass: 공유 메모리 (대용량 메시 데이터)
이게 IPC의 실무 적용 사례 전부를 보여줍니다 — 명령 전달은 파이프, 대용량 데이터는 공유 메모리, 네트워크 분산은 소켓.
15.6 언리얼 IPC 정리 표
| 시나리오 | 메커니즘 | 비고 |
|---|---|---|
빌드 도구 실행 (UBT) | CreateProc + stdout 파이프 | 표준 출력 캡처 |
| 셰이더 컴파일 워커 | 명명 파이프 + 임시 파일 | 다수 워커 병렬 |
| Live Coding | 명명 파이프 | 패치 데이터 양방향 |
| 라이팅 빌드 분산 | TCP 소켓 (Swarm) | 네트워크 분산 |
| 같은 머신 분산 통신 | UDP Messaging 플러그인 | 멀티 에디터 협업 |
| 모듈 간 메시지 (in-proc) | MessageBus | 같은 프로세스 |
| Slate IPC (드래그·복사) | Windows clipboard / OLE | OS 제공 메커니즘 |
15.7 게임 런타임에서는?
게임 런타임 자체는 단일 프로세스가 기본이라 IPC가 거의 없습니다. 네트워크 게임은 IPC가 아니라 NetDriver(UDP 소켓 기반)로 클라이언트-서버 통신. 그래서 면접에서 “언리얼 게임에서 IPC 어디 쓰나”는 질문은 보통 에디터 도구 체인 쪽으로 답하면 됩니다.
16. 꼬리질문 예상 경로
Q1. “프로세스끼리 통신할 수 있는 방법(IPC)에 대해 설명해주세요.”
IPC(Inter-Process Communication, 프로세스 간 통신)는 OS가 강제하는 주소 공간 격리를 깨지 않으면서 서로 다른 프로세스끼리 데이터를 옮기거나 동기화 신호를 교환하는 메커니즘의 총칭입니다. 프로세스 vs 스레드(19)에서 봤듯이 같은 프로세스 안의 스레드는 코드·데이터·힙을 공유하므로 전역 변수로 통신할 수 있지만, 프로세스끼리는 가상 주소 공간이 다르고 페이지 테이블도 달라서 같은 가상 주소가 서로 다른 물리 메모리를 가리킵니다. 그래서 반드시 커널이 한 번 중재해야 합니다.
분류축으로 보면 ① 데이터 전송이냐 신호 전송이냐(파이프·메시지 큐·공유 메모리·소켓 vs 시그널·세마포어·Event), ② 매번 커널을 거치느냐 사용자 모드에서 끝나느냐(파이프·소켓 vs 공유 메모리), ③ 같은 머신만 가능하냐 네트워크까지 가느냐(익명 파이프·shm vs 명명 파이프·소켓·RPC)로 정리할 수 있습니다.
메커니즘별로 보면 공유 메모리·메모리 맵 파일이 가장 빠르고(수 ns), 파이프·소켓·메시지 큐가 중간(수~수십 μs), RPC와 COM이 가장 느리지만 추상화가 가장 높습니다(수십~수백 μs).
Q2. “왜 프로세스끼리는 그냥 메모리에 못 쓰고 IPC가 필요한가요?”
주소 공간 격리(address space isolation) 때문입니다. OS는 각 프로세스에게 자기 가상 주소 공간과 페이지 테이블을 줘서, 같은 가상 주소가 서로 다른 물리 메모리를 가리키게 만듭니다. MMU(Memory Management Unit)가 페이지 테이블로 이 격리를 강제하고, 컨텍스트 스위칭(21)에서 본 것처럼 프로세스 전환 시 페이지 테이블 베이스 레지스터(x86 CR3)를 교체합니다.
이 격리가 필요한 이유는 세 가지입니다. 안정성 — 한 프로세스의 버그가 다른 프로세스의 메모리를 망가뜨리면 안 됩니다. 보안 — 악의적 프로세스가 다른 프로세스의 비밀 데이터를 읽으면 안 됩니다. 자원 격리 — 한 프로세스가 죽어도 다른 프로세스는 살아 있어야 합니다(Chrome의 탭별 프로세스 모델이 대표 예).
격리가 사용자 모드에선 절대 깨지지 않으므로, 우회하려면 반드시 커널 모드를 거쳐야 하고 그게 시스템 콜입니다. 이게 IPC가 풀어야 하는 핵심 문제입니다.
Q3. “파이프와 공유 메모리의 차이는 무엇인가요?”
가장 큰 차이는 데이터 이동 방식과 비용입니다.
파이프(pipe) 는 단방향 바이트 스트림으로, 커널이 관리하는 FIFO 버퍼를 통해 데이터가 이동합니다. 매 송수신마다 시스템 콜이 일어나고 데이터가 사용자→커널→사용자로 두 번 복사됩니다. 한 회 비용은 수 microsecond. 메시지 경계가 없어서 4바이트 두 번 쓰면 8바이트로 한 번에 읽힐 수 있습니다. 익명 파이프(
CreatePipe/pipe)는 부모-자식 한정이고, 명명 파이프(CreateNamedPipe/mkfifo)는 무관한 프로세스 간이나 네트워크 통신도 가능합니다.공유 메모리(shared memory) 는 두 프로세스가 같은 물리 페이지를 자기 가상 주소 공간에 매핑해두고, 그 메모리를 직접 읽고 쓰는 방식입니다. 초기 세팅(
CreateFileMapping/MapViewOfFile,shm_open/mmap)에만 시스템 콜이 들고, 그 뒤로는 일반 메모리 접근 속도(수 nanosecond)와 같습니다. 1MB 데이터도 복사 없이 “공유”됩니다.단 공유 메모리는 OS가 동기화를 안 해주므로 race condition을 직접 막아야 합니다 — 보통 이름 있는 Mutex·Semaphore·Event와 짝지어 씁니다. 그래서 “공유 메모리는 빠르지만 어렵다”, “파이프는 느리지만 안전하다”가 트레이드오프입니다.
Q4. “왜 공유 메모리가 가장 빠른 IPC라고 하나요?”
다른 IPC는 매 송수신마다 시스템 콜 + 모드 스위치 + 커널 안 버퍼 복사가 필요합니다. 컨텍스트 스위칭(21)에서 본 것처럼 시스템 콜 하나가 100~500 nanosecond, 거기에 데이터 복사 비용이 메시지 크기에 비례해서 붙습니다. 1MB를 보내면 사용자→커널 복사 + 커널→사용자 복사 = 총 2MB의 메모리 복사가 일어납니다.
공유 메모리는 초기 세팅 한 번만 시스템 콜이고, 그 뒤로는 두 프로세스가 같은 물리 페이지를 가리키는 가상 주소에 그냥 메모리 접근만 합니다. 1MB를 “전달”하는 게 사실은 그 영역에 쓰는 것이라 복사도 없습니다. 매 접근 비용이 수 nanosecond — 일반 메모리 접근 속도 그 자체입니다.
그래서 큰 데이터·고빈도 IPC에서 공유 메모리가 절대적으로 유리합니다. 게임 엔진의 렌더 명령 버퍼, DBMS의 buffer pool, Chrome의 텍스처 공유 모두 공유 메모리 기반입니다. 다만 동기화와 메시지 경계 처리를 직접 해야 하므로 세마포어·뮤텍스·atomic과 거의 항상 같이 씁니다.
Q5. “메시지 큐와 파이프의 차이는 무엇인가요?”
가장 큰 차이는 메시지 경계(message boundary) 가 보존되느냐입니다.
파이프는 바이트 스트림이라 송신자가 4바이트, 8바이트를 차례로 써도 수신자가 12바이트로 한 번에 받을 수 있습니다. 메시지 단위 구분은 응용 프로그램이 직접 구현해야 합니다(예: 헤더에 길이 정보).
메시지 큐는 각 메시지를 하나의 단위로 큐에 넣고, 수신자가 한 번에 한 메시지씩 받습니다. 4바이트 + 8바이트로 보낸 두 메시지가 받는 쪽에서도 4바이트, 8바이트로 분리되어 옵니다.
POSIX 메시지 큐(
mq_open/mq_send/mq_receive)는 추가로 우선순위를 지원해 우선순위 높은 메시지가 먼저 빠집니다. Windows에선 GUI 스레드 메시지 큐(PostMessage/SendMessage)와Mailslot이 메시지 큐 역할을 하는데, Mailslot은 단방향 브로드캐스트라 신뢰성이 없고 PostMessage는 같은 데스크톱·같은 머신 GUI 한정이라는 제약이 있습니다.메시지 큐는 메시지가 작고 경계가 중요한 명령 전달에 적합하고, 큰 스트림 데이터(파일 전송 등)는 파이프나 소켓이 더 자연스럽습니다.
Q6. “Windows에서 같은 프로세스에 쓸 동기화 객체와 IPC에 쓸 동기화 객체의 차이가 있나요?”
이름을 부여하느냐와 어느 객체를 고르느냐가 핵심 차이입니다. 컨텍스트 스위칭(21)에서 본 동기화 객체들을 IPC로 확장하려면:
같은 프로세스 내 한정 — Critical Section, SRWLock, Condition Variable,
std::mutex(SRWLock 기반). 이들은 사용자 모드 우선 객체로 무경합 시 빠르지만(50~100 ns) 사용자 메모리에 카운터를 두므로 다른 프로세스가 못 봅니다.프로세스 간 공유 가능 — Mutex(커널 객체), Semaphore, Event. 이름(
Global\\MyMutex)을 주고CreateMutex/OpenMutex로 다른 프로세스에서도 같은 객체에 접근할 수 있습니다. 단 무경합이라도 항상 커널 진입이라 1~3 μs로 SRWLock의 20~50배 비쌉니다.그래서 같은 프로세스 안에선 SRWLock/
std::mutex가 답이고, 프로세스 간 공유가 진짜 필요할 때만 Named Mutex/Event/Semaphore를 씁니다. POSIX는 약간 다르게 —pthread_mutex를 공유 메모리에 놓고PTHREAD_PROCESS_SHARED속성을 줘서 프로세스 간 공유합니다.전형적 패턴이 “공유 메모리 + Named Mutex/Event/Semaphore” 조합으로, 데이터는 공유 메모리에서 빠르게, 동기화는 커널 객체로 안전하게 분업합니다.
Q7. “Windows에는 POSIX 시그널 같은 게 있나요?”
직접 대응은 없습니다. Windows는 POSIX의 비동기 시그널 모델을 표준으로 지원하지 않습니다. 일부 표준 C 시그널(
SIGINT,SIGTERM)은 흉내내지만 한정적이고, 시그널 핸들러의 비동기·재진입 의미도 정확히 같지 않습니다.대신 Windows에선 Event 객체 +
WaitForSingleObject가 같은 역할을 합니다. 시그널을 보내는 측이SetEvent(hEvent)를 호출하면, 대기 중인 스레드가WaitForSingleObject에서 깨어나 핸들러 코드를 실행합니다. 차이는 시그널이 비동기 인터럽트로 끼어드는 데 비해 Event는 명시적으로 대기 중인 스레드만 깬다는 점입니다 — 그래서 async-signal-safe 같은 제약이 없고 더 다루기 쉽습니다.콘솔 앱의 Ctrl+C는
SetConsoleCtrlHandler(CTRL_C_EVENT)로 받습니다. Access violation 같은 하드웨어 예외는 SEH(__try/__except) 또는 Vectored Exception Handler로 처리합니다. 자식 프로세스 종료(SIGCHLD)는WaitForSingleObject(hProcess, INFINITE)로 대기합니다.그래서 Windows로 포팅할 때 POSIX 시그널 패턴을 그대로 옮길 수 없고, 이벤트 객체 기반의 통지 모델로 다시 설계하는 게 표준 접근입니다.
Q8. “메모리 맵 파일과 공유 메모리는 어떻게 다른가요?”
API가 거의 같고, 가리키는 대상만 다릅니다.
Windows의
CreateFileMapping은 첫 번째 인자로 파일 핸들을 받습니다.INVALID_HANDLE_VALUE를 주면 페이징 파일 기반의 익명 공유 메모리(여러 프로세스가 같은 매핑 객체를 이름으로 찾아 공유), 실제 파일 핸들을 주면 메모리 맵 파일(그 파일의 내용이 메모리로 보임)이 됩니다. POSIX도shm_open+mmap이 익명 공유 메모리,open+mmap이 메모리 맵 파일로 패턴이 같습니다.차이는 영속성과 페이지 캐시 공유입니다. 메모리 맵 파일은 실제 파일과 자동 동기화되므로 프로세스가 죽어도 데이터가 디스크에 남고, OS의 페이지 캐시(page cache)를 활용해서 같은 파일을 매핑한 여러 프로세스가 자동으로 같은 RAM 페이지를 공유합니다. 공유 메모리는 페이징 파일 기반이라 영속성이 없고 모든 프로세스가 닫으면 사라집니다.
그래서 데이터를 영속화하면서 IPC로도 쓰고 싶을 때(예: DB의 데이터 파일, 게임의 애셋 패키지), 그리고 메모리보다 큰 데이터를 다룰 때(page fault 기반 lazy loading)는 메모리 맵 파일이 자연스럽고, 순수 IPC로만 쓸 때는 익명 공유 메모리가 단순합니다.
Q9. “RPC와 일반 IPC의 차이는 무엇이고 비용은 왜 비싼가요?”
RPC(Remote Procedure Call)는 IPC를 함수 호출처럼 추상화한 고수준 메커니즘입니다. 클라이언트가
Add(2, 3)같은 일반 함수 호출을 하면, 그 호출이 다른 프로세스(또는 다른 머신)에서 실행되고 결과가 돌아옵니다. Windows의 MS-RPC, COM의 out-of-proc, gRPC·Thrift·Cap’n Proto 같은 크로스플랫폼 프레임워크가 모두 이 패러다임입니다.핵심 기술이 marshalling입니다. IDL(Interface Definition Language)로 인터페이스를 정의하면
midl이나 protoc 같은 컴파일러가 stub 코드를 자동 생성하고, stub이 함수 인자를 바이트 스트림으로 직렬화해 IPC 채널(보통 명명 파이프나 소켓)로 보냅니다. 반대편 stub이 역직렬화해서 실제 함수를 호출하고, 결과를 같은 경로로 보냅니다.비용이 비싼 이유가 이 marshalling/unmarshalling + IPC 채널 비용 + 두 번의 컨텍스트 스위치 때문입니다. 한 호출에 수십~수백 μs 걸리는데, 공유 메모리 한 줄 접근(수 ns)의 1000~10000배입니다.
대신 가치는 추상화 수준입니다. 개발자가 IPC를 명시적으로 다루지 않고, 인터페이스만 정의하면 끝납니다. 위치 투명성(클라이언트 코드가 객체가 같은 프로세스인지 다른 머신인지 몰라도 됨)도 얻습니다. 그래서 호출 빈도가 낮고 추상화가 중요한 곳(Windows Shell 확장, COM 객체 모델, 마이크로서비스)에 적합하고, 고빈도 데이터 통신에는 부적합입니다.
Q10. “Chrome 같은 멀티프로세스 앱이 IPC를 어떻게 사용하나요?”
Chrome은 보안과 안정성을 위해 탭마다 별도 프로세스를 띄우는 사이트 격리(site isolation) 모델을 채택했고, 그 비용을 IPC로 감수합니다. 그래서 IPC 사용량이 어마어마하고, 각 통신의 특성에 맞춰 메커니즘을 다르게 씁니다.
큰 그림으로 보면:
- 브라우저 메인 ↔ 렌더러 (제어 신호) — 메시지 패싱(Mojo IPC 프레임워크, 내부적으로 명명 파이프 또는 UNIX socket). 격리가 강하고 입력 검증이 엄격합니다. 렌더러는 Low Integrity Level로 샌드박스 안에 갇혀 있어서 메인 프로세스의 자원을 직접 못 만집니다.
- 렌더러 ↔ GPU 프로세스 (텍스처·렌더 명령) — 공유 메모리. 매 프레임 GB 단위로 텍스처를 주고받으니 복사를 피해야 합니다. 동기화는 세마포어와 fence 객체.
- 모든 프로세스 ↔ 메인 (종료·설정) — Event 객체 또는 Mojo 메시지.
핵심 원칙이 신뢰 경계와 데이터 크기에 따라 메커니즘 선택입니다. 신뢰가 낮으면 메시지 패싱(격리 강함), 같은 팀 코드끼리 큰 데이터는 공유 메모리(빠름). 이게 IPC 의사결정의 일반 원칙이고, 컨텍스트 스위칭(21)에서 본 동기화 객체 선택과 같은 구조입니다.
언리얼 엔진도 비슷한 패턴을 따릅니다. 빌드 도구(UBT) 같은 명령 통신은 파이프 + stdout 캡처, 셰이더 컴파일 워커처럼 다수 워커와의 대용량 데이터 통신은 공유 메모리 + 임시 파일, 라이팅 빌드 분산처럼 네트워크 분산은 TCP 소켓 — 각각의 자리에 맞는 IPC를 고릅니다.
17. 핵심 요약 카드 — 확장판
정의·분류축 3개·메커니즘 카탈로그·기본 비용 스펙트럼은 1장 요약 카드와 같으므로 반복하지 않습니다. 여기서는 본문(§12~§15)을 지나며 더해진 내용만 모읍니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
비용 스펙트럼 확장 — RPC 계열 세분 (§12 표 참조):
MS-RPC (LRPC) 수십 μs
MS-RPC (TCP) 수십~수백 μs
COM out-of-proc 수십~수백 μs
DCOM (네트워크) 수백 μs~수 ms
선택 기준:
큰 데이터·고빈도 → 공유 메모리 + Named 동기화 객체
메시지 경계 필요 → 메시지 큐 / Mailslot / UDP
네트워크 확장 가능성 → 소켓 / 명명 파이프
함수 호출 추상화 → RPC / COM
단순 신호 → Event / 시그널
신뢰 경계 강함 → 메시지 패싱·소켓·RPC (격리 강)
신뢰 경계 약함 → 공유 메모리 (속도 우선)
트레이드오프:
속도 ↔ 동기화 복잡도
격리·안전성 ↔ 추상화 수준
같은 머신 ↔ 네트워크 확장
Windows 핵심 API:
파이프 CreatePipe / CreateNamedPipe(\\.\pipe\name)
공유 메모리 CreateFileMapping(INVALID_HANDLE_VALUE) + MapViewOfFile
메모리 맵 파일 CreateFileMapping(real file handle) + MapViewOfFile
동기화 객체 CreateMutex / CreateSemaphore / CreateEvent (이름 부여)
대기 WaitForSingleObject / WaitForMultipleObjects
메시지 PostMessage / SendMessage / WM_COPYDATA / Mailslot
소켓 socket / Winsock (AF_INET, AF_UNIX) + IOCP
RPC midl IDL → RpcServerListen / 자동 stub
COM CoCreateInstance + CLSCTX_LOCAL_SERVER
언리얼 매핑:
자식 프로세스 FPlatformProcess::CreateProc
익명 파이프 FPlatformProcess::CreatePipe (stdout 캡처)
모듈 간 메시지 IMessageBus (in-proc) / UDP Messaging (분산)
Live Coding 명명 파이프
셰이더 컴파일 워커 명명 파이프 + 임시 파일
라이팅 빌드 분산 TCP 소켓 (Swarm)
Lightmass 공유 메모리 (대용량 메시 데이터)
보안:
ACL Named 객체에 SECURITY_DESCRIPTOR
Integrity Level Low IL 프로세스는 High IL 객체 접근 제한
입력 검증 IPC는 신뢰 경계 → 모든 입력 검증
기억할 한 줄:
"IPC 선택 = 빈도·크기·격리의 의사결정. 메커니즘은 OS가 주지만 비용은 엔지니어링."
18. 회귀 다리 — 다른 CS 파일 연결
| 파일 | 연결 지점 |
|---|---|
| 01_runtime | 메모리 4영역(Code/Data/Heap/Stack) — 공유 메모리는 두 프로세스의 가상 주소 공간(Stack은 분리, 공유 영역만 매핑)에 동시 매핑되는 특수 영역. 페이지 테이블 매핑 변경이 본질 |
| 03_new_vs_malloc | 힙은 프로세스별로 독립 — 한 프로세스의 new로 잡은 메모리를 다른 프로세스가 접근 못 함. IPC가 그 격리를 우회하는 방법 |
| 09_rtti_raii | std::lock_guard/std::unique_lock 같은 RAII가 Named Mutex에도 그대로 적용 — 컨텍스트 스위치 후 unlock 누락 방지 (Windows에선 CHandle 래퍼 권장) |
| 11_smart_pointer | 공유 메모리에서 raw pointer 깨짐 — 매핑 주소 차이로 P1의 포인터가 P2에선 무의미. shared_ptr 같은 추상화도 IPC 경계에선 동작 안 함 (제어 블록이 한 프로세스 주소에 있음) |
| 16_stl_containers | STL 컨테이너는 한 프로세스 한정 — 포인터·iterator 기반이라 공유 메모리에 넣어도 다른 프로세스에서 못 씀. boost::interprocess 같은 IPC 친화 컨테이너 필요 |
| 19_process_vs_thread | IPC가 프로세스 vs 스레드(19)의 직접 회귀 — “스레드는 공유, 프로세스는 격리”의 격리 쪽을 IPC가 풀어줌. PCB·페이지 테이블·핸들 테이블이 IPC 비용의 출발점 |
| 20_stack_overflow | 프로세스별 독립 스택 — IPC 메시지가 너무 크면 수신측 스택 오버플로 위험. 공유 메모리는 스택과 별개의 매핑이라 스택 비례 안 함 |
| 21_context_switching | IPC 비용 핵심 요소 — 매 송수신 = 모드 스위치 + (블로킹 IPC면) 컨텍스트 스위치. 컨텍스트 스위칭(21)의 비용 스펙트럼이 그대로 적용. 공유 메모리가 빠른 이유가 컨텍스트 스위칭(21)의 “사용자 모드에서 끝나면 컨텍스트 스위치 없음”과 같은 원리 |
핵심 요약 — 모든 IPC는 주소 공간 격리를 커널 중재로 우회하는 방법이고, 공유 메모리(수 ns)부터 RPC/COM(수십~수백 μs)까지 같은 “데이터 옮기기”에 1000배 이상 비용 차이가 난다. 그래서 선택 기준은 빈도·데이터 크기·신뢰 경계이고, 실무 기본 패턴은 공유 메모리(데이터) + Named 동기화 객체(신호)의 분업이다.