## 소개글
> [!abstract] Introduction
> 운영체제 위에서 동작하는 프로세스들은 각각 독립된 주소 공간을 가지지만, 때로는 서로 데이터를 주고받거나 실행 순서를 조율해야 할 때가 있습니다. 이를 가능하게 하는 메커니즘이 바로 프로세스 간 통신, IPC*Inter-Process Communication*입니다. 이 글에서는 IPC의 필요성과 두 가지 근본 모델(공유 메모리, 메시지 전달)을 살펴본 뒤, 리눅스가 제공하는 대표적인 IPC 메커니즘 — 파이프, 시그널, 공유 메모리, 메시지 큐, 유닉스 도메인 소켓, Futex — 의 원리와 커널 구현까지를 다룹니다.
## 왜 프로세스 간 통신이 필요한가
[[Process|프로세스]]는 운영체제가 부여한 독립된 주소 공간 안에서 실행된다. 이 격리 덕분에 하나의 프로세스가 다른 프로세스의 메모리를 실수로 훼손하는 일이 방지되지만, 동시에 프로세스끼리 직접 데이터를 주고받는 것도 불가능해진다.
다른 프로세스와 데이터를 공유하지 않고 완전히 독립적으로 실행되는 프로세스를 **독립 프로세스*independent process***라 하고, 다른 프로세스와 데이터를 주고받으며 서로 영향을 미치는 프로세스를 **협력 프로세스*cooperating process***라 한다(Silberschatz, Galvin and Gagne, 2018). 실제 시스템에서는 대부분의 프로세스가 협력 프로세스다. 웹 서버는 데이터베이스 프로세스에 질의 결과를 요청하고, 셸은 파이프(`|`)로 여러 명령어의 출력과 입력을 연결하며, 컨테이너 런타임은 커널과 상태를 교환한다.
프로세스 간 협력이 필요한 이유는 크게 세 가지다(Silberschatz, Galvin and Gagne, 2018).
**정보 공유*information sharing***: 여러 프로세스가 동일한 데이터에 접근해야 할 때, 각자 별도의 복사본을 가지는 것보다 공유하는 편이 효율적이다. 예컨대 데이터베이스의 공유 버퍼 캐시가 이에 해당한다.
**계산 가속*computation speedup***: 하나의 작업을 여러 프로세스에 분할하여 병렬로 처리하면 처리 시간을 단축할 수 있다. 이를 위해서는 부분 결과를 주고받는 통신 채널이 필수적이다.
**모듈화*modularity***: 시스템을 기능 단위의 독립적인 프로세스로 분리하면 설계, 구현, 유지보수가 용이해진다. 마이크로서비스 아키텍처가 대표적인 사례다.
이처럼 프로세스 간 협력이 불가피한 상황에서, 격리된 주소 공간 사이에 **통제된 통신 경로**를 제공하는 것이 IPC의 역할이다.
## 두 가지 근본 모델
프로세스 간 통신은 근본적으로 두 가지 모델로 나뉜다(Silberschatz, Galvin and Gagne, 2018).
![[IPCModel.png]]
### 공유 메모리 모델
공유 메모리*shared memory* 모델에서는 통신에 참여하는 프로세스들이 주소 공간의 일부를 공유한다. 커널은 공유 영역을 설정할 때만 관여하고, 이후의 읽기·쓰기는 일반적인 메모리 접근과 동일하므로 [[System Call|시스템 호출]] 오버헤드가 발생하지 않는다. 대량의 데이터를 빈번하게 교환해야 하는 상황에서 가장 빠른 IPC 방식이다.
단, 여러 프로세스가 같은 메모리 영역에 동시에 접근하면 경쟁 상태*race condition*[^race-condition-def]가 발생할 수 있으므로, 프로세스들이 동기화를 직접 책임져야 한다는 부담이 있다.
### 메시지 전달 모델
메시지 전달*message passing* 모델에서는 프로세스들이 커널이 관리하는 채널을 통해 메시지를 `send`하고 `receive`한다. 모든 통신이 커널을 경유하므로 시스템 호출 비용이 수반되지만, 동기화를 커널이 대신 처리해주므로 프로그래밍이 단순하고 안전하다. 소량의 데이터를 교환하거나, 프로세스가 서로 다른 머신에 있는 분산 환경에서 특히 적합하다.
두 모델은 상호 배타적이지 않다. 리눅스는 두 모델을 모두 지원하며, 하나의 애플리케이션이 대용량 전송에는 공유 메모리를, 제어 신호에는 메시지 전달을 병행하는 것이 일반적이다.
## 파이프: 가장 오래된 IPC
파이프*pipe*는 1973년 Unix V3에서 도입된 가장 원시적인 IPC 메커니즘이다. 한쪽 끝에서 쓰고 다른 쪽 끝에서 읽는 **단방향 바이트 스트림**을 제공한다(Silberschatz, Galvin and Gagne, 2018).
### 익명 파이프
`pipe()` 시스템 호출은 파일 서술자*file descriptor* 두 개를 반환한다. `fd[0]`은 읽기용, `fd[1]`은 쓰기용이다. 셸에서 `ls | grep txt`를 실행하면, 셸은 내부적으로 `pipe()` → `fork()` → `dup2()`를 차례로 호출하여 `ls`의 표준 출력을 `grep`의 표준 입력으로 연결한다.[^shell-pipe-implementation]
```c
int fd[2];
pipe(fd); // fd[0] = 읽기, fd[1] = 쓰기
pid_t pid = fork();
if (pid == 0) {
close(fd[1]); // 자식: 쓰기 끝 닫기
read(fd[0], buf, n); // 부모가 보낸 데이터 읽기
} else {
close(fd[0]); // 부모: 읽기 끝 닫기
write(fd[1], msg, n); // 자식에게 데이터 보내기
}
```
리눅스 커널에서 파이프의 내부 구현은 **페이지 크기 슬롯의 환형 버퍼*circular buffer***다(`fs/pipe.c`). `struct pipe_inode_info`가 이 버퍼를 관리하며, 기본값으로 16개의 슬롯(= 16 × 4KB = 64KB)을 가진다.[^pipe-buffer-size-tuning] 생산자(쓰기)는 `head` 인덱스를, 소비자(읽기)는 `tail` 인덱스를 전진시킨다. 버퍼가 비어 있으면 읽기 측이, 가득 차면 쓰기 측이 대기 큐에서 잠들고, 상대편이 동작하면 깨어나는 구조다.
```mermaid
flowchart LR
SYS["pipe2()"] --> CREATE["create_pipe_files()\n파이프 아이노드 + 환형 버퍼 할당"]
CREATE --> FDR["fd[0] — O_RDONLY"]
CREATE --> FDW["fd[1] — O_WRONLY"]
FDR & FDW --> RING["환형 버퍼\n16 슬롯 × 4KB = 64KB"]
```
### 이름 있는 파이프(FIFO)
익명 파이프는 `fork()`를 통해 파일 서술자를 상속받는 부모-자식 관계에서만 사용할 수 있다. 부모가 다른 프로세스끼리 파이프로 통신하려면 파일 시스템에 이름이 있는 파이프, 즉 **FIFO**를 사용한다. `mkfifo()` 시스템 호출로 생성하며, 내부 구현은 익명 파이프와 동일한 `pipe_inode_info`를 사용한다. 차이점은 `open()` 의미론이다 — 읽기 쪽이 열리기 전까지 쓰기 쪽의 `open()`이 블로킹된다(`O_NONBLOCK`이 아닌 한).
## 시그널: 비동기 알림
시그널*signal*은 프로세스에 비동기적으로 이벤트를 알리는 소프트웨어 인터럽트다. 데이터를 전달하기보다는 **"어떤 사건이 발생했다"**는 사실 자체를 전달하는 데 초점이 있다(Kerrisk, 2010).
### 시그널의 종류
리눅스는 표준 시그널(1~31)과 실시간 시그널*real-time signal*(32~64)을 제공한다.[^signal-numbers-architecture-dependent]
| 번호 | 이름 | 기본 동작 | 설명 |
|------|------|-----------|------|
| 2 | `SIGINT` | 종료 | 터미널에서 `Ctrl+C` 입력 |
| 9 | `SIGKILL` | 종료(포착 불가) | 프로세스 강제 종료 |
| 11 | `SIGSEGV` | 코어 덤프 | 잘못된 메모리 접근 |
| 13 | `SIGPIPE` | 종료 | 읽는 쪽이 닫힌 파이프에 쓰기 |
| 15 | `SIGTERM` | 종료 | 정상적인 종료 요청 |
| 17 | `SIGCHLD` | 무시 | 자식 프로세스 상태 변경 |
| 19 | `SIGSTOP` | 정지(포착 불가) | 프로세스 일시 정지 |
표준 시그널은 **비큐잉*non-queuing***이다. 같은 시그널이 이미 대기 중이면 추가 인스턴스가 버려진다. 반면 실시간 시그널은 **큐잉*queuing***으로, 여러 인스턴스가 대기열에 쌓일 수 있고 데이터 페이로드(`siginfo_t`)도 함께 전달할 수 있다.
### 커널 구현
시그널 전달의 핵심은 `kernel/signal.c`의 `__send_signal_locked()` 함수다. 이 함수는 대상 태스크의 `struct sigpending`에 시그널을 큐잉하고, `TIF_SIGPENDING` 플래그를 설정한 뒤 해당 스레드를 깨운다. 시그널 처리는 시스템 호출이나 인터럽트에서 사용자 공간으로 복귀하는 시점에 `get_signal()`이 호출되면서 이루어진다.[^signal-handling-at-syscall-return]
```mermaid
flowchart TD
SEND["kill(pid, SIGTERM)\n__send_signal_locked()"] --> QUEUE["sigpending에 시그널 큐잉\nTIF_SIGPENDING 설정"]
QUEUE --> WAKE["대상 스레드 깨우기"]
WAKE --> RET["시스템 호출/인터럽트 복귀 시점"]
RET --> GET["get_signal()\n대기 중인 시그널 디큐"]
GET --> ACT{"핸들러 등록?"}
ACT -->|"SIG_DFL"| DEF["기본 동작\n(종료, 정지 등)"]
ACT -->|"사용자 핸들러"| FRAME["사용자 스택에\n시그널 프레임 구성\nRIP = 핸들러 주소"]
ACT -->|"SIG_IGN"| NEXT["무시하고\n다음 시그널 처리"]
```
프로세스는 `sigaction()` 시스템 호출로 시그널 핸들러를 등록하여 기본 동작을 대체할 수 있다. 단, `SIGKILL`과 `SIGSTOP`은 포착하거나 무시할 수 없다 — 이는 커널이 어떤 상황에서든 프로세스를 종료하거나 정지시킬 수 있는 수단을 보장하기 위함이다.
## 공유 메모리
공유 메모리*shared memory*는 두 개 이상의 프로세스가 물리적으로 동일한 메모리 페이지를 자신의 주소 공간에 매핑하여, 일반적인 메모리 읽기·쓰기만으로 데이터를 교환하는 방식이다. 커널은 매핑 설정 시에만 개입하고 이후의 데이터 전송에는 관여하지 않으므로, 모든 IPC 중 가장 빠르다.
### System V 공유 메모리
System V IPC는 AT&T System V Unix(1983)에서 도입된 고전적 인터페이스로, 공유 메모리·세마포어·메시지 큐 세 가지가 한 세트를 이룬다. 공유 메모리의 수명 주기는 다음과 같다.
1. **`shmget()`**: 키*key*와 크기를 지정하여 공유 메모리 세그먼트를 생성(또는 기존 세그먼트를 조회)한다. 커널은 `shmid_kernel` 구조체를 할당하고, 내부적으로 `tmpfs` 파일을 하나 만들어 데이터를 백업한다.[^sysv-shm-tmpfs-backend]
2. **`shmat()`**: 세그먼트를 호출 프로세스의 주소 공간에 매핑한다. 내부적으로 `do_mmap()`을 호출하므로 결과적으로 `tmpfs` 파일에 대한 `mmap`과 동일하다.
3. **`shmdt()`**: 매핑을 해제한다.
4. **`shmctl(IPC_RMID)`**: 세그먼트를 삭제 표시한다. 부착 카운트(`shm_nattch`)가 0이 되면 실제로 파괴된다.
### POSIX 공유 메모리
POSIX 공유 메모리는 파일 시스템 기반의 더 현대적인 인터페이스를 제공한다.
```c
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);
void *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
```
`shm_open()`은 `/dev/shm`(tmpfs) 위에 파일을 생성하고 파일 서술자를 반환한다. 이후 `mmap()`으로 매핑하면 된다. System V 방식과 달리 파일 서술자 기반이므로 `poll`/`epoll`과 통합할 수 있고, 이름 규칙이 파일 경로로 단순하다.
### 동기화의 책임
공유 메모리 자체에는 동기화 메커니즘이 없다. 여러 프로세스가 동시에 같은 영역에 쓰면 데이터가 손상될 수 있으므로, 세마포어*semaphore*나 뮤텍스*mutex* 같은 별도의 동기화 수단을 반드시 함께 사용해야 한다. 이 동기화 부담이 공유 메모리의 가장 큰 단점이다.
## 메시지 큐
메시지 큐*message queue*는 커널이 관리하는 연결 리스트에 메시지를 쌓아두고, 수신 프로세스가 순서대로(또는 유형별로) 꺼내가는 방식이다.
### System V 메시지 큐
`msgget()`으로 큐를 생성하고, `msgsnd()`로 메시지를 보내고, `msgrcv()`로 꺼낸다. 각 메시지에는 `long` 타입의 유형 필드가 있어, 수신자가 특정 유형의 메시지만 선택적으로 꺼낼 수 있다.[^msgrcv-message-type-filtering]
```c
struct msgbuf {
long mtype; // 메시지 유형 (양수)
char mtext[256]; // 메시지 본문
};
// 송신
struct msgbuf msg = { .mtype = 1, .mtext = "hello" };
msgsnd(qid, &msg, strlen(msg.mtext), 0);
// 수신 — 유형 1인 메시지만 꺼내기
msgrcv(qid, &msg, sizeof(msg.mtext), 1, 0);
```
커널 내부에서 메시지는 `struct msg_msg` 연결 리스트로 관리된다(`ipc/msg.c`). 메시지 본문이 한 페이지를 초과하면 `msg_msgseg` 체인으로 연결된다. 기본 최대 메시지 크기(`MSGMAX`)는 8,192바이트다.
### POSIX 메시지 큐
POSIX 메시지 큐(`mq_open`, `mq_send`, `mq_receive`)는 System V 대비 몇 가지 장점이 있다.
| 항목 | System V | POSIX |
|------|----------|-------|
| 이름 체계 | 정수 키(`ftok()`) | 파일 경로(`/name`) |
| 메시지 순서 | 유형별 FIFO | **우선순위** 기반 |
| 비동기 알림 | 없음 | `mq_notify()` — 시그널 또는 스레드 |
| 파일 서술자 | 없음(ID 기반) | 있음(`poll`/`epoll` 가능) |
POSIX 메시지 큐는 커널 내에서 가상 파일 시스템(`mqueuefs`)으로 구현된다. 메시지는 레드-블랙 트리에 우선순위 순으로 정렬되므로, 최고 우선순위 메시지의 디큐가 $O(1)$이다(`ipc/mqueue.c`).[^posix-mq-rbtree-rightmost-cache]
## 메시지 전달의 설계 선택
메시지 전달 시스템을 설계할 때는 세 가지 축에서 선택이 이루어진다(Silberschatz, Galvin and Gagne, 2018).
### 직접 통신 vs. 간접 통신
**직접 통신*direct communication***에서는 송신자가 수신자의 이름을 명시한다(`send(P, msg)`). 통신 링크가 정확히 두 프로세스 사이에 자동으로 생성되므로 단순하지만, 프로세스 이름이 변경되면 코드도 수정해야 하는 경직성이 있다.
**간접 통신*indirect communication***에서는 메시지가 **메일박스*mailbox***(또는 **포트*port***)라 불리는 중간 객체를 경유한다. 여러 프로세스가 하나의 메일박스를 공유할 수 있어 유연하다. System V 메시지 큐와 POSIX 메시지 큐가 모두 이 모델에 해당한다.
### 동기 vs. 비동기
**블로킹*blocking*(동기적)**: 송신자는 수신자가 메시지를 받을 때까지, 수신자는 메시지가 도착할 때까지 대기한다. 프로그래밍이 직관적이지만 양쪽 프로세스의 진행이 서로에게 묶인다.
**비블로킹*non-blocking*(비동기적)**: 송신자는 메시지를 보내고 즉시 반환되며, 수신자는 메시지가 없으면 `NULL`이나 오류를 반환받는다. 높은 처리량이 필요할 때 유용하지만 프로그래밍 복잡도가 증가한다.
### 버퍼링 용량
커널이 메시지를 임시 저장하는 큐의 용량에 따라 세 가지 경우가 있다.
- **무용량*zero capacity***: 큐가 없어 송신자와 수신자가 직접 만나야 한다(랑데부*rendezvous*).
- **유한 용량*bounded capacity***: 큐에 $n$개의 메시지를 담을 수 있고, 꽉 차면 송신자가 대기한다.
- **무한 용량*unbounded capacity***: 이론적으로 큐 크기에 제한이 없어 송신자가 대기하지 않는다.
실제 시스템에서 무한 용량은 불가능하므로, 유한 용량이 기본이다. 리눅스 파이프는 64KB(유한), System V 메시지 큐는 `MSGMNB`(기본 16,384바이트)로 제한된다.
## 유닉스 도메인 소켓
유닉스 도메인 소켓*Unix domain socket*은 같은 호스트의 프로세스 간에 **양방향** 통신을 제공하는 가장 다재다능한 IPC 메커니즘이다(Kerrisk, 2010). 파이프가 단방향 바이트 스트림만 제공하는 것과 대조적으로, 유닉스 도메인 소켓은 스트림(`SOCK_STREAM`), 데이터그램(`SOCK_DGRAM`), 순서 패킷(`SOCK_SEQPACKET`) 세 가지 모드를 모두 지원한다.
```c
// 양방향 소켓 쌍 생성
int sv[2];
socketpair(AF_UNIX, SOCK_STREAM, 0, sv);
// 이후 fork()하면 부모-자식이 sv[0], sv[1]을 하나씩 사용
```
유닉스 도메인 소켓이 다른 IPC와 구별되는 핵심 기능은 **파일 서술자 전달*file descriptor passing***이다. `SCM_RIGHTS` 보조 메시지*ancillary message*를 통해, 한 프로세스가 자신이 가진 열린 파일 서술자를 다른 프로세스로 전송할 수 있다.[^scm-rights-fd-passing-mechanism] 커널은 송신 측의 `struct file`을 수신 측의 파일 서술자 테이블에 설치해 주므로, 수신 프로세스는 마치 자신이 직접 연 것처럼 해당 파일(소켓, 파이프, 장치 등)에 접근할 수 있다. `SCM_CREDENTIALS`를 사용하면 송신 프로세스의 PID, UID, GID를 인증된 형태로 전달하는 것도 가능하다.
| 특성 | 파이프 | 유닉스 도메인 소켓 |
|------|--------|---------------------|
| 방향 | 단방향 | 양방향 |
| 다중 클라이언트 | 불가 | 가능(`listen`/`accept`) |
| 파일 서술자 전달 | 불가 | 가능(`SCM_RIGHTS`) |
| 자격 증명 전달 | 불가 | 가능(`SCM_CREDENTIALS`) |
| 메시지 경계 보존 | 불가(바이트 스트림만) | 가능(`SOCK_DGRAM`, `SOCK_SEQPACKET`) |
| `epoll`/`select` | 가능 | 가능 |
| 상대적 성능 | 약간 빠름(구조 단순) | 약간 느림(소켓 오버헤드) |
## Futex: 사용자 공간 동기화의 기반
공유 메모리를 사용할 때 반드시 동반되는 동기화 문제를 해결하는 핵심 메커니즘이 **Futex*Fast Userspace Mutex***다. `pthread_mutex_lock()`, `pthread_cond_wait()`, `sem_wait()` 등 사용자 공간의 거의 모든 동기화 프리미티브가 Futex 위에 구축되어 있다(Love, 2010).
Futex의 설계 철학은 단순하다. **경합이 없는 경우*uncontended case*에는 커널에 진입하지 않고, 원자적 CAS*compare-and-swap*[^cas-atomic-instruction] 연산만으로 사용자 공간에서 잠금을 획득한다.** 커널 개입은 실제로 대기가 필요한 경합 상황에서만 발생한다.
```mermaid
flowchart TD
LOCK["pthread_mutex_lock()"] --> CAS{"원자적 CAS\n0 → 1 성공?"}
CAS -->|"성공"| DONE["잠금 획득\n(커널 진입 없음)"]
CAS -->|"실패 — 경합"| WAIT["futex(FUTEX_WAIT)\n커널에서 대기"]
WAIT --> SLEEP["해시 버킷에 큐잉\nschedule() — 잠듦"]
UNLOCK["pthread_mutex_unlock()"] --> SET["잠금 값 → 0"]
SET --> CHECK{"대기자 존재?"}
CHECK -->|"없음"| RET["완료\n(커널 진입 없음)"]
CHECK -->|"있음"| WAKE["futex(FUTEX_WAKE)\n대기자 깨우기"]
```
커널 측에서 Futex는 해시 테이블로 관리된다(`kernel/futex/`). 사용자 공간의 Futex 주소를 해싱하여 `futex_hash_bucket`을 찾고, 그 버킷의 우선순위 정렬 리스트에 대기 중인 `futex_q` 엔트리를 추가한다. `FUTEX_WAKE`가 호출되면 같은 해시 버킷에서 대기 중인 태스크를 찾아 깨운다.
Futex의 효율성은 "빠른 경로*fast path*에서 시스템 호출이 0회"라는 데서 비롯된다. 경합이 드문 워크로드에서는 뮤텍스 연산이 사실상 원자적 메모리 연산 한 번의 비용(수 나노초)으로 완료된다.
## IPC 메커니즘 선택 가이드
리눅스가 제공하는 IPC 메커니즘은 다양하고, 각각의 설계 목적이 다르다. 상황에 따라 적절한 메커니즘을 선택하는 것이 중요하다.
```mermaid
flowchart TD
Q1{"파일 서술자를\n전달해야 하는가?"} -->|"예"| UDS["유닉스 도메인 소켓\n(SCM_RIGHTS)"]
Q1 -->|"아니오"| Q2{"대량 데이터를\n빈번히 교환하는가?"}
Q2 -->|"예"| SHM["공유 메모리\n(POSIX shm + Futex)"]
Q2 -->|"아니오"| Q3{"양방향 통신이\n필요한가?"}
Q3 -->|"예"| UDS2["유닉스 도메인 소켓\n(socketpair)"]
Q3 -->|"아니오"| Q4{"부모-자식 간\n단순 스트림인가?"}
Q4 -->|"예"| PIPE["파이프\n(pipe2)"]
Q4 -->|"아니오"| Q5{"메시지 우선순위가\n필요한가?"}
Q5 -->|"예"| PMQ["POSIX 메시지 큐\n(mq_open)"]
Q5 -->|"아니오"| SMQ["System V 메시지 큐\n또는 파이프"]
```
| 메커니즘 | 모델 | 방향 | 데이터 유형 | 주요 용도 |
|----------|------|------|-------------|-----------|
| 파이프 | 메시지 전달 | 단방향 | 바이트 스트림 | 부모-자식 간 단순 통신, 셸 파이프라인 |
| FIFO | 메시지 전달 | 단방향 | 바이트 스트림 | 비관련 프로세스 간 단순 통신 |
| 시그널 | 알림 | 단방향 | 시그널 번호(+siginfo) | 비동기 이벤트 통지 |
| System V 공유 메모리 | 공유 메모리 | 양방향 | 원시 바이트 | 레거시 대용량 공유(PostgreSQL 등) |
| POSIX 공유 메모리 | 공유 메모리 | 양방향 | 원시 바이트 | 현대적 대용량 공유 |
| System V 메시지 큐 | 메시지 전달 | 양방향 | 유형 메시지 | 레거시 메시지 통신 |
| POSIX 메시지 큐 | 메시지 전달 | 양방향 | 우선순위 메시지 | 우선순위 기반 메시지 통신 |
| 유닉스 도메인 소켓 | 메시지 전달 | 양방향 | 바이트/데이터그램 | 범용 로컬 통신, fd 전달 |
| Futex | 동기화 | — | 정수 | 뮤텍스, 조건 변수, 세마포어 구현 |
## IPC 네임스페이스
리눅스는 **IPC 네임스페이스*IPC namespace***를 통해 IPC 자원을 격리한다(`include/linux/ipc_namespace.h`). 각 IPC 네임스페이스는 독립적인 System V 공유 메모리·세마포어·메시지 큐 세트와 POSIX 메시지 큐 마운트를 가진다.[^ipc-namespace-creation] 컨테이너(Docker, LXC 등)가 별도의 IPC 네임스페이스를 생성하므로, 컨테이너 내부에서 `ipcs`를 실행하면 해당 컨테이너의 IPC 자원만 보인다.
다만 모든 IPC 메커니즘이 네임스페이스로 격리되는 것은 아니다. 파이프와 유닉스 도메인 소켓은 파일 서술자 기반이므로 IPC 네임스페이스의 영향을 받지 않고, 시그널은 PID 네임스페이스에 의해, Futex는 가상 주소에 의해 자연스럽게 격리된다.
---
[^race-condition-def]: 경쟁 상태*race condition*란 두 개 이상의 실행 흐름이 공유 자원에 동시에 접근할 때, 접근 순서에 따라 결과가 달라지는 상황이다. 공유 메모리에서 한 프로세스가 쓰는 도중에 다른 프로세스가 읽으면 부분적으로 갱신된 데이터를 읽게 되는 것이 대표적인 예다. — Silberschatz, Galvin and Gagne (2018), §6.1.
[^shell-pipe-implementation]: 셸의 파이프라인 구현 과정: 셸은 먼저 `pipe()`로 파이프를 생성한 뒤 `fork()`로 각 명령어에 대한 자식 프로세스를 만든다. 왼쪽 명령어의 자식은 `dup2(fd[1], STDOUT_FILENO)`로 표준 출력을 파이프 쓰기 끝에 연결하고, 오른쪽 명령어의 자식은 `dup2(fd[0], STDIN_FILENO)`로 표준 입력을 파이프 읽기 끝에 연결한 뒤, 각각 `execve()`를 호출한다. — Bryant and O'Hallaron (2016), §8.4.
[^pipe-buffer-size-tuning]: 파이프의 최대 크기는 `fcntl(fd, F_SETPIPE_SZ, size)`로 조정할 수 있으며, 비특권 사용자의 한도는 `/proc/sys/fs/pipe-max-size`(기본 1MB)로 제한된다. — `fs/pipe.c`; `pipe(7)` 맨 페이지.
[^signal-numbers-architecture-dependent]: 표준 시그널 번호는 아키텍처에 따라 다를 수 있다. 예를 들어 `SIGCHLD`는 대부분의 아키텍처에서 17이지만, SPARC에서는 20이다. 본문의 번호는 x86-64 리눅스 기준이다. — `include/uapi/asm-generic/signal.h`.
[^signal-handling-at-syscall-return]: 시그널 처리가 시스템 호출 복귀 시점에 이루어지는 이유는, 그 시점이 프로세스의 레지스터 상태가 완전하게 보존되어 있고 사용자 스택에 시그널 프레임을 안전하게 구성할 수 있는 유일한 시점이기 때문이다. 커널은 `pt_regs`의 `RIP`를 시그널 핸들러 주소로 교체하고, 원래의 레지스터 상태를 `ucontext`에 저장하여 핸들러 복귀 시 `sigreturn()`으로 복원한다. — `arch/x86/kernel/signal.c`.
[^sysv-shm-tmpfs-backend]: System V 공유 메모리가 `tmpfs` 파일로 백업된다는 것은, `shmget()` + `shmat()`가 내부적으로 "커널이 이름 없는 tmpfs 파일에 `mmap()`하는 것"과 동일하다는 의미다. System V API는 이 위에 ID 기반 이름 체계와 권한 검사를 얹은 래퍼인 셈이다. — `ipc/shm.c:828`, `do_shmat()`.
[^msgrcv-message-type-filtering]: `msgrcv()`의 `msgtyp` 인자에 따른 필터링: `msgtyp == 0`이면 큐의 첫 번째 메시지를, `msgtyp > 0`이면 해당 유형의 첫 번째 메시지를, `msgtyp < 0`이면 유형이 `|msgtyp|` 이하인 메시지 중 가장 작은 유형의 것을 꺼낸다. — `ipc/msg.c`; `msgrcv(2)` 맨 페이지.
[^posix-mq-rbtree-rightmost-cache]: POSIX 메시지 큐의 레드-블랙 트리 구현에서 `msg_tree_rightmost` 포인터가 최고 우선순위 노드를 캐싱하므로, 실제 디큐 연산은 트리 탐색 없이 상수 시간에 완료된다. — `ipc/mqueue.c:133`.
[^scm-rights-fd-passing-mechanism]: `SCM_RIGHTS`를 통한 파일 서술자 전달은 커널이 송신 측의 `struct file` 포인터를 `skb`(소켓 버퍼)에 부착하고, 수신 측에서 해당 `struct file`을 새 파일 서술자 번호로 설치하는 방식으로 동작한다. 순환 참조(A가 B에게 유닉스 소켓 fd를 보내고, B가 다시 A에게 보내는 경우)를 탐지하기 위한 가비지 컬렉터가 `net/unix/garbage.c`에 구현되어 있다.
[^cas-atomic-instruction]: CAS(Compare-And-Swap)는 메모리 위치의 값이 기대값과 같을 때만 새 값으로 교체하는 원자적 연산이다. x86-64에서는 `LOCK CMPXCHG` 명령어, AArch64에서는 `LDXR`/`STXR`(또는 LSE의 `CAS`) 명령어로 구현된다.
[^ipc-namespace-creation]: IPC 네임스페이스의 생성은 `clone(CLONE_NEWIPC)` 또는 `unshare(CLONE_NEWIPC)` 시스템 호출로 이루어진다. 각 네임스페이스는 자체적인 `struct ipc_ids` 배열(공유 메모리, 세마포어, 메시지 큐 각각)과 `mqueuefs` 마운트를 가진다. — `include/linux/ipc_namespace.h:31`.
## 출처
- Silberschatz, A., Galvin, P.B. and Gagne, G. (2018) *Operating System Concepts*. 10th edn. Hoboken: Wiley.
- Kerrisk, M. (2010) *The Linux Programming Interface*. San Francisco: No Starch Press.
- Love, R. (2010) *Linux Kernel Development*. 3rd edn. Upper Saddle River: Addison-Wesley.
- Bovet, D.P. and Cesati, M. (2005) *Understanding the Linux Kernel*. 3rd edn. Sebastopol: O'Reilly.
- Bryant, R.E. and O'Hallaron, D.R. (2016) *Computer Systems: A Programmer's Perspective*. 3rd edn. Boston: Pearson.
- Stevens, W.R. and Rago, S.A. (2013) *Advanced Programming in the UNIX Environment*. 3rd edn. Upper Saddle River: Addison-Wesley.
- Linux kernel source v6.19 — `fs/pipe.c`, `kernel/signal.c`, `ipc/shm.c`, `ipc/msg.c`, `ipc/mqueue.c`, `net/unix/af_unix.c`, `kernel/futex/`.