현대 리눅스 IPC 메커니즘

Linux 커널의 현대적 IPC 메커니즘을 심층 분석합니다. eventfd/signalfd/timerfdepoll과 통합한 fd 기반 이벤트 처리, Netlink 소켓을 통한 커널-유저 공간 제어 채널, Unix Domain Socket의 고성능 로컬 통신, process_vm_readv()/process_vm_writev()를 사용한 Cross Memory Attach, memfd_create()의 익명 공유 메모리와 sealing, pidfd의 레이스 프리 프로세스 관리, 그리고 Android Binder의 단일 복사 RPC까지 다룹니다.

관련 문서: 전통적 IPC(파이프(Pipe), 시그널(Signal), System V IPC, POSIX IPC, futex)는 IPC (Inter-Process Communication) 페이지를 참고하세요.
전제 조건: IPC (전통적 IPC) 문서를 먼저 읽으세요. 파이프, 시그널, System V/POSIX IPC의 기본 개념을 이해한 뒤 이 문서를 읽으면 현대 메커니즘의 개선점을 더 잘 파악할 수 있습니다.

핵심 요약

  • eventfd / signalfd / timerfd — 다양한 이벤트 소스를 파일 디스크립터로 추상화하여 epoll과 통합할 수 있는 경량 메커니즘입니다.
  • epoll — 수만~수십만 개의 fd를 효율적으로 모니터링하는 Linux 전용 이벤트 다중화 인터페이스입니다.
  • Netlink — 커널과 유저 공간 간 구조화된 메시지를 교환하는 소켓 기반 제어 채널입니다.
  • memfd_create — 파일시스템 경로 없이 RAM 기반 익명 공유 메모리를 생성하며, sealing으로 불변성을 보장합니다.
  • pidfd — PID 재사용 레이스를 근본적으로 해결하는 파일 디스크립터 기반 프로세스 참조입니다.

단계별 이해

  1. fd 기반 이벤트 통합 — eventfd, signalfd, timerfd를 하나의 epoll 루프에 등록하면 단일 스레드에서 모든 이벤트를 비동기적으로 처리할 수 있습니다.
  2. epoll 이벤트 루프epoll_create1()으로 인스턴스를 생성하고, epoll_ctl()로 fd를 등록한 뒤, epoll_wait()로 이벤트를 대기합니다.
  3. Netlink 활용ip, ss, tc 등의 네트워크 도구가 내부적으로 Netlink를 사용합니다. AF_NETLINK 소켓으로 커널 서브시스템과 직접 통신할 수 있습니다.
  4. 안전한 공유 메모리memfd_create() + sealing으로 공유 메모리의 불변성을 커널 수준에서 보장받을 수 있습니다.

eventfd / signalfd / timerfd

Linux는 다양한 이벤트 소스를 파일 디스크립터로 추상화하여 epoll/select/poll과 통합할 수 있는 fd 기반 IPC를 제공합니다.

fd 유형 용도 핵심 구조체(Struct)
eventfd 프로세스/스레드 간 이벤트 카운터 struct eventfd_ctx
signalfd 시그널을 fd로 수신 struct signalfd_ctx
timerfd 타이머 만료를 fd로 통지 struct timerfd_ctx
/* fs/eventfd.c */
struct eventfd_ctx {
    struct kref            kref;
    wait_queue_head_t      wqh;      /* 대기 큐 */
    __u64                  count;    /* 이벤트 카운터 */
    unsigned int           flags;    /* EFD_SEMAPHORE 등 */
    int                    id;
};

/* eventfd 사용 예 */
int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);

/* 이벤트 발생 (카운터 증가) */
uint64_t val = 1;
write(efd, &val, sizeof(val));

/* 이벤트 수신 (카운터 읽고 0으로 리셋) */
uint64_t cnt;
read(efd, &cnt, sizeof(cnt));
💡

KVM과 eventfd: KVM은 ioeventfdirqfd를 통해 eventfd를 활용합니다. 게스트 VM의 I/O 포트 접근이 ioeventfd를 통해 호스트의 유저 공간 에뮬레이터(QEMU)에 통지되고, irqfd는 호스트에서 게스트에 인터럽트를 주입하는 데 사용됩니다.

signalfd 커널 내부 구조

signalfd는 전통적인 시그널 핸들러 대신 파일 디스크립터를 통해 시그널을 동기적으로 수신할 수 있게 해주는 메커니즘입니다. 시그널을 fd로 변환함으로써 epoll과 통합할 수 있고, 시그널 핸들러의 재진입성(Reentrancy) 문제를 근본적으로 회피합니다.

/* fs/signalfd.c */
struct signalfd_ctx {
    sigset_t sigmask;  /* 감시 대상 시그널 마스크 */
};

/* signalfd의 read()가 반환하는 구조체 */
struct signalfd_siginfo {
    __u32 ssi_signo;    /* 시그널 번호 */
    __s32 ssi_errno;    /* 에러 번호 */
    __s32 ssi_code;     /* 시그널 코드 (SI_USER, SI_QUEUE 등) */
    __u32 ssi_pid;      /* 송신자 PID */
    __u32 ssi_uid;      /* 송신자 UID */
    __s32 ssi_fd;       /* SIGIO용 fd */
    __u32 ssi_tid;      /* 송신자 TID */
    __u64 ssi_ptr;      /* sigqueue의 값 포인터 */
    __u64 ssi_int;      /* sigqueue의 정수 값 */
    /* ... 기타 필드 (128바이트 고정 크기) */
};

signalfd의 read()는 내부적으로 dequeue_signal()을 호출하여 task_struct->pending(개별 대기 시그널)과 signal->shared_pending(프로세스 그룹 공유 시그널)에서 시그널을 꺼냅니다. 시그널이 없으면 poll_wait()를 통해 대기 큐에 등록되어 시그널 도착 시 깨어납니다.

#include <sys/signalfd.h>
#include <sys/epoll.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>

int main(void) {
    sigset_t mask;

    /* 1. 시그널 블록 — 기본 핸들러 억제 */
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);
    sigprocmask(SIG_BLOCK, &mask, NULL);

    /* 2. signalfd 생성 */
    int sfd = signalfd(-1, &mask, SFD_NONBLOCK | SFD_CLOEXEC);

    /* 3. epoll에 등록 */
    int epfd = epoll_create1(EPOLL_CLOEXEC);
    struct epoll_event ev = { .events = EPOLLIN, .data.fd = sfd };
    epoll_ctl(epfd, EPOLL_CTL_ADD, sfd, &ev);

    /* 4. 이벤트 루프 */
    struct epoll_event events[8];
    for (;;) {
        int n = epoll_wait(epfd, events, 8, -1);
        for (int i = 0; i < n; i++) {
            struct signalfd_siginfo si;
            read(events[i].data.fd, &si, sizeof(si));
            printf("시그널 %d 수신 (PID=%u)\n", si.ssi_signo, si.ssi_pid);
            if (si.ssi_signo == SIGTERM)
                return 0;
        }
    }
}
비교 항목 전통 시그널 핸들러 signalfd
처리 방식 비동기 (핸들러 인터럽트) 동기 (read/epoll)
재진입성 핸들러 내 async-signal-safe 함수만 허용 일반 코드처럼 안전하게 처리
epoll 통합 불가 (self-pipe trick 필요) 직접 통합 가능
시그널 정보 siginfo_t (핸들러 인자) signalfd_siginfo (128바이트)
멀티스레드 핸들러 전달 스레드 예측 어려움 특정 스레드에서 read()로 수신

