IPC (Inter-Process Communication)

Linux 커널의 전통적 프로세스(Process) 간 통신(IPC)을 데이터 전달, 동기화 관점으로 체계화해 설명합니다. Pipe/FIFO 데이터 경로, 시그널(Signal) 처리, System V와 POSIX IPC의 자원 관리 모델, futex 기반 사용자 동기화, IPC 모범 사례와 디버깅(Debugging) 절차까지 심층 분석합니다.

현대 IPC 메커니즘: eventfd/signalfd/timerfd와 epoll 통합, Netlink 소켓, Cross Memory Attach, memfd_create, pidfd, Android Binder 등 현대적 IPC 메커니즘은 현대 리눅스 IPC 메커니즘 페이지에서 다룹니다.
관련 표준: POSIX.1-2017 (POSIX IPC, 시그널(Signal), 파이프), System V IPC (메시지 큐, 공유 메모리, 세마포어(Semaphore)) — 커널 IPC 메커니즘이 구현하는 핵심 규격입니다. 종합 목록은 참고자료 — 표준 & 규격 섹션을 참고하세요.
전제 조건: 프로세스 스케줄러(Scheduler)프로세스 문서를 먼저 읽으세요. 실행 단위 관리 주제는 태스크(Task) 상태 전이와 큐 정책이 핵심이므로, 스케줄링 기준과 wakeup 경로를 먼저 이해해야 합니다.

핵심 요약

  • Pipe — 단방향 바이트 스트림. 부모-자식 프로세스 간 가장 간단한 통신 수단입니다.
  • 공유 메모리 — 가장 빠른 IPC. 여러 프로세스가 같은 물리 페이지(Page)를 매핑(Mapping)하여 데이터를 공유합니다.
  • 메시지 큐 — 구조화된 메시지를 큐에 넣고 꺼내는 방식. System V와 POSIX 두 가지 API가 있습니다.
  • Unix Domain Socket — 같은 호스트 내 프로세스 간 양방향 통신. 네트워크 소켓(Socket)과 동일한 API를 사용합니다.
  • eventfd / signalfd — 이벤트 통지를 위한 경량 파일 디스크립터(File Descriptor) 기반 메커니즘입니다.

단계별 이해

  1. 파이프 체험ls | grep txt처럼 셸의 |가 바로 파이프입니다. 왼쪽 프로세스의 stdout이 오른쪽의 stdin으로 연결됩니다.

    C에서는 pipe() 시스템 콜(System Call)로 생성합니다.

  2. 공유 메모리 이해shmget()/shmat() 또는 mmap(MAP_SHARED)로 생성합니다.

    동기화(세마포어, 뮤텍스(Mutex))가 없으면 데이터 경쟁이 발생할 수 있습니다.

  3. 소켓 통신 — Unix Domain Socket은 파일 경로를 주소로 사용하며, AF_UNIX로 생성합니다.

    Docker, systemd, X11 등 많은 시스템 데몬이 UDS를 사용합니다.

  4. 선택 기준 — 단순 데이터 전달은 파이프, 대용량 공유는 공유 메모리, 구조화 메시지는 메시지 큐, 네트워크 호환 필요 시 소켓을 선택합니다.

    ipcs 명령어로 시스템의 현재 IPC 리소스를 확인할 수 있습니다.

IPC 개요와 분류

Linux 커널은 프로세스 간 데이터 교환, 동기화, 이벤트 통지를 위해 다양한 IPC 메커니즘을 제공합니다. 각 메커니즘은 서로 다른 사용 사례에 최적화되어 있으며, 커널 내부에서 별도의 서브시스템으로 구현됩니다.

