IPC (Inter-Process Communication)
Linux 커널의 전통적 프로세스(Process) 간 통신(IPC)을 데이터 전달, 동기화 관점으로 체계화해 설명합니다. Pipe/FIFO 데이터 경로, 시그널(Signal) 처리, System V와 POSIX IPC의 자원 관리 모델, futex 기반 사용자 동기화, IPC 모범 사례와 디버깅(Debugging) 절차까지 심층 분석합니다.
핵심 요약
- Pipe — 단방향 바이트 스트림. 부모-자식 프로세스 간 가장 간단한 통신 수단입니다.
- 공유 메모리 — 가장 빠른 IPC. 여러 프로세스가 같은 물리 페이지(Page)를 매핑(Mapping)하여 데이터를 공유합니다.
- 메시지 큐 — 구조화된 메시지를 큐에 넣고 꺼내는 방식. System V와 POSIX 두 가지 API가 있습니다.
- Unix Domain Socket — 같은 호스트 내 프로세스 간 양방향 통신. 네트워크 소켓(Socket)과 동일한 API를 사용합니다.
- eventfd / signalfd — 이벤트 통지를 위한 경량 파일 디스크립터(File Descriptor) 기반 메커니즘입니다.
단계별 이해
- 파이프 체험 —
ls | grep txt처럼 셸의|가 바로 파이프입니다. 왼쪽 프로세스의 stdout이 오른쪽의 stdin으로 연결됩니다.C에서는
pipe()시스템 콜(System Call)로 생성합니다. - 공유 메모리 이해 —
shmget()/shmat()또는mmap(MAP_SHARED)로 생성합니다.동기화(세마포어, 뮤텍스(Mutex))가 없으면 데이터 경쟁이 발생할 수 있습니다.
- 소켓 통신 — Unix Domain Socket은 파일 경로를 주소로 사용하며,
AF_UNIX로 생성합니다.Docker, systemd, X11 등 많은 시스템 데몬이 UDS를 사용합니다.
- 선택 기준 — 단순 데이터 전달은 파이프, 대용량 공유는 공유 메모리, 구조화 메시지는 메시지 큐, 네트워크 호환 필요 시 소켓을 선택합니다.
ipcs명령어로 시스템의 현재 IPC 리소스를 확인할 수 있습니다.
IPC 개요와 분류
Linux 커널은 프로세스 간 데이터 교환, 동기화, 이벤트 통지를 위해 다양한 IPC 메커니즘을 제공합니다. 각 메커니즘은 서로 다른 사용 사례에 최적화되어 있으며, 커널 내부에서 별도의 서브시스템으로 구현됩니다.
IPC 메커니즘 분류
| 범주 | 메커니즘 | 데이터 방향 | 핵심 특징 |
|---|---|---|---|
| 바이트 스트림 | Pipe, FIFO | 단방향 | fd 기반, 부모-자식 또는 이름 기반 통신 |
| 시그널 | Signal, signalfd | 단방향 (통지) | 비동기 이벤트 전달, 제한된 정보량 |
| 메시지 전달 | SysV MQ, POSIX MQ | 양방향 | 메시지 경계 보존, 우선순위(Priority) 지원 |
| 공유 메모리 | SysV SHM, POSIX SHM, mmap | 양방향 | 최고 성능, 별도 동기화 필요 |
| 동기화 | SysV Sem, POSIX Sem, Futex | - | 프로세스 간 잠금(Lock)/동기화 |
| 이벤트/fd 기반 | eventfd, signalfd, timerfd | 단방향 | epoll 통합, 이벤트 루프(Event Loop) 친화적 |
| 소켓 | Unix Domain Socket, Netlink | 양방향 | 네트워크 API 활용, 유연한 프로토콜 |
IPC Namespace
Linux 커널은 ipc_namespace를 통해 IPC 자원을 네임스페이스별로 격리합니다. System V IPC 객체(메시지 큐, 공유 메모리, 세마포어)는 모두 이 네임스페이스(Namespace)에 의해 격리되며, 컨테이너(Container) 환경에서 IPC 자원의 독립성을 보장합니다.
/* include/linux/ipc_namespace.h */
struct ipc_namespace {
struct ipc_ids ids[3]; /* IPC_SEM, IPC_MSG, IPC_SHM */
int sem_ctls[4]; /* SEMMSL, SEMMNS, SEMOPM, SEMMNI */
int msg_ctlmax; /* MSGMAX */
int msg_ctlmnb; /* MSGMNB */
int msg_ctlmni; /* MSGMNI */
size_t shm_ctlmax; /* SHMMAX */
size_t shm_ctlall; /* SHMALL */
int shm_ctlmni; /* SHMMNI */
...
};
/* IPC 식별자 관리 구조체 */
struct ipc_ids {
int in_use; /* 사용 중인 IPC 객체 수 */
unsigned short seq; /* 시퀀스 번호 */
struct rw_semaphore rwsem; /* 읽기/쓰기 세마포어 */
struct idr ipcs_idr; /* IDR 기반 ID 관리 */
int max_idx; /* 최대 인덱스 */
...
};
/* 모든 SysV IPC 객체의 공통 권한 구조체 */
struct kern_ipc_perm {
spinlock_t lock;
int id; /* IPC 식별자 */
key_t key; /* IPC 키 */
kuid_t uid, gid; /* 소유자 */
kuid_t cuid, cgid; /* 생성자 */
umode_t mode; /* 접근 모드 */
unsigned long seq; /* 시퀀스 번호 */
refcount_t refcount;
...
};
코드 설명
-
ipc_namespace
컨테이너(Container) 격리의 핵심 구조체입니다. 각 IPC 네임스페이스는 독립적인
ids[3]배열을 가지며, 인덱스 0/1/2가 각각 세마포어/메시지 큐/공유 메모리에 대응합니다.sem_ctls,msg_ctl*,shm_ctl*필드는/proc/sys/kernel/아래 sysctl 한계값의 네임스페이스별 복사본으로, Docker나 LXC 같은 컨테이너 런타임이 IPC 자원 한도를 독립적으로 설정할 수 있게 합니다. (include/linux/ipc_namespace.h) -
ipc_ids
IPC 객체의 ID 할당과 조회를 관리합니다.
ipcs_idr은 기존 배열 기반 ID 관리를 대체한 IDR(ID Radix tree) 구조로,O(log n)탐색을 제공합니다.rwsem은ipcget()(쓰기)과ipc_obtain_object_check()(읽기) 사이의 동시성을 제어하며,seq는 ID 재사용 시 stale 핸들을 탐지하기 위한 시퀀스 번호입니다. (ipc/util.h) -
kern_ipc_perm
모든 System V IPC 객체(
msg_queue,shmid_kernel,sem_array)의 첫 번째 멤버로 임베딩되어, 다형적 접근을 가능하게 하는 공통 헤더입니다.key는ftok()가 생성하는 사용자 식별자이고,id는 커널이 인덱스+시퀀스를 조합하여 반환하는 실제 핸들입니다.uid/gid와cuid/cgid를 분리하여 소유권 이전 후에도 원래 생성자를 추적할 수 있으며,mode는 파일 퍼미션과 동일한 9비트 접근 제어를 제공합니다. (include/linux/ipc.h)
IPC 키와 ID: System V IPC에서 key_t는 ftok()로 생성하는 전역 식별자이고, id는 커널이 반환하는 실제 IPC 객체 핸들입니다. ID는 인덱스와 시퀀스 번호로 구성되어 재사용 시 stale 참조를 감지합니다.
Pipes & FIFOs
Pipe는 Linux에서 가장 기본적인 IPC 메커니즘으로, 두 개의 파일 디스크립터(읽기/쓰기)를 통해 단방향 바이트 스트림을 전달합니다. 커널 내부에서 pipe는 pipefs 가상 파일시스템(VFS) 위의 특수 inode로 구현됩니다.
Pipe 내부 구조
Pipe의 핵심 자료구조는 struct pipe_inode_info이며, 환형 버퍼(Buffer)로 구현됩니다. 기본 버퍼 크기는 16개 pipe_buffer 슬롯(총 64KB)이며, F_SETPIPE_SZ로 최대 1MB까지 조절 가능합니다.
/* include/linux/pipe_fs_i.h */
struct pipe_inode_info {
struct mutex mutex; /* pipe 잠금 */
wait_queue_head_t rd_wait; /* 읽기 대기 큐 */
wait_queue_head_t wr_wait; /* 쓰기 대기 큐 */
unsigned int head; /* 쓰기 위치 (생산자) */
unsigned int tail; /* 읽기 위치 (소비자) */
unsigned int max_usage; /* 최대 슬롯 수 */
unsigned int ring_size; /* 할당된 링 크기 */
unsigned int readers; /* 읽기 참조 카운트 */
unsigned int writers; /* 쓰기 참조 카운트 */
struct pipe_buffer *bufs; /* pipe_buffer 배열 */
...
};
struct pipe_buffer {
struct page *page; /* 데이터가 담긴 페이지 */
unsigned int offset; /* 페이지 내 오프셋 */
unsigned int len; /* 데이터 길이 */
const struct pipe_buf_operations *ops;
unsigned int flags;
};
pipe2() 시스템 콜
pipe2()는 파이프를 생성하는 시스템 콜로, 커널 내부에서 do_pipe2()를 통해 처리됩니다. O_CLOEXEC과 O_NONBLOCK 플래그를 지원합니다.
/* fs/pipe.c - do_pipe2() 핵심 흐름 (간략화) */
static int do_pipe2(int __user *fildes, int flags)
{
struct file *files[2];
int fd[2];
int error;
/* 1. pipe inode + pipe_inode_info 할당 */
error = __do_pipe_flags(fd, files, flags);
if (!error) {
/* 2. fd[0] = 읽기, fd[1] = 쓰기를 유저에 복사 */
if (copy_to_user(fildes, fd, sizeof(fd))) {
fput(files[0]);
fput(files[1]);
put_unused_fd(fd[0]);
put_unused_fd(fd[1]);
error = -EFAULT;
} else {
fd_install(fd[0], files[0]);
fd_install(fd[1], files[1]);
}
}
return error;
}
splice / tee / vmsplice
Linux는 파이프와 파일 사이에서 데이터를 복사 없이(zero-copy) 이동할 수 있는 시스템 콜을 제공합니다. 이들은 pipe_buffer의 페이지 참조를 직접 전달하여 불필요한 메모리 복사를 제거합니다.
| 시스템 콜 | 동작 | 용도 |
|---|---|---|
splice() |
fd ↔ pipe 간 zero-copy 이동 | 파일→파이프→소켓 (sendfile 대체) |
tee() |
pipe → pipe 간 zero-copy 복제 | 파이프 데이터 분기 (tee 명령어) |
vmsplice() |
유저 메모리 → pipe zero-copy | 유저 버퍼를 파이프에 직접 매핑 |
Named Pipes (FIFO)
FIFO(Named Pipe)는 파일시스템(Filesystem)에 이름을 가지는 파이프로, mkfifo() 또는 mknod()로 생성합니다. 관계없는 프로세스 간에도 파이프 통신이 가능하며, 내부적으로는 anonymous pipe와 동일한 pipe_inode_info 구조를 사용합니다.
/* FIFO 생성과 사용 (유저 공간) */
mkfifo("/tmp/myfifo", 0666);
/* Writer 프로세스 */
int wfd = open("/tmp/myfifo", O_WRONLY);
write(wfd, buf, len);
/* Reader 프로세스 */
int rfd = open("/tmp/myfifo", O_RDONLY);
read(rfd, buf, len);
Pipe 크기 제한: /proc/sys/fs/pipe-max-size는 비특권 사용자가 설정할 수 있는 최대 파이프 크기(기본 1MB)를 제어합니다. /proc/sys/fs/pipe-user-pages-soft와 pipe-user-pages-hard는 사용자별 총 파이프 버퍼 페이지 수를 제한합니다.
Signals
시그널은 프로세스에 비동기적으로 이벤트를 통지하는 가장 오래된 IPC 메커니즘입니다. 커널은 시그널 전달 시 대상 프로세스의 task_struct에 시그널 정보를 큐잉하고, 해당 프로세스가 유저 모드로 복귀할 때 시그널 핸들러(Handler)를 실행합니다.
표준 시그널 목록
리눅스는 31개의 표준 시그널(1~31)을 정의합니다. 각 시그널의 기본 동작(default action)은 다섯 가지 중 하나입니다:
| 기본 동작 | 설명 |
|---|---|
| Term | 프로세스 종료 |
| Core | 코어 덤프(Core Dump) 생성 후 종료 |
| Stop | 프로세스 중지 (suspended) |
| Cont | 중지된 프로세스 재개 |
| Ign | 시그널 무시 |
| 번호 | 이름 | 기본 동작 | 설명 |
|---|---|---|---|
| 1 | SIGHUP | Term | 제어 터미널 hangup 또는 제어 프로세스 종료 |
| 2 | SIGINT | Term | 키보드 인터럽트 (Ctrl+C) |
| 3 | SIGQUIT | Core | 키보드 종료 (Ctrl+\) |
| 4 | SIGILL | Core | 잘못된 명령어 (illegal instruction) |
| 5 | SIGTRAP | Core | 트레이스/브레이크포인트 트랩 |
| 6 | SIGABRT | Core | abort() 호출 |
| 7 | SIGBUS | Core | 버스(Bus) 오류 (잘못된 메모리 접근 정렬) |
| 8 | SIGFPE | Core | 부동소수점 예외 (0으로 나누기 포함) |
| 9 | SIGKILL | Term | 강제 종료 (캐치/무시 불가) |
| 10 | SIGUSR1 | Term | 사용자 정의 시그널 1 |
| 11 | SIGSEGV | Core | 잘못된 메모리 참조 (segmentation fault) |
| 12 | SIGUSR2 | Term | 사용자 정의 시그널 2 |
| 13 | SIGPIPE | Term | 읽는 쪽이 없는 파이프에 쓰기 |
| 14 | SIGALRM | Term | alarm() 타이머(Timer) 만료 |
| 15 | SIGTERM | Term | 정상 종료 요청 |
| 16 | SIGSTKFLT | Term | 코프로세서 스택 오류 (미사용) |
| 17 | SIGCHLD | Ign | 자식 프로세스 중지 또는 종료 |
| 18 | SIGCONT | Cont | 중지된 프로세스 재개 |
| 19 | SIGSTOP | Stop | 프로세스 중지 (캐치/무시 불가) |
| 20 | SIGTSTP | Stop | 터미널 중지 (Ctrl+Z) |
| 21 | SIGTTIN | Stop | 백그라운드 프로세스의 터미널 입력 |
| 22 | SIGTTOU | Stop | 백그라운드 프로세스의 터미널 출력 |
| 23 | SIGURG | Ign | 소켓의 긴급(OOB) 데이터 |
| 24 | SIGXCPU | Core | CPU 시간 제한 초과 |
| 25 | SIGXFSZ | Core | 파일 크기 제한 초과 |
| 26 | SIGVTALRM | Term | 가상 타이머 만료 |
| 27 | SIGPROF | Term | 프로파일링(Profiling) 타이머 만료 |
| 28 | SIGWINCH | Ign | 터미널 윈도우 크기 변경 |
| 29 | SIGIO | Term | I/O 가능 (async I/O) |
| 30 | SIGPWR | Term | 전원 장애 |
| 31 | SIGSYS | Core | 잘못된 시스템 콜 (seccomp 필터 위반 포함) |
SIGKILL(9)과 SIGSTOP(19)은 캐치, 무시, 마스크할 수 없는 유일한 시그널입니다. 커널은 sigaction()과 sigprocmask()에서 이 두 시그널에 대한 변경을 자동으로 무시합니다.
시그널 생성 경로
시그널은 네 가지 소스에서 생성됩니다:
| 생성 소스 | 메커니즘 | 대표 시그널 |
|---|---|---|
| 하드웨어 예외 | CPU 트랩/폴트 → 커널 예외 핸들러 → force_sig() |
SIGSEGV, SIGFPE, SIGBUS, SIGILL |
| 커널 소프트웨어 이벤트 | 커널 내부 조건 발생 → send_sig() |
SIGCHLD, SIGPIPE, SIGURG, SIGALRM |
| 유저 프로세스 | 시스템 콜 → 권한 검사 → __send_signal() |
kill(), tkill(), tgkill(), rt_sigqueueinfo() |
| 터미널 드라이버 | TTY line discipline → isig() → kill_pgrp() |
SIGINT(Ctrl+C), SIGQUIT(Ctrl+\), SIGTSTP(Ctrl+Z) |
/* 하드웨어 예외에 의한 시그널 생성 (arch/x86/kernel/traps.c 기반) */
/* 예: 0으로 나누기 → #DE 예외 → do_divide_error() */
static void do_error_trap(struct pt_regs *regs, long error_code,
char *str, unsigned long trapnr, int signr)
{
/* 유저 모드에서 발생한 경우 시그널 전송 */
if (user_mode(regs)) {
force_sig(signr); /* SIGFPE, SIGSEGV 등 */
return;
}
/* 커널 모드면 oops/panic */
die(str, regs, error_code);
}
/* 유저 프로세스에 의한 시그널 생성 (kernel/signal.c) */
/* kill(pid, sig) → sys_kill() → kill_something_info() */
SYSCALL_DEFINE2(kill, pid_t, pid, int, sig)
{
struct kernel_siginfo info;
prepare_kill_siginfo(sig, &info);
return kill_something_info(sig, &info, pid);
}
시그널 전달 내부 구조
/* kernel/signal.c - __send_signal() 핵심 (간략화) */
static int __send_signal(int sig, struct kernel_siginfo *info,
struct task_struct *t, enum pid_type type)
{
struct sigpending *pending;
struct sigqueue *q;
/* 프로세스/스레드 pending 큐 선택 */
pending = (type != PIDTYPE_PID) ?
&t->signal->shared_pending : &t->pending;
/* 이미 동일 시그널이 pending이면 (비RT) 무시 */
if (legacy_queue(pending, sig))
goto ret;
/* sigqueue 구조체 할당 */
q = __sigqueue_alloc(sig, t, GFP_ATOMIC, override_rlimit);
if (q) {
list_add_tail(&q->list, &pending->list);
copy_siginfo(&q->info, info);
}
/* 시그널 비트마스크 설정 */
sigaddset(&pending->signal, sig);
/* TIF_SIGPENDING 플래그 설정 → 유저 복귀 시 확인 */
complete_signal(sig, t, type);
ret:
return 0;
}
코드 설명
-
pending 큐 선택
PIDTYPE_PID이면 특정 스레드의t->pending에, 그 외(PIDTYPE_TGID등)이면 프로세스 그룹 공유 큐t->signal->shared_pending에 시그널을 넣습니다.kill(pid, sig)는 그룹 큐를,tgkill(tgid, tid, sig)는 개별 스레드 큐를 사용합니다. - legacy_queue() 검사 표준 시그널(1~31번)은 큐잉되지 않으므로, 동일 시그널이 이미 pending 비트마스크에 설정되어 있으면 중복 전달을 방지합니다. 반면 실시간 시그널(32~64번)은 이 검사를 건너뛰어 동일 시그널이 여러 번 큐잉될 수 있습니다.
-
__sigqueue_alloc()
sigqueue구조체를GFP_ATOMIC으로 할당합니다. 인터럽트 컨텍스트(하드웨어 예외 등)에서도 호출될 수 있으므로 슬립 불가능한 할당을 사용합니다. 사용자당 시그널 큐 제한(RLIMIT_SIGPENDING)을 초과하면 할당이 실패하고,siginfo없이 시그널 비트만 설정됩니다. -
complete_signal()
대상 스레드(또는 스레드 그룹 중 적합한 스레드)에
TIF_SIGPENDING플래그를 설정합니다. 이 플래그는 시스템 콜/인터럽트에서 유저 모드로 복귀하는 경로(exit_to_user_mode_loop())에서 확인되어 시그널 전달을 트리거합니다. (kernel/signal.c)
시그널 핸들러 실행 메커니즘
시그널 핸들러 실행은 커널이 유저 스택을 직접 조작하는 정교한 과정입니다. 프로세스가 시스템 콜이나 인터럽트에서 유저 모드로 복귀하기 직전에 do_signal()이 호출되며, pending 시그널이 있으면 handle_signal()을 통해 핸들러 실행을 설정합니다.
/* arch/x86/kernel/signal.c - 핸들러 실행 설정 (간략화) */
static int setup_rt_frame(struct ksignal *ksig,
struct pt_regs *regs)
{
struct rt_sigframe __user *frame;
/* 유저 스택에 rt_sigframe 공간 확보 */
frame = get_sigframe(ksig, regs, sizeof(*frame));
/* 현재 레지스터 상태를 ucontext에 저장 */
put_user_sigcontext(&frame->uc.uc_mcontext, regs);
/* siginfo 복사 */
copy_siginfo_to_user(&frame->info, &ksig->info);
/* 시그널 마스크 저장 */
__put_user(current->blocked, &frame->uc.uc_sigmask);
/* sigreturn 트램폴린 설정 (VDSO 사용) */
frame->pretcode = current->mm->context.vdso +
vdso_image_32.sym___kernel_rt_sigreturn;
/* 레지스터 조작: 핸들러가 실행되도록 설정 */
regs->ip = (unsigned long)ksig->ka.sa.sa_handler;
regs->sp = (unsigned long)frame;
regs->di = ksig->sig; /* 첫 번째 인자: signo */
regs->si = (unsigned long)&frame->info; /* SA_SIGINFO: siginfo */
regs->dx = (unsigned long)&frame->uc; /* SA_SIGINFO: ucontext */
return 0;
}
/* 핸들러 종료 후 트램폴린이 호출하는 시스템 콜 */
SYSCALL_DEFINE0(rt_sigreturn)
{
struct pt_regs *regs = current_pt_regs();
struct rt_sigframe __user *frame;
frame = (struct rt_sigframe __user *)(regs->sp - sizeof(long));
/* ucontext에서 레지스터와 시그널 마스크 복원 */
restore_sigcontext(regs, &frame->uc.uc_mcontext);
set_current_blocked(&frame->uc.uc_sigmask);
return regs->ax; /* 원래 시스템 콜 반환값 복원 */
}
sighand_struct
sighand_struct는 프로세스(스레드 그룹)가 공유하는 시그널 핸들러 테이블을 관리합니다. 64개 시그널 각각에 대한 k_sigaction을 저장합니다.
/* include/linux/sched/signal.h */
struct sighand_struct {
refcount_t count; /* 참조 카운트 */
struct k_sigaction action[_NSIG]; /* 64개 시그널 핸들러 */
spinlock_t siglock; /* 시그널 처리 잠금 */
wait_queue_head_t signalfd_wqh; /* signalfd 대기 큐 */
};
struct k_sigaction {
struct sigaction sa;
/* sa.sa_handler: SIG_DFL, SIG_IGN, 또는 유저 핸들러 */
/* sa.sa_flags: SA_RESTART, SA_SIGINFO, SA_NOCLDSTOP, ... */
/* sa.sa_mask: 핸들러 실행 중 차단할 시그널 마스크 */
};
시그널 마스크
각 스레드(Thread)는 task_struct.blocked에 시그널 마스크(sigset_t)를 가지며, sigprocmask()로 제어합니다. 마스크된 시그널은 pending 상태로 유지되다가 마스크가 해제되면 전달됩니다. SIGKILL과 SIGSTOP은 마스크할 수 없습니다.
대체 시그널 스택 (sigaltstack)
시그널 핸들러는 기본적으로 현재 프로세스의 유저 스택에서 실행됩니다. 문제는 스택 오버플로(Stack Overflow)가 발생하면 SIGSEGV가 생성되는데, 핸들러도 같은 스택에서 실행하려 하면 또 다시 스택 오버플로가 발생하여 핸들러 자체를 실행할 수 없는 것입니다. sigaltstack()은 이 문제를 해결하기 위해 별도의 시그널 전용 스택을 설정합니다.
/* 유저 공간: 대체 시그널 스택 설정 */
stack_t ss;
ss.ss_sp = malloc(SIGSTKSZ); /* 시그널 스택 메모리 할당 */
ss.ss_size = SIGSTKSZ; /* 기본 크기 (보통 8192) */
ss.ss_flags = 0;
sigaltstack(&ss, NULL);
/* SA_ONSTACK 플래그로 핸들러 등록 */
struct sigaction sa;
sa.sa_handler = segv_handler;
sa.sa_flags = SA_ONSTACK; /* 대체 스택에서 실행 */
sigaction(SIGSEGV, &sa, NULL);
/* kernel/signal.c - get_sigframe()에서 대체 스택 선택 로직 (간략화) */
static unsigned long get_sigframe(struct ksignal *ksig,
struct pt_regs *regs,
size_t frame_size)
{
unsigned long sp = regs->sp;
/* SA_ONSTACK 설정되었고, 현재 대체 스택 위가 아닌 경우 */
if (ksig->ka.sa.sa_flags & SA_ONSTACK) {
if (!on_sig_stack(sp) &&
!(current->sas_ss_flags & SS_DISABLE))
sp = current->sas_ss_sp + current->sas_ss_size;
}
sp -= frame_size;
sp = round_down(sp, 16); /* 16바이트 정렬 */
return sp;
}
ss_flags 값: SS_DISABLE는 대체 스택을 비활성화하고, SS_ONSTACK은 현재 대체 스택 위에서 실행 중임을 나타냅니다 (읽기 전용(Read-Only)). SS_AUTODISARM(Linux 4.7+)은 핸들러 진입 시 자동으로 대체 스택을 해제하여 중첩 시그널 시 스택 충돌을 방지합니다.
fork/exec 시그널 상속
fork()와 exec()는 시그널 관련 속성을 각각 다르게 처리합니다. 이 차이를 이해하는 것은 데몬 프로세스나 멀티프로세스 프로그램 설계에 중요합니다.
| 시그널 속성 | fork() |
exec() |
|---|---|---|
| 시그널 핸들러 | 상속 (부모와 동일) | 모두 SIG_DFL로 리셋 |
시그널 마스크 (blocked) |
상속 | 유지 |
| Pending 시그널 | 클리어 (자식은 비어있음) | 유지 |
| SIG_IGN 설정 | 상속 | 유지 (SIG_IGN은 리셋 안 됨) |
| 대체 시그널 스택 | 상속 | 비활성화 |
SA_NOCLDWAIT 등 플래그 |
상속 | SIG_DFL 리셋 시 함께 클리어 |
/* kernel/fork.c - copy_sighand() (fork 시 핸들러 테이블 복사) */
static int copy_sighand(unsigned long clone_flags,
struct task_struct *tsk)
{
if (clone_flags & CLONE_SIGHAND) {
/* 스레드 생성: sighand_struct 공유 (refcount++) */
refcount_inc(¤t->sighand->count);
return 0;
}
/* 프로세스 fork: sighand_struct 복사 */
struct sighand_struct *sig = kmem_cache_alloc(sighand_cachep, GFP_KERNEL);
memcpy(sig->action, current->sighand->action, sizeof(sig->action));
tsk->sighand = sig;
return 0;
}
/* fs/exec.c - flush_signal_handlers() (exec 시 핸들러 리셋) */
void flush_signal_handlers(struct task_struct *t)
{
struct k_sigaction *ka = &t->sighand->action[0];
for (int i = _NSIG; i != 0; i--) {
/* SIG_IGN은 유지, 나머지 핸들러는 SIG_DFL로 */
if (ka->sa.sa_handler != SIG_IGN)
ka->sa.sa_handler = SIG_DFL;
ka->sa.sa_flags = 0;
sigemptyset(&ka->sa.sa_mask);
ka++;
}
}
멀티스레드 시그널 전달
POSIX 스레드 모델에서 시그널 전달은 프로세스 지향(process-directed)과 스레드 지향(thread-directed)으로 나뉩니다. 커널은 이 두 종류를 shared_pending과 pending 큐로 구분합니다.
| 구분 | 프로세스 지향 시그널 | 스레드 지향 시그널 |
|---|---|---|
| 큐 | signal->shared_pending |
task_struct->pending |
| 발생 API | kill(), sigqueue() |
tgkill(), tkill(), pthread_kill() |
| 하드웨어 예외 | — | 예외 발생 스레드에 직접 전달 |
| 전달 대상 | 마스크 안 한 임의의 스레드 | 지정된 특정 스레드 |
/* kernel/signal.c - complete_signal() (간략화)
* 프로세스 지향 시그널의 대상 스레드 선택 알고리즘 */
static void complete_signal(int sig, struct task_struct *p,
enum pid_type type)
{
struct task_struct *t, *signal_target;
/* 1단계: 메인 스레드가 마스크 안 했으면 메인 스레드 선택 */
signal_target = p;
if (wants_signal(sig, p))
goto found;
/* 2단계: 마스크 안 한 스레드를 순회하여 찾기 */
t = p;
while_each_thread(p, t) {
if (wants_signal(sig, t)) {
signal_target = t;
goto found;
}
}
return; /* 모든 스레드가 마스크 → pending으로 유지 */
found:
/* TIF_SIGPENDING 설정하고 필요 시 깨움 */
signal_wake_up(signal_target, sig == SIGKILL);
}
/* wants_signal(): 해당 스레드가 시그널을 받을 수 있는지 확인 */
static bool wants_signal(int sig, struct task_struct *p)
{
if (sigismember(&p->blocked, sig))
return false; /* 마스크됨 */
if (p->flags & PF_EXITING)
return false; /* 종료 중 */
if (task_is_stopped_or_traced(p))
return sig == SIGKILL; /* 중지 상태면 SIGKILL만 */
return true;
}
멀티스레드 시그널 처리 패턴: 일반적으로 모든 스레드에서 pthread_sigmask()로 시그널을 블록하고, 전담 스레드에서 sigwait() 또는 signalfd()로 동기적으로 처리하는 것이 가장 안전합니다. 이렇게 하면 비동기 시그널 핸들러의 복잡한 안전성 문제를 피할 수 있습니다.
실시간(Real-time) 시그널
표준 시그널(1~31)은 pending 비트마스크만 설정하므로 동일 시그널이 여러 번 발생해도 한 번만 전달됩니다. 실시간 시그널(SIGRTMIN~SIGRTMAX, 32~64)은 큐잉되어 발생 횟수만큼 전달되며, sigqueue()로 추가 데이터(sigval)를 전달할 수 있습니다.
| 속성 | 표준 시그널 (1~31) | 실시간 시그널 (32~64) |
|---|---|---|
| 큐잉 | 안 됨 (비트마스크만) | 됨 (발생 횟수만큼) |
| 순서 보장(Ordering) | 보장 안 됨 | 번호 순서 보장 |
| 추가 데이터 | 제한적 (siginfo) | sigval (int 또는 void*) |
| 기본 동작 | 시그널별 다름 | 모두 프로세스 종료 |
signalfd
signalfd()는 시그널을 파일 디스크립터를 통해 동기적으로 수신할 수 있게 합니다. epoll/select/poll과 통합하여 이벤트 루프에서 시그널을 처리할 수 있습니다.
/* signalfd 사용 예 (유저 공간) */
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);
/* 시그널을 블록하고 fd로 수신 */
sigprocmask(SIG_BLOCK, &mask, NULL);
int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);
/* epoll에 등록 후 이벤트 루프에서 처리 */
struct signalfd_siginfo si;
read(sfd, &si, sizeof(si));
printf("signal %d from PID %d\\n", si.ssi_signo, si.ssi_pid);
pidfd_send_signal()
전통적인 kill() 시스템 콜은 PID로 대상 프로세스를 지정하는데, PID는 프로세스 종료 후 재사용될 수 있어 race condition이 발생할 수 있습니다. Linux 5.1에서 추가된 pidfd_send_signal()은 파일 디스크립터 기반으로 프로세스를 식별하여 이 문제를 해결합니다.
| 비교 | kill(pid, sig) |
pidfd_send_signal(pidfd, sig, ...) |
|---|---|---|
| 대상 식별 | PID (정수) | pidfd (파일 디스크립터) |
| PID 재사용 안전 | 안전하지 않음 | 안전 (fd가 특정 프로세스에 바인딩) |
| race condition | 검사~전송 사이 대상 변경 가능 | fd가 유효한 한 동일 프로세스 보장 |
| 추가 정보 | siginfo 제한적 | siginfo_t 직접 지정 가능 |
/* pidfd를 이용한 안전한 시그널 전송 (유저 공간) */
#include <sys/syscall.h>
#include <signal.h>
/* 1. 대상 프로세스의 pidfd 획득 (Linux 5.3+) */
int pidfd = syscall(SYS_pidfd_open, target_pid, 0);
if (pidfd < 0) {
perror("pidfd_open");
return -1;
}
/* 이 시점에서 target_pid가 종료되고 PID가 재사용되더라도
* pidfd는 원래 프로세스를 가리킨다.
* 원래 프로세스가 이미 종료되었으면 ESRCH 반환 */
/* 2. pidfd를 통해 시그널 전송 */
int ret = syscall(SYS_pidfd_send_signal, pidfd, SIGTERM, NULL, 0);
if (ret < 0)
perror("pidfd_send_signal");
/* 3. pidfd는 pollable — waitid(P_PIDFD)와도 연동 가능 */
close(pidfd);
/* kernel/signal.c - pidfd_send_signal 구현 (간략화) */
SYSCALL_DEFINE4(pidfd_send_signal, int, pidfd, int, sig,
siginfo_t __user *, info, unsigned int, flags)
{
struct pid *pid;
struct task_struct *task;
/* pidfd에서 struct pid 획득 */
pid = pidfd_to_pid(pidfd);
/* pid에서 task_struct 조회 (RCU 보호) */
task = pid_task(pid, PIDTYPE_PID);
if (!task)
return -ESRCH; /* 프로세스 이미 종료 */
/* 권한 검사 후 시그널 전송 */
return group_send_sig_info(sig, &kinfo, task, PIDTYPE_TGID);
}
시그널 핸들러 안전성 (Async-Signal Safety): 시그널 핸들러는 프로그램 실행의 임의 지점에서 비동기적으로 호출됩니다. 따라서 핸들러 내에서는 async-signal-safe 함수만 사용해야 합니다. 대표적 안전 함수: write(), _exit(), signal(), sigaction(), read(), open(), close(), kill(). malloc(), printf(), pthread_mutex_lock() 등 대부분의 라이브러리 함수는 안전하지 않습니다. 핸들러에서 전역 변수에 접근할 때는 volatile sig_atomic_t 타입을 사용해야 합니다.
System V IPC
System V IPC는 메시지 큐, 공유 메모리, 세마포어의 세 가지 IPC 메커니즘을 제공하는 전통적인 UNIX IPC 인터페이스입니다. 커널의 ipc/ 디렉토리에 구현되어 있으며, 공통 인프라(ipc_ids, kern_ipc_perm)를 공유합니다.
SysV IPC 공통 인프라
/* ipc/util.h - IPC 연산 테이블 */
struct ipc_ops {
int (*getnew)(struct ipc_namespace *, struct ipc_params *);
int (*associate)(struct kern_ipc_perm *, int);
int (*more_checks)(struct kern_ipc_perm *, struct ipc_params *);
};
/* 공통 IPC 객체 조회/생성 흐름 */
/* xxxget() → ipcget() → ipcget_new() / ipcget_public()
→ ops->getnew() 또는 ops->associate() */
Message Queues (메시지 큐)
System V 메시지 큐는 프로세스 간에 메시지 경계가 보존되는 메시지를 주고받을 수 있게 합니다. 메시지는 타입 필드를 가지며, 수신자는 특정 타입의 메시지만 선택적으로 수신할 수 있습니다.
/* ipc/msg.c - 핵심 구조체 */
struct msg_queue {
struct kern_ipc_perm q_perm; /* 공통 IPC 권한 */
time64_t q_stime; /* 마지막 msgsnd 시간 */
time64_t q_rtime; /* 마지막 msgrcv 시간 */
unsigned long q_cbytes; /* 큐 내 총 바이트 수 */
unsigned long q_qnum; /* 큐 내 메시지 수 */
unsigned long q_qbytes; /* 큐 최대 바이트 */
struct list_head q_messages; /* 메시지 연결 리스트 */
struct list_head q_receivers; /* 수신 대기 프로세스 */
struct list_head q_senders; /* 송신 대기 프로세스 */
};
struct msg_msg {
struct list_head m_list;
long m_type; /* 메시지 타입 */
size_t m_ts; /* 메시지 텍스트 크기 */
struct msg_msgseg *next; /* 큰 메시지용 세그먼트 */
void *security;
/* 이후: 메시지 데이터 (인라인) */
};
코드 설명
-
msg_queue
하나의 System V 메시지 큐를 나타내며, 첫 멤버
q_perm(kern_ipc_perm)을 통해 공통 IPC 인프라에 등록됩니다.q_messages는 큐에 저장된msg_msg노드들의 이중 연결 리스트이고,q_receivers/q_senders는 각각msgrcv()/msgsnd()에서 블로킹된 프로세스의 대기 리스트입니다.q_cbytes가q_qbytes(기본값MSGMNB=16384)를 초과하면 송신자가 슬립합니다. (ipc/msg.c) -
msg_msg
개별 메시지를 나타내는 구조체로, 헤더 직후에 메시지 데이터가 인라인으로 저장됩니다. 한 페이지(4KB)에 담기지 않는 큰 메시지는
next포인터로msg_msgseg체인을 연결하여 분할 저장합니다.m_type필드가 메시지 선택적 수신의 핵심으로,msgrcv()의msgtyp인자가 양수이면 해당 타입만, 음수이면 절대값 이하의 최소 타입을 선택합니다.security는 LSM(SELinux 등)이 메시지별 보안 레이블을 부착하는 데 사용됩니다. (ipc/msg.c)
/* 유저 공간 사용 예 */
#include <sys/msg.h>
struct msgbuf {
long mtype;
char mtext[256];
};
/* 메시지 큐 생성/열기 */
key_t key = ftok("/tmp/mqfile", 65);
int msqid = msgget(key, 0666 | IPC_CREAT);
/* 메시지 전송 */
struct msgbuf msg = { .mtype = 1 };
strcpy(msg.mtext, "Hello IPC");
msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0);
/* 메시지 수신 (타입 1만) */
struct msgbuf rcv;
msgrcv(msqid, &rcv, sizeof(rcv.mtext), 1, 0);
msgsnd/msgrcv 커널 내부 흐름
메시지 큐 크기 제한: System V 메시지 큐는 세 가지 커널 파라미터로 제한됩니다.
| 파라미터 | 의미 | 기본값 | sysctl 경로 |
|---|---|---|---|
MSGMAX | 단일 메시지 최대 크기 (바이트) | 8,192 | kernel.msgmax |
MSGMNB | 큐 하나의 최대 총 바이트 | 16,384 | kernel.msgmnb |
MSGMNI | 시스템 전체 최대 큐 수 | 32,000 | kernel.msgmni |
큐가 MSGMNB에 도달하면 msgsnd()는 공간이 확보될 때까지 대기합니다 (IPC_NOWAIT 미설정 시). 대기 프로세스는 msg_queue.q_senders 리스트에 등록되며, msgrcv()가 메시지를 꺼낸 뒤 깨웁니다.
Shared Memory (공유 메모리)
System V 공유 메모리는 가장 빠른 IPC 메커니즘으로, 여러 프로세스가 동일한 물리 메모리(Physical Memory) 영역을 자신의 가상 주소 공간(Address Space)에 매핑하여 직접 접근합니다. 별도의 동기화 메커니즘(세마포어, futex 등)이 필요합니다.
/* ipc/shm.c - 핵심 구조체 */
struct shmid_kernel {
struct kern_ipc_perm shm_perm;
struct file *shm_file; /* shmem/hugetlb 파일 */
unsigned long shm_nattch; /* attach 카운트 */
size_t shm_segsz; /* 세그먼트 크기 */
time64_t shm_atim; /* 마지막 attach 시간 */
time64_t shm_dtim; /* 마지막 detach 시간 */
pid_t shm_cprid; /* 생성자 PID */
pid_t shm_lprid; /* 마지막 작업 PID */
...
};
코드 설명
-
shm_perm
kern_ipc_perm을 첫 멤버로 임베딩하여,ipc_obtain_object_check()등 공통 IPC 조회 경로에서 다형적으로 접근할 수 있습니다. -
shm_file
공유 메모리의 실제 백엔드(Backend)를 가리키는
struct file포인터입니다. 일반 공유 메모리는shmem_kernel_file_setup()으로 tmpfs 파일을 생성하고,SHM_HUGETLB플래그 사용 시 hugetlbfs 파일을 생성합니다.shmat()시 이 파일을do_mmap()으로 프로세스 주소 공간에 매핑합니다. (ipc/shm.c의newseg()→shmem_kernel_file_setup()) -
shm_nattch
현재 이 세그먼트를
shmat()한 프로세스 수입니다.shmctl(IPC_RMID)호출 시 즉시 삭제되지 않고SHM_DEST플래그만 설정되며,shm_nattch가 0이 되는 마지막shmdt()시점에 실제 메모리가 해제됩니다. -
shm_cprid / shm_lprid
각각 세그먼트 생성자와 마지막
shmat()/shmdt()/shmctl()수행자의 PID를 기록합니다.ipcs -pm명령으로 확인할 수 있어 디버깅 시 어떤 프로세스가 공유 메모리를 사용하는지 추적하는 데 유용합니다.
Semaphores (세마포어)
System V 세마포어는 프로세스 간 동기화를 위한 카운팅 세마포어 배열을 제공합니다. 하나의 세마포어 세트에 여러 개의 세마포어를 포함할 수 있으며, semop()으로 원자적(Atomic) 다중 연산을 수행합니다.
/* ipc/sem.c - 핵심 구조체 */
struct sem_array {
struct kern_ipc_perm sem_perm;
time64_t sem_ctime; /* 마지막 변경 시간 */
struct list_head pending_alter; /* 대기 중인 연산 */
struct list_head pending_const; /* 대기 중인 연산 (0 대기) */
struct sem *sems; /* 세마포어 배열 */
};
struct sem {
int semval; /* 현재 값 */
pid_t sempid; /* 마지막 연산 PID */
struct list_head pending_alter;
struct list_head pending_const;
time64_t sem_otime; /* 마지막 semop 시간 */
};
/* semop() 연산 구조체 */
struct sembuf {
unsigned short sem_num; /* 세마포어 인덱스 */
short sem_op; /* 연산 (+n, -n, 0) */
short sem_flg; /* IPC_NOWAIT, SEM_UNDO */
};
코드 설명
-
sem_array
하나의 세마포어 세트를 나타냅니다.
sems는semget()의nsems인자만큼 할당된struct sem배열 포인터입니다.pending_alter와pending_const는 세트 전체에 걸친 복합 연산(여러 세마포어를 원자적으로 조작)이 블로킹될 때 대기 큐 역할을 합니다. 커널 5.10+에서는 단일 세마포어 연산 시 세트 전체 잠금 대신 개별sem.lock만 획득하는 최적화가 적용되어 높은 동시성을 달성합니다. (ipc/sem.c) -
struct sem
개별 세마포어 인스턴스입니다.
semval이 카운터 값이며,pending_alter/pending_const는 이 특정 세마포어에서 대기 중인 연산 리스트입니다.sempid는 마지막으로 성공한semop()을 수행한 프로세스의 PID로,GETPID명령으로 조회 가능합니다. -
sembuf
semop()시스템 콜에 전달하는 연산 지시 구조체입니다.sem_op이 양수면 값 증가(V 연산), 음수면 값 감소(P 연산), 0이면 값이 0이 될 때까지 대기합니다.SEM_UNDO플래그는 프로세스가 비정상 종료 시 커널이 연산을 자동 복원하도록sem_undo리스트에 기록합니다. 이 undo 메커니즘은exit_sem()(ipc/sem.c)에서 프로세스 종료 시 처리됩니다.
세마포어 사용 예제
#include <sys/sem.h>
#include <sys/ipc.h>
#include <stdio.h>
#include <stdlib.h>
/* semctl()용 공용체 (일부 시스템에서 직접 정의 필요) */
union semun {
int val;
struct semid_ds *buf;
unsigned short *array;
};
/* 세마포어 P 연산 (wait/lock) */
void sem_wait(int semid)
{
struct sembuf sb = {
.sem_num = 0,
.sem_op = -1, /* 값 감소 (잠금) */
.sem_flg = SEM_UNDO /* 프로세스 종료 시 자동 복원 */
};
semop(semid, &sb, 1);
}
/* 세마포어 V 연산 (signal/unlock) */
void sem_signal(int semid)
{
struct sembuf sb = {
.sem_num = 0,
.sem_op = 1, /* 값 증가 (해제) */
.sem_flg = SEM_UNDO
};
semop(semid, &sb, 1);
}
int main(void)
{
key_t key = ftok("/tmp/semfile", 65);
/* 세마포어 세트 생성 (1개 세마포어) */
int semid = semget(key, 1, 0666 | IPC_CREAT);
/* 초기값 1로 설정 (바이너리 세마포어) */
union semun arg = { .val = 1 };
semctl(semid, 0, SETVAL, arg);
/* 임계 구역 진입 */
sem_wait(semid);
printf("임계 구역 실행 중...\n");
/* ... 공유 자원 접근 ... */
sem_signal(semid);
/* 정리: 세마포어 세트 삭제 */
semctl(semid, 0, IPC_RMID);
return 0;
}
semop() 커널 내부 흐름
SEM_UNDO: SEM_UNDO 플래그를 사용하면 프로세스가 비정상 종료해도 커널이 세마포어 값을 자동으로 원복합니다. task_struct의 sysvsem.undo_list에 undo 정보를 기록하며, exit_sem()에서 처리됩니다.
POSIX IPC
POSIX IPC는 System V IPC의 현대적 대안으로, 파일 디스크립터 기반의 일관된 API를 제공합니다. 이름 규칙(/name), 에러 처리, 권한 모델이 일반 파일 API와 유사하여 사용이 직관적입니다.
POSIX MQ / SHM / Sem
| 메커니즘 | API | 커널 구현 |
|---|---|---|
| Message Queue | mq_open(), mq_send(), mq_receive() |
mqueue 가상 파일시스템 (ipc/mqueue.c) |
| Shared Memory | shm_open(), mmap(), shm_unlink() |
tmpfs 기반 (/dev/shm) |
| Semaphore | sem_open(), sem_wait(), sem_post() |
futex 기반 (glibc 구현) |
POSIX Message Queue 커널 내부
POSIX 메시지 큐는 전용 가상 파일시스템인 mqueue(ipc/mqueue.c)를 통해 구현됩니다. 각 큐는 mqueue_inode_info 구조체로 관리되며, 메시지들은 우선순위(Priority)에 따라 정렬된 레드-블랙 트리(Red-Black Tree)로 저장됩니다. 이를 통해 mq_receive()는 항상 가장 높은 우선순위의 메시지를 먼저 반환합니다.
/* ipc/mqueue.c - 핵심 구조체 */
struct mqueue_inode_info {
spinlock_t lock;
struct rb_root msg_tree; /* RB-tree: 우선순위별 메시지 */
struct posix_msg_tree_node *node_cache;
struct mq_attr attr; /* mq_maxmsg, mq_msgsize 등 */
struct sigevent notify; /* mq_notify 설정 */
struct pid *notify_owner; /* 통지 대상 프로세스 */
struct user_namespace *notify_user_ns;
struct ucounts *ucounts; /* 사용자별 큐 카운트 */
unsigned long qsize; /* 큐 내 총 바이트 */
struct inode vfs_inode;
struct list_head e_wait_q[2]; /* [0]=수신대기, [1]=송신대기 */
};
/* 우선순위별 메시지 트리 노드 */
struct posix_msg_tree_node {
struct rb_node rb_node;
struct list_head msg_list; /* 동일 우선순위 메시지 리스트 */
int priority;
};
코드 설명
-
mqueue_inode_info
POSIX 메시지 큐의 커널 표현으로,
mqueue가상 파일시스템(보통/dev/mqueue에 마운트)의 inode에 임베딩됩니다. System V 메시지 큐와 달리 파일 디스크립터 기반이므로select()/poll()/epoll()과 통합 가능합니다. (ipc/mqueue.c) -
msg_tree (RB-tree)
System V 메시지 큐의 단순 연결 리스트와 달리, POSIX MQ는 레드-블랙 트리(Red-Black Tree)로 우선순위별 메시지를 관리합니다.
mq_receive()는 항상 가장 높은 우선순위의 메시지를O(log n)에 추출합니다. -
notify / notify_owner
mq_notify()로 등록한 비동기 통지 설정입니다. 빈 큐에 첫 메시지가 도착하면notify_owner프로세스에 시그널을 보내거나 새 스레드를 생성합니다. 한 큐에 하나의 통지만 등록 가능하며, 통지가 발생하면 자동으로 해제됩니다. -
e_wait_q[2]
인덱스 0은 수신 대기(
mq_receive()블로킹), 인덱스 1은 송신 대기(mq_send()블로킹) 프로세스의 대기 큐입니다. 큐가 꽉 차면 송신자가e_wait_q[1]에서 슬립하고, 비어 있으면 수신자가e_wait_q[0]에서 슬립합니다. -
posix_msg_tree_node
동일 우선순위를 가진 메시지들을
msg_list에 FIFO 순서로 연결하고,rb_node를 통해 RB-tree에 삽입됩니다. 같은 우선순위의 메시지가 모두 소비되면 해당 노드는node_cache에 캐싱되어 재할당 비용을 줄입니다.
mq_notify()는 큐에 메시지가 도착했을 때 비동기 알림을 받을 수 있게 합니다. 두 가지 주요 통지 방식이 있습니다:
| 통지 방식 | sigev_notify 값 | 동작 |
|---|---|---|
| 시그널 전달 | SIGEV_SIGNAL | 지정한 시그널(sigev_signo)을 프로세스에 전송 |
| 스레드 생성 | SIGEV_THREAD | 새 스레드를 생성하여 sigev_notify_function 실행 |
| 없음 | SIGEV_NONE | 통지 없음 (등록만) |
POSIX MQ 우선순위 예제
#include <mqueue.h>
#include <stdio.h>
#include <string.h>
int main(void)
{
struct mq_attr attr = {
.mq_maxmsg = 10,
.mq_msgsize = 256
};
/* 큐 생성 (이름은 반드시 '/'로 시작) */
mqd_t mq = mq_open("/test_queue", O_CREAT | O_RDWR, 0644, &attr);
/* 우선순위별 메시지 전송 (높은 숫자 = 높은 우선순위) */
mq_send(mq, "low priority", 13, 1); /* 우선순위 1 */
mq_send(mq, "high priority", 14, 10); /* 우선순위 10 */
mq_send(mq, "mid priority", 13, 5); /* 우선순위 5 */
/* 수신: 항상 가장 높은 우선순위부터 */
char buf[256];
unsigned int prio;
mq_receive(mq, buf, 256, &prio);
printf("1st: %s (prio=%u)\n", buf, prio); /* "high priority" (10) */
mq_receive(mq, buf, 256, &prio);
printf("2nd: %s (prio=%u)\n", buf, prio); /* "mid priority" (5) */
mq_close(mq);
mq_unlink("/test_queue");
return 0;
}
-lrt 링크 필요: gcc -o mq_example mq_example.c -lrt. POSIX MQ API는 librt(Real-Time library)에 구현되어 있습니다.
POSIX Shared Memory 커널 내부
POSIX 공유 메모리는 shm_open()으로 생성하며, 실제로는 tmpfs(/dev/shm) 위에 일반 파일을 여는 것과 동일합니다. glibc의 shm_open()은 내부적으로 /dev/shm/ 경로에 대해 open()을 호출합니다. 이후 ftruncate()로 크기를 설정하고 mmap()으로 주소 공간에 매핑합니다.
/* POSIX 공유 메모리 예제 */
#include <sys/mman.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#define SHM_SIZE 4096
int main(void)
{
/* 공유 메모리 객체 생성 (이름 '/'로 시작) */
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0644);
/* 크기 설정 */
ftruncate(fd, SHM_SIZE);
/* 주소 공간에 매핑 */
void *ptr = mmap(NULL, SHM_SIZE,
PROT_READ | PROT_WRITE,
MAP_SHARED, fd, 0);
close(fd); /* fd는 mmap 후 닫아도 됨 */
/* 공유 메모리에 데이터 기록 */
strcpy(ptr, "Hello from POSIX SHM");
printf("Written: %s\n", (char *)ptr);
/* 정리 */
munmap(ptr, SHM_SIZE);
shm_unlink("/my_shm");
return 0;
}
memfd_create() — 현대적 대안: Linux 3.17에서 추가된 memfd_create()는 파일시스템 경로 없이 익명 공유 메모리를 생성합니다. 반환된 fd를 Unix Domain Socket의 SCM_RIGHTS로 다른 프로세스에 전달할 수 있어, 이름 충돌이나 파일시스템 마운트 의존성이 없습니다. MFD_CLOEXEC, MFD_ALLOW_SEALING 플래그로 보안을 강화할 수 있습니다.
int fd = memfd_create("shared_data", MFD_CLOEXEC);
ftruncate(fd, 4096);
void *p = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
/* fd를 SCM_RIGHTS로 다른 프로세스에 전달 가능 */
POSIX Named Semaphore 내부 구현
POSIX Named Semaphore는 sem_open()으로 생성하며, glibc 내부에서 /dev/shm/sem.NAME 경로에 파일을 생성합니다. 파일 내용은 sem_t 구조체가 매핑되며, 실제 동기화는 커널의 futex 메커니즘을 통해 수행됩니다. 경합이 없는 경우(uncontended) 커널 진입 없이 원자적 연산만으로 처리됩니다.
/* glibc 내부 sem_t 구조 (간략화) - nptl/internaltypes.h */
struct new_sem {
unsigned int value; /* 세마포어 현재 값 (atomic) */
unsigned int nwaiters; /* 대기 중인 스레드 수 */
int private; /* FUTEX_PRIVATE_FLAG 여부 */
};
/*
* sem_wait() 흐름:
* 1. atomic_decrement(&value) — 성공(>0)이면 즉시 리턴 (Fast Path)
* 2. 실패(0)이면 → futex(FUTEX_WAIT, &value, 0) → 커널 슬립
*
* sem_post() 흐름:
* 1. atomic_increment(&value)
* 2. nwaiters > 0 이면 → futex(FUTEX_WAKE, &value, 1)
*/
sem_open()은 named 세마포어(파일 기반)를 생성하고, sem_init()은 unnamed 세마포어(메모리 기반)를 초기화합니다. unnamed 세마포어의 pshared=1 설정 시 공유 메모리에 배치하여 프로세스 간 공유가 가능합니다. 두 방식 모두 내부적으로 futex를 사용합니다.
SysV vs POSIX 비교
| 항목 | System V IPC | POSIX IPC |
|---|---|---|
| 식별 | 정수 키 (ftok()) |
이름 문자열 (/name) |
| API 스타일 | xxxget(), xxxctl(), xxxop() |
xxx_open(), xxx_close(), xxx_unlink() |
| fd 기반 | 아니오 (정수 ID) | 예 (MQ, SHM) |
| epoll 통합 | 불가 | MQ: mq_notify() + fd |
| 네임스페이스 격리 | ipc_namespace로 격리 |
SHM: mount_namespace, MQ: ipc_namespace |
| 자원 정리 | 명시적 삭제 필요 (xxxctl(IPC_RMID)) |
xxx_unlink() + 참조 카운트(Reference Count) 기반 |
Futex (Fast Userspace Mutex)
Futex는 유저 공간에서 빠르게 동기화를 수행하고, 경합(contention)이 발생할 때만 커널에 진입하는 하이브리드 동기화 메커니즘입니다. glibc의 pthread_mutex, pthread_cond, sem_wait() 등 거의 모든 유저 공간 동기화 프리미티브의 근간입니다.
Futex 동작 원리
futex() 시스템 콜
/* kernel/futex/waitwake.c - futex_wait() 핵심 (간략화) */
int futex_wait(u32 __user *uaddr, unsigned int flags,
u32 val, ktime_t *abs_time, u32 bitset)
{
struct futex_hash_bucket *hb;
struct futex_q q = futex_q_init;
/* 1. futex 키 계산 (공유/비공유) */
get_futex_key(uaddr, flags, &q.key);
/* 2. hash bucket 결정 및 잠금 */
hb = futex_q_lock(&q);
/* 3. 현재 값 검증 (값이 바뀌었으면 즉시 리턴) */
if (futex_get_value_locked(&uval, uaddr))
goto out;
if (uval != val) {
futex_q_unlock(hb);
return -EAGAIN;
}
/* 4. 대기 큐에 등록하고 슬립 */
futex_wait_queue(hb, &q, abs_time);
return 0;
}
코드 설명
-
get_futex_key()
유저 공간 주소
uaddr에서 futex 키를 계산합니다. 프로세스 간 공유 futex(FUTEX_SHARED)는 페이지의struct page+ 오프셋으로 키를 생성하고, 프로세스 내부 futex(FUTEX_PRIVATE)는mm_struct+ 가상 주소로 키를 생성합니다. 공유 키는 서로 다른 프로세스가 같은 물리 페이지를 매핑해도 동일한 해시 버킷에 매칭됩니다. (kernel/futex/core.c) -
futex_q_lock()
계산된 키로 글로벌 해시 테이블(
futex_queues[], 기본 256개 버킷)에서 대응하는futex_hash_bucket을 찾아spinlock을 획득합니다. 이 잠금 하에서 값 검증과 대기 큐 등록이 원자적으로 수행되어 lost wakeup 문제를 방지합니다. -
값 검증 (uval != val)
futex의 핵심 설계 원칙인 "조건부 슬립"입니다. 해시 버킷 잠금을 잡은 상태에서 유저 공간 값을 다시 읽어, 기대값(
val)과 다르면-EAGAIN을 반환합니다. 이로써FUTEX_WAKE가FUTEX_WAIT사이에 발생해도 잠자는 태스크가 영구히 깨어나지 못하는 경쟁 조건을 방지합니다. -
futex_wait_queue()
현재 태스크를 해시 버킷의 대기 리스트에 등록하고,
schedule()로 슬립합니다. 타임아웃(abs_time)이 지정된 경우hrtimer를 설정하여 시간 만료 시 자동으로 깨어납니다.FUTEX_WAKE호출자는 같은 키의 해시 버킷에서 대기자를 찾아wake_up_q()로 깨웁니다. (kernel/futex/waitwake.c)
PI-Futex (Priority Inheritance)
PI-Futex는 우선순위 역전(Priority Inversion) 문제를 해결하기 위해 FUTEX_LOCK_PI/FUTEX_UNLOCK_PI 연산을 제공합니다. 우선순위 역전은 실시간(Real-Time) 시스템에서 치명적인 문제로, 높은 우선순위의 태스크가 낮은 우선순위 태스크가 보유한 lock을 기다리는 동안, 중간 우선순위의 태스크가 실행되어 전체 시스템의 응답 시간이 무한히 늘어나는 현상입니다.
PI-Futex는 커널 내부의 rt_mutex 인프라를 활용합니다. FUTEX_LOCK_PI 호출 시 유저 공간 futex 값의 하위 비트에 소유자 TID가 기록되며, 경합 발생 시 커널이 소유자의 우선순위를 자동으로 조정합니다.
/* kernel/futex/pi.c - PI futex 핵심 흐름 (간략화) */
int futex_lock_pi(u32 __user *uaddr, unsigned int flags,
ktime_t *time, int trylock)
{
struct rt_mutex_waiter rt_waiter;
struct futex_pi_state *pi_state;
struct task_struct *exiting = NULL;
/* 1. futex 키 계산, hash bucket 잠금 */
get_futex_key(uaddr, flags, &q.key);
hb = futex_q_lock(&q);
/* 2. 유저 공간 값에서 소유자 TID 확인 */
get_futex_value_locked(&uval, uaddr);
/* 3. 소유자가 없으면 CAS로 즉시 획득 (fast path) */
if (!(uval & FUTEX_TID_MASK)) {
futex_atomic_cmpxchg_inatomic(uaddr, uval,
uval | current->pid);
return 0;
}
/* 4. 소유자 task_struct 조회, PI state 연결 */
attach_to_pi_state(uval, pi_state, &exiting);
/* 5. rt_mutex를 통한 PI 대기 (소유자 우선순위 부스트) */
rt_mutex_slowlock_block(&pi_state->pi_mutex,
&rt_waiter, NULL);
/* 깨어나면 lock 획득 완료, 유저값에 자신의 TID 기록 */
return 0;
}
/* PI-Futex 사용 예 (glibc pthread_mutex_init에서 자동 사용) */
#include <pthread.h>
pthread_mutex_t pi_mutex;
pthread_mutexattr_t attr;
/* PI 프로토콜 설정 */
pthread_mutexattr_init(&attr);
pthread_mutexattr_setprotocol(&attr, PTHREAD_PRIO_INHERIT);
pthread_mutex_init(&pi_mutex, &attr);
/* 이후 pthread_mutex_lock()은 내부적으로 FUTEX_LOCK_PI 사용 */
pthread_mutex_lock(&pi_mutex);
/* 임계 구역 */
pthread_mutex_unlock(&pi_mutex);
Robust Futex
Robust futex는 lock을 보유한 프로세스가 비정상 종료(crash)했을 때 deadlock을 방지하는 메커니즘입니다. 일반 futex에서는 lock 소유자가 죽으면 다른 대기자들이 영원히 깨어나지 못하는 문제가 발생합니다. Robust futex는 이 문제를 커널이 자동으로 감지하고 복구합니다.
동작 원리는 다음과 같습니다:
- 프로세스가
set_robust_list()로 robust futex 리스트의 헤드를 커널에 등록합니다. - glibc의
pthread_mutex_lock()이 lock 획득 시 해당 futex를 robust 리스트에 연결합니다. - 프로세스가 비정상 종료하면 커널이
exit_robust_list()를 호출합니다. - 리스트의 각 futex에
FUTEX_OWNER_DIED비트(bit 30)를 설정합니다. - 해당 futex에서 대기 중인 프로세스가 깨어나
EOWNERDEAD를 받고 복구 절차를 수행합니다.
/* Robust futex 사용 예 */
#include <pthread.h>
#include <stdio.h>
#include <errno.h>
/* 공유 메모리에 배치된 mutex (프로세스 간 공유) */
pthread_mutex_t *shared_mutex; /* mmap으로 공유 메모리에 매핑 */
void init_robust_mutex(void)
{
pthread_mutexattr_t attr;
pthread_mutexattr_init(&attr);
/* robust 속성 설정 */
pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST);
/* 프로세스 간 공유 설정 */
pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED);
pthread_mutex_init(shared_mutex, &attr);
}
void safe_lock(void)
{
int ret = pthread_mutex_lock(shared_mutex);
if (ret == EOWNERDEAD) {
/* 이전 소유자가 죽었음 → 공유 상태 복구 필요 */
printf("이전 소유자 사망 감지, 상태 복구 중...\n");
/* ... 공유 데이터 일관성 복구 ... */
/* mutex를 일관된 상태로 표시 */
pthread_mutex_consistent(shared_mutex);
} else if (ret == ENOTRECOVERABLE) {
/* 복구 불가능 상태 (consistent 호출 안 하고 unlock한 경우) */
fprintf(stderr, "mutex 복구 불가\n");
return;
}
/* 정상 임계 구역 진입 */
/* ... */
pthread_mutex_unlock(shared_mutex);
}
/* 커널 내부: exit_robust_list() 핵심 (간략화) */
/* kernel/futex/core.c */
void exit_robust_list(struct task_struct *curr)
{
struct robust_list_head __user *head = curr->robust_list;
struct robust_list __user *entry, *next_entry;
/* robust 리스트 순회 */
for (entry = head->list.next; entry != &head->list;
entry = next_entry) {
next_entry = entry->next;
/* futex 값에서 소유자 TID 확인 */
get_user(uval, futex_uaddr);
if ((uval & FUTEX_TID_MASK) == curr->pid) {
/* FUTEX_OWNER_DIED 비트 설정 */
handle_futex_death(futex_uaddr, curr);
/* → FUTEX_WAKE로 대기자 깨움 */
}
}
}
EOWNERDEAD 처리 필수: pthread_mutex_lock()이 EOWNERDEAD를 반환하면 반드시 공유 상태를 복구한 뒤 pthread_mutex_consistent()를 호출해야 합니다. 이를 생략하고 unlock()만 하면 mutex는 ENOTRECOVERABLE 상태가 되어 어떤 프로세스도 다시 lock할 수 없습니다.
futex2: Linux 5.16+에서 futex_waitv() 시스템 콜이 추가되어 여러 futex를 동시에 대기할 수 있습니다. Windows의 WaitForMultipleObjects()에 대응하며, Proton/Wine의 게임 호환성을 위해 도입되었습니다.
ntsync — NT 동기화 프리미티브 (v6.14+)
커널 6.14에서 ntsync 서브시스템이 추가되었습니다. Windows NT 커널의 동기화 원시 객체(뮤텍스, 세마포어, 이벤트)를 Linux 커널에 네이티브로 구현하여, Wine/Proton의 게임 에뮬레이션 성능을 크게 향상시킵니다.
| NT 객체 | ntsync 대응 | 기존 Wine 구현 |
|---|---|---|
NtCreateMutant | /dev/ntsync ioctl | 서버 프로세스 RPC (wineserver) |
NtCreateSemaphore | /dev/ntsync ioctl | 서버 프로세스 RPC |
NtCreateEvent | /dev/ntsync ioctl | 서버 프로세스 RPC |
WaitForMultipleObjects | 커널 내 원자적 다중 대기 | wineserver 경유 (고비용) |
WaitForMultipleObjects 같은 NT 동기화 연산을 wineserver 프로세스를 경유한 RPC로 처리했습니다. ntsync는 이를 커널 내에서 직접 처리하여 프로세스 간 통신 오버헤드(Overhead)를 제거합니다. 특히 동기화가 빈번한 게임에서 프레임 레이트가 유의미하게 향상됩니다(벤치마크에 따라 5~20%).
현대 IPC 메커니즘: eventfd/signalfd/timerfd, epoll, Netlink, Unix Domain Socket, Cross Memory Attach, memfd_create, pidfd, Android Binder 등 현대적 IPC 메커니즘은 현대 리눅스 IPC 메커니즘 페이지에서 상세히 다룹니다.
IPC 흔한 실수와 모범 사례
IPC 프로그래밍에서 자주 발생하는 실수와 이를 방지하기 위한 모범 사례를 정리합니다.
| 흔한 실수 | 증상 | 모범 사례 |
|---|---|---|
| SysV IPC 자원 미정리 | 프로세스 종료 후에도 ipcs에 자원 잔존, 시스템 한계 도달 |
항상 IPC_RMID로 제거하거나, 자동 정리되는 POSIX IPC 사용 |
| Pipe 데드락 (양쪽 fd를 같은 프로세스에서 보유) | write가 버퍼 가득 차서 블록, read 불가 | fork() 후 사용하지 않는 pipe 끝을 즉시 close() |
| 시그널 핸들러에서 재진입 불안전 함수 호출 | 교착, 데이터 손상, 미정의 동작 | async-signal-safe 함수만 사용하거나, signalfd로 전환 |
| 공유 메모리에 동기화 없이 접근 | 데이터 경합(Data Race), 불일치 읽기 | 반드시 세마포어(Semaphore)/뮤텍스(Mutex)/futex와 함께 사용 |
| Unix socket 버퍼 오버플로 무시 | send() 블록 또는 EAGAIN 반환, 메시지 유실 |
반환값 확인, 논블로킹(Non-blocking) + epoll 사용 |
FIFO open() 블로킹 |
읽기/쓰기 쪽 중 하나만 열면 무한 대기 | O_NONBLOCK 사용 또는 O_RDWR로 열기 |
| futex 의사 깨움(Spurious Wakeup) 미처리 | 조건 미충족 상태에서 진행, 논리 오류 | 항상 루프 내에서 조건 재검사: while (cond) futex_wait() |
epoll EPOLLET 모드에서 불완전 읽기 |
데이터 잔류, 이벤트 미수신 (edge 재발생 안 됨) | ET 모드에서는 EAGAIN이 반환될 때까지 논블로킹 read로 완전히 소진 |
| PID 재사용 레이스로 잘못된 프로세스에 시그널 | 의도하지 않은 프로세스 종료 | pidfd_open() + pidfd_send_signal() 사용 |
| 메시지 큐 크기 제한 무시 | mq_send() 블록 또는 EAGAIN |
mq_getattr()로 용량 확인, 적절한 mq_maxmsg 설정 |
IPC 선택 가이드: 새 프로젝트에서는 SysV IPC보다 POSIX IPC(자동 정리, fd 기반)를 권장합니다. 단순 이벤트 통지에는 eventfd, 타이머에는 timerfd, 시그널 처리에는 signalfd를 사용하여 모든 이벤트 소스를 하나의 epoll 루프로 통합하면 코드 복잡성을 크게 줄일 수 있습니다. 이러한 현대적 메커니즘의 상세 내용은 현대 리눅스 IPC 메커니즘 페이지를 참고하세요.
IPC 종합 비교
전통적 IPC와 현대 IPC를 포함한 전체 비교표입니다. 현대 메커니즘(eventfd, epoll, Netlink, memfd, pidfd 등)의 상세 설명은 현대 리눅스 IPC 메커니즘 페이지를 참고하세요.
| 메커니즘 | 방향 | 메시지 경계 | fd 기반 | epoll 통합 | Namespace | 속도 | 주요 용도 |
|---|---|---|---|---|---|---|---|
| Pipe | 단방향 | 없음 | O | O | - | 빠름 | 부모-자식 통신, 셸 파이프라인(Pipeline) |
| FIFO | 단방향 | 없음 | O | O | mount NS | 빠름 | 관계없는 프로세스 간 통신 |
| Signal | 단방향 | - | X | signalfd | PID NS | 빠름 | 비동기 이벤트 통지 |
| SysV MQ | 양방향 | 있음 | X | X | IPC NS | 보통 | 타입별 메시지 교환 |
| SysV SHM | 양방향 | - | X | X | IPC NS | 최고 | 대량 데이터 공유 |
| SysV Sem | 동기화 | - | X | X | IPC NS | 보통 | 프로세스 간 잠금 |
| POSIX MQ | 양방향 | 있음 | O | O | IPC NS | 보통 | 우선순위 메시지, fd 통합 |
| POSIX SHM | 양방향 | - | O | X | mount NS | 최고 | /dev/shm 기반 공유 |
| Futex | 동기화 | - | X | X | - | 최고 | 저수준 동기화 프리미티브 |
| eventfd | 양방향 | 카운터 | O | O | - | 빠름 | 이벤트 통지, KVM 연동 |
| Netlink | 양방향 | 있음 | O | O | net NS | 보통 | 커널-유저 제어 채널 |
| Unix Socket | 양방향 | 선택 | O | O | net NS | 빠름 | 범용 로컬 IPC, fd 전달 |
| timerfd | 단방향 | 카운터 | O | O | - | 빠름 | 타이머 이벤트 |
IPC 디버깅
커널 IPC 자원의 상태를 진단하고 문제를 해결하기 위한 도구들입니다.
ipcs / ipcrm
# System V IPC 자원 조회
ipcs -a # 모든 IPC 자원 (MQ, SHM, SEM)
ipcs -m # 공유 메모리만
ipcs -q # 메시지 큐만
ipcs -s # 세마포어만
ipcs -l # 시스템 제한값 표시
# IPC 자원 삭제
ipcrm -m <shmid> # 공유 메모리 삭제
ipcrm -q <msqid> # 메시지 큐 삭제
ipcrm -s <semid> # 세마포어 삭제
/proc/sysvipc
# 커널이 노출하는 IPC 정보
cat /proc/sysvipc/msg # 메시지 큐 상세
cat /proc/sysvipc/shm # 공유 메모리 상세
cat /proc/sysvipc/sem # 세마포어 상세
# IPC 제한 파라미터 확인/변경
sysctl kernel.msgmax # 최대 메시지 크기
sysctl kernel.shmmax # 최대 공유 메모리 세그먼트 크기
sysctl kernel.sem # SEMMSL SEMMNS SEMOPM SEMMNI
strace / bpftrace
# IPC 관련 시스템 콜 추적
strace -e trace=ipc ./program # SysV IPC 시스템 콜 추적
strace -e trace=%signal ./program # 시그널 관련 추적
strace -e read,write -e fd=3 ./program # 특정 fd의 read/write 추적
# bpftrace로 pipe 쓰기 모니터링
bpftrace -e 'tracepoint:syscalls:sys_enter_write
/args->fd > 2/ { @bytes = hist(args->count); }'
# futex 경합 분석
bpftrace -e 'tracepoint:syscalls:sys_enter_futex
{ @ops = count(); @cmd[args->op & 0xf] = count(); }'
lsof로 IPC 자원 조회
# 특정 프로세스의 열린 IPC 관련 fd 조회
lsof -p <PID> | grep -E 'pipe|socket|eventfd|signalfd|timerfd'
# Unix 도메인 소켓 사용 현황
lsof -U # 모든 Unix 소켓
lsof -U -a -c nginx # nginx의 Unix 소켓만
# 특정 파이프/소켓의 양쪽 끝 찾기
lsof | grep 'pipe:\[12345\]' # inode 번호로 pipe 양쪽 프로세스 식별
# /proc/PID/fdinfo로 상세 정보 확인
cat /proc/<PID>/fdinfo/3 # fd 3의 상세 정보
# eventfd-count: 0 ← eventfd의 현재 카운터
# sigmask: 0000000000000006 ← signalfd의 시그널 마스크
# clockid: 0 ← timerfd의 클럭 소스
# /proc/PID/fd 심볼릭 링크로 fd 타입 확인
ls -la /proc/<PID>/fd/
# lrwx------ 1 user user 0 ... 3 -> pipe:[12345]
# lrwx------ 1 user user 0 ... 4 -> socket:[67890]
# lrwx------ 1 user user 0 ... 5 -> anon_inode:[eventfd]
perf를 사용한 IPC 성능 분석
# IPC 중 컨텍스트 스위치 횟수 측정
perf stat -e context-switches,cpu-migrations,page-faults \
-p <PID> -- sleep 10
# 시스템 콜 레이턴시 추적 (IPC 관련)
perf trace -e sendmsg,recvmsg,read,write,epoll_wait \
-p <PID> -s 2>/dev/null
# 특정 IPC 시스템 콜의 지연 시간 히스토그램
perf trace -e epoll_wait --duration 1 -p <PID> 2>&1 | \
awk '{print $NF}' | sort -n
# flamegraph로 IPC 병목 지점 식별
perf record -g -p <PID> -- sleep 30
perf script | stackcollapse-perf.pl | flamegraph.pl > ipc-flame.svg
# bpftrace로 pipe/socket read 지연 시간 측정
bpftrace -e 'tracepoint:syscalls:sys_enter_read { @start[tid] = nsecs; }
tracepoint:syscalls:sys_exit_read /@start[tid]/ {
@usecs = hist((nsecs - @start[tid]) / 1000);
delete(@start[tid]);
}'
일반적인 IPC 문제 진단
| 증상 | 가능한 원인 | 진단 방법 | 해결책 |
|---|---|---|---|
| 프로세스 hang (응답 없음) | futex/세마포어 데드락 | cat /proc/PID/wchan, strace -p PID |
잠금 순서 통일, 타임아웃(Timeout) 추가 |
ipcs에 좀비 자원 잔존 |
SysV IPC IPC_RMID 누락 |
ipcs -a, /proc/sysvipc/* 확인 |
ipcrm으로 수동 삭제, POSIX IPC로 전환 |
pipe/socket write() 블록 |
수신 측 읽기 안 함, 버퍼 가득 참 | cat /proc/PID/fdinfo/N, ss -x |
논블로킹 + epoll, 버퍼 크기 조정 (SO_SNDBUF) |
EACCES / EPERM |
IPC 네임스페이스(Namespace) 불일치, 권한 부족 | ls -la /dev/shm/, nsenter로 NS 확인 |
같은 네임스페이스 사용, 적절한 권한 부여 |
SIGPIPE로 프로세스 종료 |
pipe/socket 상대방이 이미 닫힘 | strace에서 write → SIGPIPE 확인 |
signal(SIGPIPE, SIG_IGN) 또는 MSG_NOSIGNAL |
ENOMEM / ENOSPC |
IPC 시스템 제한 초과 | sysctl kernel.msg*, sysctl kernel.shm* |
sysctl로 제한값 증가, 불필요한 자원 정리 |
| epoll에서 이벤트 수신 안 됨 | ET 모드 불완전 소진, fd 미등록 | strace -e epoll_ctl,epoll_wait |
LT 모드 사용, 또는 ET에서 EAGAIN까지 읽기 |
참고 링크
- Linux Kernel — IPC Userspace API — 커널 공식 문서의 IPC 유저스페이스 API 섹션입니다
- sysvipc(7) — System V IPC 메커니즘(메시지 큐, 세마포어, 공유 메모리) 전체 개요 매뉴얼입니다
- pipe(7) — 파이프와 FIFO의 용량, 원자성 보장, O_NONBLOCK 동작을 설명합니다
- pipe(2) — pipe() 및 pipe2() 시스템 콜의 인터페이스와 플래그를 다룹니다
- mq_overview(7) — POSIX 메시지 큐의 생성, 속성, 알림 메커니즘을 설명합니다
- shm_overview(7) — POSIX 공유 메모리 객체의 생성과 메모리 매핑 방법을 다룹니다
- sem_overview(7) — POSIX 세마포어(named/unnamed)의 동작과 사용법을 설명합니다
- msgget(2) — System V 메시지 큐 생성 및 접근 시스템 콜입니다
- shmget(2) — System V 공유 메모리 세그먼트 할당 시스템 콜입니다
- semget(2) — System V 세마포어 집합 생성 시스템 콜입니다
- futex(2) — Fast Userspace Mutex 시스템 콜의 op 코드와 동작을 다룹니다
- signal(7) — 리눅스 시그널의 종류, 기본 동작, 실시간 시그널 확장을 설명합니다
- LWN: Scalability of pipes — 파이프 구현의 확장성 개선 패치와 성능 분석을 다룹니다
- LWN: A futex overview and update — futex의 내부 구현과 PI futex, robust futex를 심층 분석합니다
- POSIX.1-2024 (IEEE Std 1003.1) — IPC 관련 POSIX 표준 규격의 최신 버전입니다
ipc/msg.c— System V 메시지 큐의 커널 구현부입니다ipc/shm.c— System V 공유 메모리의 커널 구현부입니다ipc/sem.c— System V 세마포어의 커널 구현부입니다ipc/mqueue.c— POSIX 메시지 큐의 커널 구현부입니다fs/pipe.c— 파이프와 FIFO의 커널 구현부입니다kernel/futex/— futex 서브시스템 전체 구현 디렉터리입니다kernel/signal.c— 시그널 처리의 커널 구현부입니다
관련 문서
- 현대 리눅스 IPC 메커니즘 - eventfd/signalfd/timerfd, epoll, Netlink, memfd, pidfd, Android Binder
- Unix Domain Socket - AF_UNIX 커널 구현, SCM_RIGHTS, GC, 성능 최적화
- 동기화 기법 - spinlock, mutex, rwlock 등 커널 내부 동기화 프리미티브
- 시그널 처리 - 프로세스 간 신호 기반 IPC
- 네트워크 스택 - Netlink 심층 구현
- 네임스페이스 - IPC namespace를 포함한 커널 네임스페이스 격리
- 프로세스 관리 - task_struct, 시그널 핸들링, fork/exec