timerfd 커널 내부 구조

timerfd는 커널의 고해상도 타이머(hrtimer)를 파일 디스크립터로 노출하여 epoll 기반 이벤트 루프에서 타이머를 통합 관리할 수 있게 합니다.

/* fs/timerfd.c */
struct timerfd_ctx {
    union {
        struct hrtimer   tmr;      /* 고해상도 타이머 */
        struct alarm     alarm;    /* CLOCK_REALTIME_ALARM용 */
    } t;
    ktime_t                   moffs;    /* CLOCK_REALTIME 보정값 */
    wait_queue_head_t         wqh;      /* 대기 큐 */
    u64                       ticks;    /* 만료 횟수 카운터 */
    int                       clockid;  /* 클럭 소스 ID */
    short unsigned            expired;  /* 만료 상태 플래그 */
    short unsigned            settime_flags; /* TFD_TIMER_ABSTIME 등 */
    struct rcu_head           rcu;
    struct list_head          clist;    /* cancel list */
    spinlock_t                cancel_lock;
    bool                      might_cancel;
};
클럭 소스 기준 절전 모드(Suspend) 시간 변경 영향 용도
CLOCK_MONOTONIC 부팅 시점 멈춤 없음 일반 타이머, 성능 측정
CLOCK_REALTIME UTC 에포크(Epoch) 멈춤 영향 받음 (NTP 등) 절대 시각 기반 스케줄링
CLOCK_BOOTTIME 부팅 시점 계속 진행 없음 절전 포함 경과 시간
CLOCK_REALTIME_ALARM UTC 에포크 시스템 깨움 영향 받음 알람 (Android wake alarm)

TFD_TIMER_ABSTIME 플래그를 사용하면 절대 시각 기반으로 타이머를 설정할 수 있습니다. 이 플래그 없이는 상대 시간(현재로부터의 경과 시간)으로 해석됩니다.

#include <sys/timerfd.h>
#include <sys/epoll.h>
#include <stdint.h>
#include <stdio.h>
#include <unistd.h>

int main(void) {
    int epfd = epoll_create1(EPOLL_CLOEXEC);

    /* --- 원샷(One-shot) 타이머: 3초 후 1회 발동 --- */
    int tfd1 = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
    struct itimerspec one = {
        .it_value    = { .tv_sec = 3, .tv_nsec = 0 },
        .it_interval = { .tv_sec = 0, .tv_nsec = 0 }  /* interval=0 → 원샷 */
    };
    timerfd_settime(tfd1, 0, &one, NULL);

    /* --- 주기적(Periodic) 타이머: 1초마다 반복 --- */
    int tfd2 = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC);
    struct itimerspec periodic = {
        .it_value    = { .tv_sec = 1, .tv_nsec = 0 },
        .it_interval = { .tv_sec = 1, .tv_nsec = 0 }  /* 1초마다 반복 */
    };
    timerfd_settime(tfd2, 0, &periodic, NULL);

    /* epoll에 둘 다 등록 */
    struct epoll_event ev1 = { .events = EPOLLIN, .data.fd = tfd1 };
    struct epoll_event ev2 = { .events = EPOLLIN, .data.fd = tfd2 };
    epoll_ctl(epfd, EPOLL_CTL_ADD, tfd1, &ev1);
    epoll_ctl(epfd, EPOLL_CTL_ADD, tfd2, &ev2);

    /* 이벤트 루프 */
    struct epoll_event events[4];
    for (int count = 0; count < 10; ) {
        int n = epoll_wait(epfd, events, 4, -1);
        for (int i = 0; i < n; i++) {
            uint64_t ticks;
            read(events[i].data.fd, &ticks, sizeof(ticks));
            if (events[i].data.fd == tfd1)
                printf("원샷 타이머 만료! (ticks=%lu)\n", ticks);
            else
                printf("주기 타이머 #%d (ticks=%lu)\n", ++count, ticks);
        }
    }
    close(tfd1); close(tfd2); close(epfd);
}
ℹ️

커널 내부 흐름: timerfd_settime()hrtimer_start()를 호출하여 고해상도 타이머를 등록합니다. 타이머 만료 시 timerfd_tmrproc() 콜백이 실행되어 ticks 카운터를 증가시키고 wake_up()으로 대기 중인 epoll_wait()/read() 호출자를 깨웁니다. read()는 누적된 ticks 값을 반환하고 카운터를 0으로 리셋합니다.

epoll + fd 통합 이벤트 루프 패턴

eventfd, signalfd, timerfd를 포함한 다양한 fd 소스를 하나의 epoll 인스턴스로 통합하면 단일 이벤트 루프에서 모든 I/O와 이벤트를 처리할 수 있습니다.

epoll 통합 이벤트 루프 패턴 epoll_wait() 이벤트 루프 (Event Loop) eventfd 프로세스/스레드 간 이벤트 signalfd 시그널 동기 수신 timerfd 주기적/원샷 타이머 pipe 자식 프로세스 출력 Unix Socket 로컬 클라이언트 연결 TCP Socket 네트워크 클라이언트 하나의 epoll 인스턴스로 모든 fd 소스를 통합 단일 스레드 비동기 이벤트 처리 가능

epoll: 확장 가능한 I/O 이벤트 다중화(Multiplexing)

epoll은 Linux 전용 I/O 이벤트 알림 인터페이스로, select()/poll()의 확장성 한계를 극복하기 위해 Linux 2.5.44(2002)에 도입되었습니다. 수만~수십만 개의 파일 디스크립터를 효율적으로 모니터링할 수 있으며, Nginx, HAProxy, Redis, Node.js 등 고성능 네트워크 서버의 핵심 이벤트 루프 기반입니다.

select/poll의 한계와 epoll의 차별점

비교 항목 select poll epoll
fd 상한 FD_SETSIZE (1024) 제한 없음 (배열 크기) 제한 없음 (/proc/sys/fs/epoll/max_user_watches)
fd 전달 방식 매 호출마다 전체 fd_set 복사 매 호출마다 전체 pollfd 배열 복사 커널이 관심 fd를 영속 관리 (epoll_ctl로 1회 등록)
이벤트 검사 O(n) — 전체 fd 순회 O(n) — 전체 배열 순회 O(1) — ready list에서 즉시 반환
Edge-Triggered 불가 불가 지원 (EPOLLET)
스레드 안전 불가 (fd_set 공유 불가) 제한적 EPOLLEXCLUSIVE로 thundering herd 방지
ℹ️

O(1) 이벤트 전달: select/poll은 호출마다 커널이 등록된 모든 fd를 순회하며 이벤트를 확인합니다. fd가 10만 개여도 이벤트가 발생한 fd가 10개뿐이면, select/poll은 10만 번 검사하지만 epoll_wait은 ready list에서 10개만 반환합니다.

epoll API

#include <sys/epoll.h>

/* 1. epoll 인스턴스 생성 — 커널에 struct eventpoll 할당 */
int epfd = epoll_create1(EPOLL_CLOEXEC);
/* epoll_create(size)는 폐기 예정 — size 인자 무시됨 */