Linux IPC 아키텍처 전체 개요 유저 공간 (User Space) Shell / Daemon Application A Application B glibc (pthread) systemd / D-Bus Android (Binder) 시스템 콜 인터페이스 (System Call Interface) pipe2() kill() rt_sigaction() msgget/snd/rcv mq_open mq_send/recv futex() eventfd signalfd / timerfd socket() / bind() ioctl (binder) 커널 공간 (Kernel Space) Pipe / FIFO pipefs (fs/pipe.c) pipe_inode_info circular buffer Signal kernel/signal.c sigpending, sigaction TIF_SIGPENDING SysV IPC msg.c, shm.c, sem.c ipc_namespace kern_ipc_perm + IDR POSIX IPC ipc/mqueue.c tmpfs (/dev/shm) mqueue_inode_info Futex kernel/futex/ hash bucket rt_mutex / PI eventfd signalfd / timerfd fs/eventfd.c epoll 통합, wait_queue Socket AF_UNIX / Netlink sk_buff, unix_sock SCM_RIGHTS Binder drivers/android/ binder.c mmap + 트랜잭션 공통 인프라 wait_queue_head_t | schedule() / wake_up() | copy_from_user / copy_to_user | ipc_namespace | VFS (file_operations) | mm_struct (mmap) 유저 공간 시스템 콜 커널 서브시스템 공통 인프라
Linux IPC 아키텍처 전체 개요 - 유저 공간 애플리케이션이 시스템 콜을 통해 커널의 각 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) 탐색을 제공합니다. rwsemipcget()(쓰기)과 ipc_obtain_object_check()(읽기) 사이의 동시성을 제어하며, seq는 ID 재사용 시 stale 핸들을 탐지하기 위한 시퀀스 번호입니다. (ipc/util.h)
  • kern_ipc_perm 모든 System V IPC 객체(msg_queue, shmid_kernel, sem_array)의 첫 번째 멤버로 임베딩되어, 다형적 접근을 가능하게 하는 공통 헤더입니다. keyftok()가 생성하는 사용자 식별자이고, id는 커널이 인덱스+시퀀스를 조합하여 반환하는 실제 핸들입니다. uid/gidcuid/cgid를 분리하여 소유권 이전 후에도 원래 생성자를 추적할 수 있으며, mode는 파일 퍼미션과 동일한 9비트 접근 제어를 제공합니다. (include/linux/ipc.h)
ℹ️

IPC 키와 ID: System V IPC에서 key_tftok()로 생성하는 전역 식별자이고, 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;
};
Pipe 환형 버퍼 구조 Writer write(fd[1]) pipe_buffer[ring_size] [0] [1] [2] [3] [4] ... [N-1] tail (읽기) head (쓰기) Reader read(fd[0]) 기본: 16 슬롯 × 4KB 페이지 = 64KB 가득 차면 writer 블록 / 비면 reader 블록 pipe_buffer: { page*, offset, len, ops, flags }
Pipe 환형 버퍼 구조 - Writer가 head에 쓰고 Reader가 tail에서 읽음

pipe2() 시스템 콜

pipe2()는 파이프를 생성하는 시스템 콜로, 커널 내부에서 do_pipe2()를 통해 처리됩니다. O_CLOEXECO_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-softpipe-user-pages-hard는 사용자별 총 파이프 버퍼 페이지 수를 제한합니다.

Signals

시그널은 프로세스에 비동기적으로 이벤트를 통지하는 가장 오래된 IPC 메커니즘입니다. 커널은 시그널 전달 시 대상 프로세스의 task_struct에 시그널 정보를 큐잉하고, 해당 프로세스가 유저 모드로 복귀할 때 시그널 핸들러(Handler)를 실행합니다.

표준 시그널 목록

리눅스는 31개의 표준 시그널(1~31)을 정의합니다. 각 시그널의 기본 동작(default action)은 다섯 가지 중 하나입니다:

기본 동작 설명
Term프로세스 종료
Core코어 덤프(Core Dump) 생성 후 종료
Stop프로세스 중지 (suspended)
Cont중지된 프로세스 재개
Ign시그널 무시
번호 이름 기본 동작 설명
1SIGHUPTerm제어 터미널 hangup 또는 제어 프로세스 종료
2SIGINTTerm키보드 인터럽트 (Ctrl+C)
3SIGQUITCore키보드 종료 (Ctrl+\)
4SIGILLCore잘못된 명령어 (illegal instruction)
5SIGTRAPCore트레이스/브레이크포인트 트랩
6SIGABRTCoreabort() 호출
7SIGBUSCore버스(Bus) 오류 (잘못된 메모리 접근 정렬)
8SIGFPECore부동소수점 예외 (0으로 나누기 포함)
9SIGKILLTerm강제 종료 (캐치/무시 불가)
10SIGUSR1Term사용자 정의 시그널 1
11SIGSEGVCore잘못된 메모리 참조 (segmentation fault)
12SIGUSR2Term사용자 정의 시그널 2
13SIGPIPETerm읽는 쪽이 없는 파이프에 쓰기
14SIGALRMTermalarm() 타이머(Timer) 만료
15SIGTERMTerm정상 종료 요청
16SIGSTKFLTTerm코프로세서 스택 오류 (미사용)
17SIGCHLDIgn자식 프로세스 중지 또는 종료
18SIGCONTCont중지된 프로세스 재개
19SIGSTOPStop프로세스 중지 (캐치/무시 불가)
20SIGTSTPStop터미널 중지 (Ctrl+Z)
21SIGTTINStop백그라운드 프로세스의 터미널 입력
22SIGTTOUStop백그라운드 프로세스의 터미널 출력
23SIGURGIgn소켓의 긴급(OOB) 데이터
24SIGXCPUCoreCPU 시간 제한 초과
25SIGXFSZCore파일 크기 제한 초과
26SIGVTALRMTerm가상 타이머 만료
27SIGPROFTerm프로파일링(Profiling) 타이머 만료
28SIGWINCHIgn터미널 윈도우 크기 변경
29SIGIOTermI/O 가능 (async I/O)
30SIGPWRTerm전원 장애
31SIGSYSCore잘못된 시스템 콜 (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);
}

시그널 전달 내부 구조

Signal 전달 흐름 kill() / rt_sigqueueinfo() 시그널 발생 __send_signal() sigqueue 할당 sigpending 큐 TIF_SIGPENDING 설정 do_signal() 유저 복귀 시 handler 실행 struct sigpending { struct list_head list; /* sigqueue */ sigset_t signal; /* 비트마스크 */ }; struct sigqueue { struct list_head list; kernel_siginfo_t info; /* signo, code, ... */ };
Signal 전달 흐름 - 시그널 발생부터 핸들러 실행까지
/* 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()을 통해 핸들러 실행을 설정합니다.

시그널 핸들러 실행 흐름 유저 → 커널 전환 syscall / interrupt do_signal() pending 확인 handle_signal() 핸들러 선택 setup_rt_frame() 유저 스택 조작 handler() 유저 모드 handler 종료 return rt_sigreturn() trampoline이 호출 restore_sigcontext() 레지스터/마스크 복원 원래 실행 재개 유저 모드 유저 스택 rt_sigframe 구조 pretcode (sigreturn) struct siginfo struct ucontext ↑ 리턴 주소를 sigreturn trampoline로 ↑ si_signo, si_code si_addr 등 정보 ↑ uc_mcontext: 저장된 레지스터 + 시그널 마스크 핸들러 return → pretcode의 sigreturn 트램폴린 실행 → 커널이 ucontext로 원래 상태 복원
시그널 핸들러 실행과 스택 프레임(Stack Frame) 구조 - 커널이 유저 스택을 조작하여 핸들러를 호출하고 sigreturn으로 복원
/* 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 상태로 유지되다가 마스크가 해제되면 전달됩니다. SIGKILLSIGSTOP은 마스크할 수 없습니다.

대체 시그널 스택 (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(&current->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_pendingpending 큐로 구분합니다.

구분 프로세스 지향 시그널 스레드 지향 시그널
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_cbytesq_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 커널 내부 흐름

SysV 메시지 큐 커널 흐름 msgsnd() 경로 copy_from_user() alloc_msg() → msg_msg 할당 ipc_lock(msg_ids) 획득 q_messages에 msg 연결 list_add_tail(&msg→m_list) wake_up(q_receivers) msgrcv() 경로 ipc_lock(msg_ids) 획득 q_messages에서 타입 검색 find_msg(msq, msgtyp) 발견 미발견 unlink msg_msg q_receivers에 등록 schedule() → 슬립 copy_to_user() struct msg_queue q_messages (msg_msg 리스트) q_receivers (대기 수신자) q_senders (대기 송신자) q_qnum / q_qbytes wake
SysV 메시지 큐 - msgsnd()는 메시지를 복사·할당·연결 후 수신 대기자를 깨우고, msgrcv()는 타입별 검색 후 복사하거나 대기
ℹ️

메시지 큐 크기 제한: System V 메시지 큐는 세 가지 커널 파라미터로 제한됩니다.

파라미터의미기본값sysctl 경로
MSGMAX단일 메시지 최대 크기 (바이트)8,192kernel.msgmax
MSGMNB큐 하나의 최대 총 바이트16,384kernel.msgmnb
MSGMNI시스템 전체 최대 큐 수32,000kernel.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.cnewseg()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 명령으로 확인할 수 있어 디버깅 시 어떤 프로세스가 공유 메모리를 사용하는지 추적하는 데 유용합니다.
SysV Shared Memory 매핑 프로세스 A 코드 영역 SHM 매핑 (shmat) 힙/스택 프로세스 B 코드 영역 SHM 매핑 (shmat) 힙/스택 공유 물리 페이지 shmem / tmpfs backing shmid_kernel → shm_file 페이지 테이블 페이지 테이블
SysV Shared Memory - 두 프로세스가 동일한 물리 페이지를 각자의 가상 주소(Virtual Address)에 매핑

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 하나의 세마포어 세트를 나타냅니다. semssemget()nsems 인자만큼 할당된 struct sem 배열 포인터입니다. pending_alterpending_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() 커널 내부 흐름

semop() 커널 내부 흐름 sys_semop() / sys_semtimedop() copy_from_user(sembuf[]) try_atomic_semop() 모든 연산이 즉시 수행 가능한지 검사 성공 (Fast Path) 실패 (Slow Path) sem.semval 값 업데이트 SEM_UNDO → undo 값 기록 대기자 wake (do_smart_update) return 0 sem_queue 할당 pending_alter 리스트에 등록 schedule() → 슬립 (TASK_INTERRUPTIBLE) 다른 프로세스가 semop() do_smart_update() → wake_up try_atomic_semop() 재시도 SEM_UNDO 추적 task_struct → sysvsem.undo_list exit_sem()에서 자동 복원
semop() 커널 흐름 - Fast Path에서는 즉시 세마포어 값을 업데이트하고, Slow Path에서는 pending 리스트에 등록 후 슬립
⚠️

SEM_UNDO: SEM_UNDO 플래그를 사용하면 프로세스가 비정상 종료해도 커널이 세마포어 값을 자동으로 원복합니다. task_structsysvsem.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)
 */