/* 2. 관심 fd 등록/수정/삭제 */
struct epoll_event ev = {
    .events  = EPOLLIN | EPOLLET,      /* 이벤트 마스크 */
    .data.fd = client_fd               /* 사용자 데이터 (union) */
};
epoll_ctl(epfd, EPOLL_CTL_ADD, client_fd, &ev);  /* 등록 */
epoll_ctl(epfd, EPOLL_CTL_MOD, client_fd, &ev);  /* 수정 */
epoll_ctl(epfd, EPOLL_CTL_DEL, client_fd, NULL); /* 삭제 */

/* 3. 이벤트 대기 — ready 상태인 fd만 반환 */
struct epoll_event events[64];
int nfds = epoll_wait(epfd, events, 64, -1);  /* -1 = 무한 대기 */
for (int i = 0; i < nfds; i++) {
    if (events[i].events & EPOLLIN)
        handle_read(events[i].data.fd);
}

커널 내부 자료구조

epoll은 세 가지 핵심 자료구조의 조합으로 동작합니다: Red-Black Tree(관심 fd 관리), Ready List(이벤트 발생 fd), Wait Queue(epoll_wait 호출자 대기).

/* fs/eventpoll.c */
struct eventpoll {
    rwlock_t             lock;          /* rdllist/ovflist 보호 */
    struct mutex         mtx;           /* epoll_ctl 직렬화 */
    wait_queue_head_t    wq;            /* epoll_wait() 대기 큐 */
    wait_queue_head_t    poll_wait;     /* epoll fd 자체가 poll 될 때 */
    struct list_head     rdllist;       /* ★ ready list (이벤트 발생 fd) */
    struct rb_root_cached rbr;          /* ★ rbtree (등록된 전체 fd) */
    struct epitem       *ovflist;       /* 전송 중 오버플로 리스트 */
    struct wakeup_source *ws;            /* EPOLLWAKEUP용 */
    struct user_struct  *user;          /* 리소스 제한 추적 */
    struct file        *file;          /* epoll fd의 struct file */
    struct hlist_head    refs;          /* 중첩 epoll 참조 */
    unsigned int         napi_id;       /* busy poll NAPI ID */
};

/* 등록된 각 fd를 나타내는 노드 */
struct epitem {
    union {
        struct rb_node  rbn;          /* rbtree 노드 */
        struct rcu_head rcu;          /* RCU 해제용 */
    };
    struct list_head     rdllink;      /* ready list 링크 */
    struct epitem       *next;         /* ovflist 체인 */
    struct epoll_filefd  ffd;           /* {file *, fd} 쌍 */
    struct eppoll_entry *pwqlist;       /* poll wait queue 리스트 */
    struct eventpoll   *ep;            /* 소속 eventpoll */
    struct epoll_event  event;         /* 사용자 이벤트 + 데이터 */
};
epoll 내부 아키텍처 struct eventpoll epoll_create1()로 생성 rb_root_cached (rbtree) 등록된 모든 fd 관리 ep ep ep rdllist (ready list) 이벤트 발생 fd만 fd=5 fd=12 fd=28 epoll_ctl(ADD) rbtree 삽입 ep_poll_callback() ready list에 추가 epoll_wait() ready list 수확 wake_up() / 드라이버 통지
epoll 내부 구조: rbtree(전체 fd) + ready list(이벤트 fd) + callback 메커니즘

이벤트 전달 흐름

epoll의 핵심은 콜백(Callback) 기반 이벤트 전달입니다. fd 등록 시 해당 파일의 wait queue에 콜백(ep_poll_callback)을 설치하여, 이벤트 발생 시 커널이 자동으로 ready list에 추가합니다.

/* fs/eventpoll.c — 핵심 흐름 요약 */

/* ① epoll_ctl(EPOLL_CTL_ADD): fd 등록 */
static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
                     struct file *tfile, int fd)
{
    struct epitem *epi;
    epi = kmem_cache_zalloc(epi_cache, GFP_KERNEL);

    /* rbtree에 삽입 (fd + file 포인터로 정렬) */
    ep_rbtree_insert(ep, epi);

    /* 대상 fd의 wait queue에 콜백 등록 */
    init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
    revents = ep_item_poll(epi, &epq.pt, 1);
    /* → 내부에서 file->poll()을 호출하여 */
    /*   ep_poll_callback을 wait queue에 설치 */

    /* 이미 ready 상태이면 즉시 ready list에 추가 */
    if (revents && !ep_is_linked(epi)) {
        list_add_tail(&epi->rdllink, &ep->rdllist);
        wake_up(&ep->wq);  /* epoll_wait 대기자 깨우기 */
    }
}

/* ② 이벤트 발생 시: 드라이버가 wake_up() → ep_poll_callback 호출 */
static int ep_poll_callback(wait_queue_entry_t *wait,
                            unsigned mode, int sync, void *key)
{
    struct epitem *epi = ep_item_from_wait(wait);
    struct eventpoll *ep = epi->ep;
    __poll_t pollflags = (__poll_t)(unsigned long)key;

    /* 이벤트 마스크 확인 */
    if (pollflags && !(pollflags & epi->event.events))
        return 0;  /* 관심 없는 이벤트 무시 */

    /* ready list에 추가 (중복 방지: rdllink가 이미 연결되어 있으면 스킵) */
    if (!ep_is_linked(epi))
        list_add_tail(&epi->rdllink, &ep->rdllist);

    /* epoll_wait()에서 대기 중인 태스크 깨우기 */
    wake_up(&ep->wq);
    return 1;
}

/* ③ epoll_wait(): ready list에서 이벤트 수확 */
static int ep_poll(struct eventpoll *ep, struct epoll_event *events,
                   int maxevents, struct timespec64 *timeout)
{
    /* ready list가 비어있으면 wq에서 대기 */
    if (list_empty(&ep->rdllist))
        schedule_hrtimeout_range(timeout, ...);

    /* ready list → 사용자 공간 epoll_event 배열로 복사 */
    return ep_send_events(ep, events, maxevents);
}

Level-Triggered vs Edge-Triggered

epoll의 두 가지 트리거 모드는 이벤트 재전달 정책의 차이입니다.

모드 플래그 동작 특징
LT (Level-Triggered) 기본값 조건이 유지되는 동안 epoll_wait가 반복 반환 프로그래밍 용이, select/poll과 동일 의미론
ET (Edge-Triggered) EPOLLET 상태 변화 시 한 번만 반환 높은 성능, 반드시 논블로킹 + EAGAIN까지 읽기
/* fs/eventpoll.c — ep_send_events_proc() 내 LT/ET 분기 */
static __poll_t ep_send_events_proc(struct eventpoll *ep,
                                     struct list_head *txlist, ...)
{
    struct epitem *epi;

    list_for_each_entry_safe(epi, tmp, txlist, rdllink) {
        /* ready list에서 분리 */
        list_del_init(&epi->rdllink);

        /* 실제 이벤트 재확인 (poll 호출) */
        revents = ep_item_poll(epi, &pt, 1);
        if (!revents)
            continue;

        /* 사용자 공간으로 이벤트 전달 */
        __put_user(revents, &uevent->events);
        __put_user(epi->event.data, &uevent->data);

        /* ★ LT 모드: 이벤트가 아직 유효하면 ready list에 재삽입 */
        if (!(epi->event.events & EPOLLET))
            list_add_tail(&epi->rdllink, &ep->rdllist);
    }
}
⚠️

ET 모드 필수 패턴: Edge-Triggered 사용 시 반드시 (1) 소켓을 O_NONBLOCK으로 설정하고, (2) read()/recv()EAGAIN을 반환할 때까지 루프로 읽어야 합니다. 그렇지 않으면 버퍼에 남은 데이터가 영원히 통지되지 않는 starvation이 발생합니다.

/* ET 모드 올바른 읽기 패턴 */
void handle_et_read(int fd) {
    for (;;) {
        ssize_t n = read(fd, buf, sizeof(buf));
        if (n == -1) {
            if (errno == EAGAIN || errno == EWOULDBLOCK)
                break;       /* 모든 데이터 소진 — 정상 종료 */
            perror("read");
            break;
        }
        if (n == 0) {            /* 연결 종료 */
            close(fd);
            break;
        }
        process_data(buf, n);
    }
}

고급 이벤트 플래그

플래그 도입 버전 설명
EPOLLIN 2.5.44 읽기 가능 (데이터 도착 또는 FIN 수신)
EPOLLOUT 2.5.44 쓰기 가능 (송신 버퍼 여유)
EPOLLRDHUP 2.6.17 상대방이 연결을 반만 닫음 (shutdown(SHUT_WR)) — EPOLLIN+read()=0보다 명확
EPOLLHUP 2.5.44 연결 완전 종료 (자동 설정, 등록 불필요)
EPOLLERR 2.5.44 에러 발생 (자동 설정, 등록 불필요)
EPOLLET 2.5.44 Edge-Triggered 모드 활성화
EPOLLONESHOT 2.6.2 이벤트 1회 전달 후 자동 비활성화 — 재활성화는 epoll_ctl(MOD)
EPOLLEXCLUSIVE 4.5 여러 epoll이 같은 fd를 감시할 때 하나만 깨움 (thundering herd 방지)
EPOLLWAKEUP 3.5 이벤트 처리 중 시스템 suspend 방지 (wakeup source 유지)

EPOLLONESHOT과 멀티스레드 패턴

EPOLLONESHOT은 멀티스레드 서버에서 하나의 fd를 여러 스레드가 동시에 처리하는 것을 방지합니다. 이벤트가 발생하면 해당 fd의 이벤트 감시가 자동으로 비활성화되어, 정확히 하나의 스레드만 처리하게 됩니다.

/* 멀티스레드 epoll 서버 — EPOLLONESHOT 패턴 */
void *worker_thread(void *arg) {
    int epfd = *(int *)arg;
    struct epoll_event events[32];

    for (;;) {
        int n = epoll_wait(epfd, events, 32, -1);
        for (int i = 0; i < n; i++) {
            int fd = events[i].data.fd;

            /* 이 시점에서 fd의 이벤트 감시는 이미 비활성화 */
            /* 다른 스레드는 이 fd의 이벤트를 받지 않음 */
            handle_et_read(fd);

            /* 처리 완료 후 재활성화 */
            struct epoll_event ev = {
                .events = EPOLLIN | EPOLLET | EPOLLONESHOT,
                .data.fd = fd
            };
            epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev);
        }
    }
}

/* Nginx-style: 여러 워커 프로세스가 listen fd를 공유할 때 */
/* EPOLLEXCLUSIVE로 thundering herd 방지 (커널 4.5+) */
struct epoll_event ev = {
    .events = EPOLLIN | EPOLLEXCLUSIVE,
    .data.fd = listen_fd
};
epoll_ctl(epfd, EPOLL_CTL_ADD, listen_fd, &ev);

epoll_pwait / epoll_pwait2

epoll_pwait()은 시그널 마스크를 원자적으로 설정하면서 이벤트를 대기하여, epoll_waitsigprocmask 사이의 경쟁 조건(Race Condition)을 방지합니다. epoll_pwait2()(Linux 5.11+)는 타임아웃을 struct timespec으로 지정하여 나노초 정밀도를 제공합니다.

/* epoll_pwait: 시그널 안전한 이벤트 대기 */
sigset_t sigmask;
sigemptyset(&sigmask);
sigaddset(&sigmask, SIGINT);

/* SIGINT를 차단한 상태로 이벤트 대기 */
/* → 반환 후 원래 시그널 마스크 자동 복원 */
int n = epoll_pwait(epfd, events, 64, -1, &sigmask);

/* epoll_pwait2: 나노초 정밀도 타임아웃 (5.11+) */
struct timespec ts = { .tv_sec = 0, .tv_nsec = 500000000 };  /* 500ms */
int n = epoll_pwait2(epfd, events, 64, &ts, NULL);

실전 패턴: ET 모드 이벤트 루프 서버

#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <unistd.h>

#define MAX_EVENTS 1024

static void set_nonblock(int fd) {
    int flags = fcntl(fd, F_GETFL);
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

int main(void) {
    /* listen 소켓 생성 */
    int lfd = socket(AF_INET, SOCK_STREAM | SOCK_NONBLOCK, 0);
    int opt = 1;
    setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR | SO_REUSEPORT, &opt, sizeof(opt));

    struct sockaddr_in addr = {
        .sin_family = AF_INET,
        .sin_port = htons(8080),
        .sin_addr.s_addr = INADDR_ANY
    };
    bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
    listen(lfd, SOMAXCONN);

    /* epoll 인스턴스 생성 + listen fd 등록 */
    int epfd = epoll_create1(EPOLL_CLOEXEC);
    struct epoll_event ev = { .events = EPOLLIN, .data.fd = lfd };
    epoll_ctl(epfd, EPOLL_CTL_ADD, lfd, &ev);

    struct epoll_event events[MAX_EVENTS];
    for (;;) {
        int nfds = epoll_wait(epfd, events, MAX_EVENTS, -1);

        for (int i = 0; i < nfds; i++) {
            int fd = events[i].data.fd;

            if (fd == lfd) {
                /* 새 연결 수락 (ET: 모두 accept) */
                for (;;) {
                    int cfd = accept4(lfd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC);
                    if (cfd == -1) break;
                    ev.events = EPOLLIN | EPOLLET | EPOLLRDHUP;
                    ev.data.fd = cfd;
                    epoll_ctl(epfd, EPOLL_CTL_ADD, cfd, &ev);
                }
            } else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR)) {
                /* 연결 종료 또는 에러 */
                epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
                close(fd);
            } else if (events[i].events & EPOLLIN) {
                /* 데이터 수신 (ET: EAGAIN까지 읽기) */
                handle_et_read(fd);
            }
        }
    }
}

epoll 주의 사항과 함정

☢️

dup/fork와 epoll: epoll은 fd가 아니라 파일 디스크립션(struct file) 단위로 이벤트를 감시합니다. dup()이나 fork()로 fd가 복제되면, 원본 fd를 close()해도 복제된 fd가 남아있는 한 epoll 모니터링이 계속됩니다. 모든 복제본이 닫혀야 epoll에서 자동 해제됩니다.