Named vs Unnamed: 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 Fast/Slow 경로 유저 공간 Fast Path cmpxchg(futex_word) 성공? lock 획득/해제 완료 (커널 진입 없음) 대부분의 경우 성공 커널 공간 futex(FUTEX_WAIT) 시스템 콜 진입 futex_wait() hash bucket 대기 futex(FUTEX_WAKE) 대기자 깨우기 (wake_up) 실패 (경합) 커널: futex_hash_bucket에 waiter를 등록하고 TASK_INTERRUPTIBLE로 슬립 해제 측: futex_word 변경 후 FUTEX_WAKE로 waiter를 깨움
Futex Fast/Slow 경로 - 유저 공간 cmpxchg 성공 시 커널 진입 없이 완료

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_WAKEFUTEX_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을 기다리는 동안, 중간 우선순위의 태스크가 실행되어 전체 시스템의 응답 시간이 무한히 늘어나는 현상입니다.

우선순위 역전 (Priority Inversion) 문제와 PI 해결 문제 시나리오 (PI 없음) 시간 → A (높음) B (중간) C (낮음) C: lock 보유, 실행 도착 A: lock 대기 (blocked) ← 우선순위 역전! B: C를 선점(preempt), 실행 중... C: B에 의해 선점됨 (lock 여전히 보유) C: unlock A: 드디어 실행! PI 해결 시나리오 (FUTEX_LOCK_PI) 시간 → A (높음) B (중간) C (낮음→높음) C: lock 보유, 실행 도착 A: 대기 C: PI 부스트! (A 우선순위로 승격) B: 선점 불가 (C가 더 높은 우선순위) A: 빠르게 실행! B: A 완료 후 실행 PI boost 커널 메커니즘: rt_mutex 체인 FUTEX_LOCK_PI → futex_lock_pi() → rt_mutex_slowlock() → task_boost_prio(owner, waiter→prio) 우선순위 체인(PI chain)을 따라 중첩된 lock 소유자까지 전파 (rt_mutex_adjust_prio_chain)
우선순위 역전 - PI 없이는 중간 우선순위 B가 C를 선점하여 A가 무한 대기. PI-Futex는 C의 우선순위를 A 수준으로 부스트하여 해결

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는 이 문제를 커널이 자동으로 감지하고 복구합니다.

동작 원리는 다음과 같습니다:

  1. 프로세스가 set_robust_list()로 robust futex 리스트의 헤드를 커널에 등록합니다.
  2. glibc의 pthread_mutex_lock()이 lock 획득 시 해당 futex를 robust 리스트에 연결합니다.
  3. 프로세스가 비정상 종료하면 커널이 exit_robust_list()를 호출합니다.
  4. 리스트의 각 futex에 FUTEX_OWNER_DIED 비트(bit 30)를 설정합니다.
  5. 해당 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 경유 (고비용)
성능 개선 원리: 기존 Wine은 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까지 읽기

참고 링크