함정 증상 해결책
ET + 불완전 읽기 이벤트 누락, 데이터 정체 EAGAIN까지 반드시 루프 읽기
close(fd)DEL 누락 dup/fork 시 좀비 모니터링 close() 전 항상 EPOLL_CTL_DEL
LT + EPOLLOUT 상시 등록 CPU 100% (항상 쓰기 가능 이벤트 발생) 쓸 데이터가 있을 때만 EPOLLOUT 활성화
스레드 간 fd 경합 같은 이벤트를 여러 스레드가 처리 EPOLLONESHOT 또는 EPOLLEXCLUSIVE 사용
epoll fd 누수 fd 테이블 고갈 EPOLL_CLOEXEC 사용, 정리 코드 필수
중첩 epoll 순환 참조 커널 교착 가능 epoll fd를 다른 epoll에 등록 시 순환 검사 (ep_loop_check)

epoll 커널 튜닝 파라미터

# 사용자당 최대 epoll watch 수 (기본값: 약 400,000~800,000)
cat /proc/sys/fs/epoll/max_user_watches

# 각 watch는 약 90바이트(struct epitem) + 20바이트(struct eppoll_entry)
# → 100만 watch ≈ 약 100MB 커널 메모리

# ulimit -n (fd 상한)도 함께 조정 필요
ulimit -n 1048576

# /etc/security/limits.conf 영구 설정
# *  soft  nofile  1048576
# *  hard  nofile  1048576
💡

io_uring과의 관계: epoll은 이벤트 준비 상태를 알려주고 실제 I/O는 별도 시스템 콜이 필요하지만, io_uring은 I/O 제출과 완료를 공유 메모리 링으로 처리하여 시스템 콜 자체를 최소화합니다. 네트워크 소켓 이벤트 다중화에는 epoll이 여전히 표준이며, 파일 I/O와 네트워크를 통합해야 하는 고성능 시나리오에서는 io_uring을 고려하세요. 상세 비교는 io_uring vs epoll을 참조하세요.

Netlink는 커널과 유저 공간 프로세스, 또는 유저 공간 프로세스 간 통신을 위한 소켓 기반 IPC입니다. AF_NETLINK 주소 패밀리를 사용하며, 라우팅 테이블(Routing Table) 관리(NETLINK_ROUTE), 방화벽(Firewall) 설정(NETLINK_NETFILTER), 감사 로그(NETLINK_AUDIT) 등에 광범위하게 사용됩니다.

/* Netlink 메시지 헤더 */
struct nlmsghdr {
    __u32 nlmsg_len;     /* 전체 메시지 길이 */
    __u16 nlmsg_type;    /* 메시지 타입 */
    __u16 nlmsg_flags;   /* 플래그 (NLM_F_REQUEST, NLM_F_DUMP, ...) */
    __u32 nlmsg_seq;     /* 시퀀스 번호 */
    __u32 nlmsg_pid;     /* 포트 ID */
};

/* 주요 Netlink 프로토콜 패밀리 */
/* NETLINK_ROUTE    - 라우팅, 링크, 주소 관리 (ip 명령어) */
/* NETLINK_NETFILTER - nftables, conntrack */
/* NETLINK_KOBJECT_UEVENT - udev 이벤트 */
/* NETLINK_AUDIT    - 감사 서브시스템 */
/* NETLINK_GENERIC  - Generic Netlink (확장 가능) */
ℹ️

상세 정보: Netlink 소켓의 아키텍처, rtnetlink, genetlink 등 심층 내용은 네트워크 스택(Network Stack) 문서에서 다룹니다.

Netlink 소켓은 표준 소켓 API로 생성하며, struct sockaddr_nl로 바인딩합니다. nl_pid는 유저 공간 소켓의 포트 ID(일반적으로 프로세스 PID), nl_groups는 멀티캐스트(Multicast) 그룹 비트마스크입니다.

#include <linux/netlink.h>
#include <sys/socket.h>

/* Netlink 주소 구조체 */
struct sockaddr_nl {
    __kernel_sa_family_t nl_family;  /* AF_NETLINK */
    unsigned short       nl_pad;     /* 패딩 (0) */
    __u32                nl_pid;     /* 포트 ID (0=커널, 보통 getpid()) */
    __u32                nl_groups;  /* 멀티캐스트 그룹 비트마스크 */
};

/* 소켓 생성 — NETLINK_ROUTE로 라우팅/링크 관리 */
int fd = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);

/* 유니캐스트 바인딩 — 커널과 1:1 통신 */
struct sockaddr_nl sa = {
    .nl_family = AF_NETLINK,
    .nl_pid    = getpid(),  /* 포트 ID (유일해야 함) */
    .nl_groups = 0          /* 멀티캐스트 미구독 */
};
bind(fd, (struct sockaddr *)&sa, sizeof(sa));

/* 멀티캐스트 바인딩 — 네트워크 링크 변경 이벤트 수신 */
struct sockaddr_nl sa_mc = {
    .nl_family = AF_NETLINK,
    .nl_pid    = 0,
    .nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR  /* 링크 + IPv4 주소 변경 */
};
bind(fd, (struct sockaddr *)&sa_mc, sizeof(sa_mc));

Netlink 메시지는 nlmsghdr 헤더 뒤에 프로토콜별 페이로드(Payload)가 따르고, 페이로드 내부에는 TLV(Type-Length-Value) 형식의 nlattr 속성들이 중첩(Nested)될 수 있습니다. 모든 영역은 NLMSG_ALIGN(4) 경계로 정렬됩니다.

Netlink 메시지 레이아웃:

┌──────────────────────────────────────────────────────────────┐
│ nlmsghdr (16 bytes)                                          │
│   nlmsg_len | nlmsg_type | nlmsg_flags | nlmsg_seq | pid   │
├──────────────────────────────────────────────────────────────┤
│ [padding to NLMSG_ALIGN]                                    │
├──────────────────────────────────────────────────────────────┤
│ Protocol Header (예: struct ifinfomsg, struct rtmsg)        │
├──────────────────────────────────────────────────────────────┤
│ [padding to NLMSG_ALIGN]                                    │
├──────────────────────────────────────────────────────────────┤
│ Attributes (nlattr TLV)                                     │
│  ┌─────────────────────────────────────────────────┐        │
│  │ nla_len (2B) | nla_type (2B) | payload | [pad]  │        │
│  ├─────────────────────────────────────────────────┤        │
│  │ nla_len | nla_type | nested attrs...    | [pad]  │        │
│  └─────────────────────────────────────────────────┘        │
└──────────────────────────────────────────────────────────────┘
/* Netlink 속성 (TLV) */
struct nlattr {
    __u16 nla_len;   /* 전체 속성 길이 (헤더 + 페이로드) */
    __u16 nla_type;  /* 속성 타입 (프로토콜별 정의) */
    /* 페이로드 데이터가 이어짐 */
};

/* 정렬 및 헬퍼 매크로 */
#define NLA_ALIGNTO    4
#define NLA_ALIGN(len) (((len) + NLA_ALIGNTO - 1) & ~(NLA_ALIGNTO - 1))
#define NLA_HDRLEN     ((int) NLA_ALIGN(sizeof(struct nlattr)))  /* = 4 */

/* 메시지 구성 헬퍼 매크로 */
#define NLMSG_ALIGN(len)  (((len) + 3) & ~3)
#define NLMSG_HDRLEN      ((int) NLMSG_ALIGN(sizeof(struct nlmsghdr)))
#define NLMSG_DATA(nlh)   ((void *)((char *)(nlh) + NLMSG_HDRLEN))
#define NLMSG_NEXT(nlh, len) ...  /* 다음 메시지로 이동 */

/* 속성 순회 매크로 */
#define nla_for_each_attr(pos, head, len, rem) \
    for (pos = head, rem = len; \
         nla_ok(pos, rem); \
         pos = nla_next(pos, &rem))

기존 Netlink 프로토콜 패밀리(NETLINK_ROUTE, NETLINK_NETFILTER 등)는 헤더 파일에 하드코딩된 상수로 정의되어 있어 최대 32개까지만 등록할 수 있습니다. Generic Netlink(genetlink)는 이 제한을 극복하기 위해 단일 NETLINK_GENERIC 패밀리 위에 동적 패밀리 등록을 지원합니다.

/* include/net/genetlink.h */
struct genl_family {
    int                      id;        /* 동적 할당 패밀리 ID (커널이 부여) */
    unsigned int             hdrsize;   /* 사용자 헤더 크기 */
    char                     name[GENL_NAMSIZ]; /* 패밀리 이름 */
    unsigned int             version;   /* 프로토콜 버전 */
    unsigned int             maxattr;   /* 최대 속성 번호 */
    const struct nla_policy  *policy;    /* 속성 검증 정책 */
    const struct genl_ops   *ops;       /* 명령 핸들러 배열 */
    unsigned int             n_ops;     /* ops 배열 크기 */
    const struct genl_multicast_group *mcgrps;
    unsigned int             n_mcgrps;
    struct module           *module;
};

struct genl_ops {
    int  (*doit)(struct sk_buff *skb, struct genl_info *info);  /* 단일 요청 */
    int  (*dumpit)(struct sk_buff *skb, struct netlink_callback *cb); /* 덤프 */
    u8   cmd;       /* 명령 번호 */
    u8   flags;     /* GENL_ADMIN_PERM 등 */
};

유저 공간에서 genetlink 패밀리를 사용하려면 먼저 패밀리 이름으로 ID를 조회해야 합니다. 컨트롤러 패밀리(GENL_ID_CTRL)에 CTRL_CMD_GETFAMILY 명령을 보내면 커널이 해당 패밀리의 동적 ID를 반환합니다. nl80211(WiFi), taskstats(태스크 통계), TASKSTATS 등이 genetlink의 대표적 사용 예입니다.

/* 커널 모듈에서 genetlink 패밀리 등록 예제 (스켈레톤) */
#include <net/genetlink.h>

/* 명령 정의 */
enum {
    MY_CMD_UNSPEC,
    MY_CMD_ECHO,      /* 에코 명령 */
    __MY_CMD_MAX,
};

/* 속성 정의 */
enum {
    MY_ATTR_UNSPEC,
    MY_ATTR_MSG,       /* NLA_STRING 타입 메시지 */
    __MY_ATTR_MAX,
};

static const struct nla_policy my_policy[__MY_ATTR_MAX] = {
    [MY_ATTR_MSG] = { .type = NLA_STRING, .len = 256 },
};

static int my_echo_doit(struct sk_buff *skb, struct genl_info *info)
{
    struct sk_buff *reply;
    void *hdr;

    if (!info->attrs[MY_ATTR_MSG])
        return -EINVAL;

    reply = genlmsg_new(NLMSG_DEFAULT_SIZE, GFP_KERNEL);
    hdr = genlmsg_put_reply(reply, info, &my_family, 0, MY_CMD_ECHO);
    nla_put_string(reply, MY_ATTR_MSG, nla_data(info->attrs[MY_ATTR_MSG]));
    genlmsg_end(reply, hdr);
    return genlmsg_reply(reply, info);
}

static const struct genl_ops my_ops[] = {
    {
        .cmd    = MY_CMD_ECHO,
        .doit   = my_echo_doit,
        .flags  = 0,
    },
};

static struct genl_family my_family = {
    .name    = "MY_GENL",
    .version = 1,
    .maxattr = __MY_ATTR_MAX - 1,
    .policy  = my_policy,
    .ops     = my_ops,
    .n_ops   = ARRAY_SIZE(my_ops),
    .module  = THIS_MODULE,
};

/* 모듈 초기화 시 */
genl_register_family(&my_family);
/* 모듈 해제 시 */
genl_unregister_family(&my_family);

Netlink 멀티캐스트를 통해 커널은 이벤트를 여러 유저 공간 프로세스에 동시에 브로드캐스트(Broadcast)할 수 있습니다. 대표적으로 NETLINK_KOBJECT_UEVENT는 udev에 디바이스 핫플러그(Hotplug) 이벤트를 전달하며, RTNLGRP_LINK는 네트워크 인터페이스 상태 변경을 알립니다.

멀티캐스트 그룹 패밀리 이벤트 내용 대표 수신자
RTNLGRP_LINK NETLINK_ROUTE 인터페이스 up/down, 생성/삭제 NetworkManager, systemd-networkd
RTNLGRP_IPV4_IFADDR NETLINK_ROUTE IPv4 주소 추가/삭제 ip monitor, dhclient
RTNLGRP_IPV6_IFADDR NETLINK_ROUTE IPv6 주소 추가/삭제 radvd, NetworkManager
RTNLGRP_IPV4_ROUTE NETLINK_ROUTE IPv4 라우팅 변경 ip monitor, bird
KOBJECT_UEVENT NETLINK_KOBJECT_UEVENT 디바이스 추가/제거 udevd, mdev
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/rtnetlink.h>
#include <net/if.h>

/* 네트워크 링크 변경 모니터링 예제 */
int main(void) {
    int fd = socket(AF_NETLINK, SOCK_DGRAM, NETLINK_ROUTE);

    struct sockaddr_nl sa = {
        .nl_family = AF_NETLINK,
        .nl_groups = RTMGRP_LINK | RTMGRP_IPV4_IFADDR
    };
    bind(fd, (struct sockaddr *)&sa, sizeof(sa));

    char buf[8192];
    for (;;) {
        ssize_t len = recv(fd, buf, sizeof(buf), 0);
        struct nlmsghdr *nh = (struct nlmsghdr *)buf;

        for (; NLMSG_OK(nh, len); nh = NLMSG_NEXT(nh, len)) {
            if (nh->nlmsg_type == RTM_NEWLINK || nh->nlmsg_type == RTM_DELLINK) {
                struct ifinfomsg *ifi = NLMSG_DATA(nh);
                char name[IF_NAMESIZE];
                if_indextoname(ifi->ifi_index, name);
                printf("링크 %s: %s (flags=0x%x)\n",
                       nh->nlmsg_type == RTM_NEWLINK ? "UP" : "DOWN",
                       name, ifi->ifi_flags);
            }
            if (nh->nlmsg_type == RTM_NEWADDR || nh->nlmsg_type == RTM_DELADDR) {
                printf("주소 변경 감지 (type=%d)\n", nh->nlmsg_type);
            }
        }
    }
    close(fd);
}
Netlink 아키텍처 유저 공간 (User Space) ip / ss iw (WiFi) udevd tc (QoS) nft / iptables 사용자 앱 socket(AF_NETLINK, SOCK_DGRAM, NETLINK_*) 커널 공간 (Kernel Space) Netlink Core (af_netlink.c) 메시지 라우팅, 멀티캐스트 분배 rtnetlink NETLINK_ROUTE genetlink NETLINK_GENERIC nfnetlink NETLINK_NETFILTER uevent KOBJECT_UEVENT audit NETLINK_AUDIT 멀티캐스트 그룹 RTNLGRP_LINK | RTNLGRP_IPV4_IFADDR KOBJECT_UEVENT → udevd 브로드캐스트

Unix Domain Sockets

Unix Domain Socket(AF_UNIX / AF_LOCAL)은 같은 호스트 내 프로세스 간 통신에 최적화된 소켓 기반 IPC입니다. TCP/IP 프로토콜 스택(체크섬(Checksum), 라우팅(Routing), 시퀀스 번호 등)을 완전히 우회하여 커널 내부에서 직접 sk_buff를 전달하므로, localhost TCP 대비 2~3배 높은 처리량(Throughput)과 현저히 낮은 레이턴시를 제공합니다.

세 가지 소켓 타입을 지원합니다:

일반 소켓 API 외에도 SCM_RIGHTS(파일 디스크립터 전달)와 SCM_CREDENTIALS(프로세스 자격 증명 전달) 같은 고유 기능을 제공하며, systemd 소켓 활성화(Socket Activation), 컨테이너 런타임(containerd, CRI-O), 데이터베이스(PostgreSQL, MySQL) 등에서 핵심 통신 채널로 사용됩니다.

상세 문서: 커널 자료구조(struct unix_sock), 주소 유형(pathname/abstract/socketpair) 비교, SCM_RIGHTS/SCM_CREDENTIALS 구현, 가비지 컬렉터(GC), 성능 최적화, 컨테이너 환경 활용 등은 Unix Domain Socket 페이지에서 상세히 다룹니다.

Cross Memory Attach

process_vm_readv()process_vm_writev()는 Linux 3.2에 도입된 시스템 콜로, 한 프로세스가 다른 프로세스의 메모리를 직접 읽거나 쓸 수 있게 합니다. 커널 내부에서 단일 복사로 처리되어 공유 메모리나 pipe를 경유하는 것보다 효율적이며, 디버거(Debugger), 체크포인트/복원(CRIU), 고성능 MPI 라이브러리 등에서 사용됩니다.

#include <sys/uio.h>

/* process_vm_readv — 원격 프로세스 메모리 읽기 */
ssize_t process_vm_readv(
    pid_t                  pid,          /* 대상 프로세스 PID */
    const struct iovec   *local_iov,    /* 로컬 scatter 버퍼 */
    unsigned long          liovcnt,      /* 로컬 iovec 수 */
    const struct iovec   *remote_iov,   /* 원격 gather 버퍼 */
    unsigned long          riovcnt,      /* 원격 iovec 수 */
    unsigned long          flags         /* 현재 0 */
);

/* process_vm_writev — 원격 프로세스 메모리 쓰기 */
ssize_t process_vm_writev(
    pid_t pid,
    const struct iovec *local_iov, unsigned long liovcnt,
    const struct iovec *remote_iov, unsigned long riovcnt,
    unsigned long flags
);
#include <sys/uio.h>
#include <stdio.h>
#include <string.h>

/* 대상 프로세스(pid)의 remote_addr에서 데이터 읽기 예제 */
int read_remote_memory(pid_t pid, void *remote_addr, size_t len)
{
    char buf[4096];
    struct iovec local  = { .iov_base = buf,         .iov_len = len };
    struct iovec remote = { .iov_base = remote_addr, .iov_len = len };

    ssize_t nread = process_vm_readv(pid, &local, 1, &remote, 1, 0);
    if (nread < 0) {
        perror("process_vm_readv");
        return -1;
    }
    printf("원격 프로세스에서 %zd 바이트 읽기 완료\n", nread);
    return 0;
}

/* scatter-gather: 여러 영역을 한 번의 호출로 읽기 */
void scatter_read(pid_t pid)
{
    char buf1[128], buf2[256];
    struct iovec local[2]  = {
        { buf1, sizeof(buf1) },
        { buf2, sizeof(buf2) }
    };
    struct iovec remote[2] = {
        { (void *)0x7fff00001000, 128 },
        { (void *)0x7fff00002000, 256 }
    };
    process_vm_readv(pid, local, 2, remote, 2, 0);
}
⚠️

보안 요구사항: process_vm_readv()/process_vm_writev()PTRACE_MODE_ATTACH 권한 검사를 수행합니다. 호출자와 대상이 같은 UID이거나 호출자가 CAP_SYS_PTRACE 능력(Capability)을 가져야 합니다. Yama LSM이 활성화된 경우 /proc/sys/kernel/yama/ptrace_scope 설정도 확인됩니다.

memfd_create

memfd_create()는 파일시스템 경로 없이 RAM에 백업되는 익명 파일(Anonymous File)을 생성합니다. tmpfs 기반으로 동작하며, mmap()으로 공유 메모리를 설정하거나 다른 프로세스에 SCM_RIGHTS로 fd를 전달하여 익명 공유 메모리를 구현할 수 있습니다. 특히 sealing 메커니즘으로 파일 크기나 내용의 변경을 금지할 수 있어 안전한 IPC에 적합합니다.

#define _GNU_SOURCE
#include <sys/mman.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <linux/memfd.h>

int main(void) {
    /* 1. 익명 파일 생성 (MFD_ALLOW_SEALING으로 seal 허용) */
    int fd = memfd_create("shared-buffer", MFD_CLOEXEC | MFD_ALLOW_SEALING);

    /* 2. 크기 설정 */
    ftruncate(fd, 4096);

    /* 3. 메모리 매핑하여 데이터 기록 */
    char *ptr = mmap(NULL, 4096, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
    strcpy(ptr, "Hello from memfd!");

    /* 4. Sealing — 쓰기 완료 후 내용 변경 금지 */
    fcntl(fd, F_ADD_SEALS,
          F_SEAL_WRITE |   /* 쓰기 금지 */
          F_SEAL_SHRINK |  /* 축소 금지 */
          F_SEAL_GROW |    /* 확장 금지 */
          F_SEAL_SEAL);    /* 추가 seal 금지 (불변) */

    /* 이제 fd를 다른 프로세스에 전달 (SCM_RIGHTS 또는 /proc/PID/fd/N)
     * 수신자는 내용이 변경되지 않음을 보장받음 */

    /* 5. seal 상태 확인 */
    int seals = fcntl(fd, F_GET_SEALS);
    printf("Seals: 0x%x\n", seals);
    printf("  WRITE:  %s\n", (seals & F_SEAL_WRITE)  ? "yes" : "no");
    printf("  SHRINK: %s\n", (seals & F_SEAL_SHRINK) ? "yes" : "no");
    printf("  GROW:   %s\n", (seals & F_SEAL_GROW)   ? "yes" : "no");

    munmap(ptr, 4096);
    close(fd);
}
Seal 플래그 효과 용도
F_SEAL_SEAL 추가 seal 설정 금지 seal 상태를 불변(Immutable)으로 만듦
F_SEAL_SHRINK ftruncate()로 크기 축소 금지 매핑 무효화(SIGBUS) 방지
F_SEAL_GROW ftruncate()/write()로 크기 확장 금지 메모리 사용량 예측 가능
F_SEAL_WRITE 모든 쓰기 금지 (mmap PROT_WRITE 포함) IPC 버퍼 불변 보장
F_SEAL_FUTURE_WRITE 새로운 쓰기 매핑 금지 (기존 매핑은 허용) 점진적 잠금
💡

memfd_secret() (v5.14+): memfd_secret()는 한 단계 더 나아가 커널 직접 매핑(direct map)에서 해당 메모리를 제거합니다. 커널조차 해당 페이지에 접근할 수 없으므로 비밀 키(Secret Key), 암호화 자료 등을 보호하는 데 사용됩니다. 단, 커널 직접 매핑 수정은 TLB 플러시 비용이 크므로 성능에 민감한 대량 데이터에는 부적합합니다.

ℹ️

활용 사례: Wayland는 memfd + sealing으로 컴포지터(Compositor)와 클라이언트 간 그래픽 버퍼를 공유합니다. 클라이언트가 렌더링 완료 후 F_SEAL_WRITE를 설정하면 컴포지터는 버퍼가 변경되지 않음을 보장받아 안전하게 합성할 수 있습니다. 샌드박스 환경에서도 파일시스템 접근 없이 메모리를 공유할 수 있어 유용합니다.

pidfd — 레이스 프리 프로세스 관리

pidfd는 Linux 5.2+에 도입된 프로세스를 가리키는 파일 디스크립터입니다. 전통적인 PID 기반 API(kill(), waitpid())는 PID 재사용(Recycle) 레이스(Race) 문제가 있습니다 — 프로세스가 종료되고 같은 PID가 다른 프로세스에 재할당되면 잘못된 프로세스에 시그널을 보낼 수 있습니다. pidfd는 파일 디스크립터의 참조 카운팅으로 이 문제를 근본적으로 해결합니다.

#define _GNU_SOURCE
#include <sys/pidfd.h>
#include <sys/wait.h>
#include <sys/syscall.h>
#include <signal.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>

int main(void) {
    pid_t child = fork();
    if (child == 0) {
        sleep(5);
        return 42;
    }

    /* 1. pidfd_open — PID를 fd로 변환 (레이스 프리) */
    int pidfd = pidfd_open(child, 0);
    /* pidfd는 프로세스가 종료되어도 유효 (좀비 상태까지 참조) */

    /* 2. pidfd_send_signal — 안전한 시그널 전달 */
    /*    PID가 재사용되어도 원래 프로세스에만 전달됨 */
    pidfd_send_signal(pidfd, SIGCONT, NULL, 0);

    /* 3. epoll에 등록하여 비동기 종료 감지 */
    int epfd = epoll_create1(EPOLL_CLOEXEC);
    struct epoll_event ev = { .events = EPOLLIN, .data.fd = pidfd };
    epoll_ctl(epfd, EPOLL_CTL_ADD, pidfd, &ev);

    struct epoll_event events[1];
    epoll_wait(epfd, events, 1, -1);  /* 자식 종료 대기 */

    /* 4. waitid(P_PIDFD) — pidfd로 종료 상태 수확 */
    siginfo_t si;
    waitid(P_PIDFD, pidfd, &si, WEXITED);
    printf("자식 종료: PID=%d, 상태=%d\n", si.si_pid, si.si_status);

    /* 5. pidfd_getfd — 다른 프로세스의 fd를 복제 (Linux 5.6+) */
    /*    대상 프로세스의 fd 3을 현재 프로세스로 가져옴 */
    /* int stolen_fd = pidfd_getfd(pidfd, 3, 0); */

    close(pidfd);
    close(epfd);
}
API 도입 버전 기능
pidfd_open(pid, flags) Linux 5.3 기존 PID를 pidfd로 변환
pidfd_send_signal(pidfd, sig, info, flags) Linux 5.1 레이스 프리 시그널 전달
pidfd_getfd(pidfd, targetfd, flags) Linux 5.6 대상 프로세스의 fd 복제 (dup over process)
waitid(P_PIDFD, pidfd, ...) Linux 5.4 pidfd로 종료 상태 수확
clone3(CLONE_PIDFD) Linux 5.2 fork 시 pidfd를 동시에 생성
💡

활용 사례: systemd는 pidfd를 사용하여 서비스 프로세스를 추적합니다. PID 재사용 레이스 없이 정확한 프로세스에 시그널을 보내고 종료를 감지할 수 있습니다. 컨테이너 런타임(runc, crun)도 pidfd로 컨테이너 init 프로세스를 관리하며, pidfd_getfd()는 디버거나 라이브 마이그레이션 도구에서 타겟 프로세스의 fd를 안전하게 복제하는 데 활용됩니다.

Android Binder IPC

Binder는 Android의 핵심 IPC 메커니즘으로, 커널 드라이버(drivers/android/binder.c)가 구현합니다. 일반 Unix IPC(pipe, UDS 등)와 달리 단일 복사(single-copy) 전송, 호출자 PID/UID 자동 첨부, 참조 카운팅 기반 원격 객체 수명 관리, 사망 통지(death notification) 등 Android에 특화된 기능을 제공합니다.

Binder vs Unix Domain Socket

특성BinderUnix Domain Socket
복사 횟수1회 (mmap 활용)2회 (유저→커널→유저)
호출자 인증커널이 PID/UID 자동 첨부SCM_CREDENTIALS (opt-in)
객체 수명 관리참조 카운팅, 사망 통지없음
통신 모델동기 RPC (기본)스트림 / 데이터그램
보안SELinux Binder 훅파일 권한 + SCM
오버헤드ioctl + mmap 설정 비용단순 소켓 생성
사용 범위Android 전용모든 Unix 계열 OS
Binder 트랜잭션 흐름 (단일 복사) 클라이언트 프로세스 Proxy 객체 (BpBinder) ioctl(BINDER_WRITE_READ) BC_TRANSACTION → 데이터 전송 요청 타겟 프로세스 Stub 객체 (BBinder) ioctl(BINDER_WRITE_READ) BR_TRANSACTION ← 데이터 수신 통지 Binder 드라이버 (drivers/android/binder.c) copy_from_user() binder_buffer 물리 페이지 → mmap 매핑 물리 페이지를 타겟 프로세스의 mmap 영역에 직접 매핑 (추가 복사 없음) 응답 흐름 (Reply) 타겟: BC_REPLY → 드라이버 → 클라이언트: BR_REPLY 동일한 단일 복사 메커니즘으로 응답 전달 ※ UDS: send()→커널 복사→recv()→유저 복사 (2회) | Binder: copy_from_user()→mmap 매핑 (1회)

Binder 단일 복사 메커니즘:

  1. 송신: copy_from_user()로 유저 데이터 → 커널 binder_buffer
  2. 커널이 binder_buffer의 물리 페이지를 수신 측 mmap 영역에 매핑
  3. 수신: mmap된 가상 주소에서 직접 데이터 접근 (추가 복사 없음)

UDS는 send()→커널 버퍼 복사 → recv()→유저 버퍼 복사로 2회 복사가 필요합니다.

Android는 3개의 Binder 도메인을 사용합니다:

Binder 자료구조, 프로토콜, mmap 메커니즘, SELinux 정책 등 내용은 Android 커널 — Binder IPC 섹션을 참고합니다.

참고 링크