현대 리눅스 IPC 메커니즘
Linux 커널의 현대적 IPC 메커니즘을 심층 분석합니다. eventfd/signalfd/timerfd를 epoll과 통합한 fd 기반 이벤트 처리, Netlink 소켓을 통한 커널-유저 공간 제어 채널, Unix Domain Socket의 고성능 로컬 통신, process_vm_readv()/process_vm_writev()를 사용한 Cross Memory Attach, memfd_create()의 익명 공유 메모리와 sealing, pidfd의 레이스 프리 프로세스 관리, 그리고 Android Binder의 단일 복사 RPC까지 다룹니다.
핵심 요약
- eventfd / signalfd / timerfd — 다양한 이벤트 소스를 파일 디스크립터로 추상화하여 epoll과 통합할 수 있는 경량 메커니즘입니다.
- epoll — 수만~수십만 개의 fd를 효율적으로 모니터링하는 Linux 전용 이벤트 다중화 인터페이스입니다.
- Netlink — 커널과 유저 공간 간 구조화된 메시지를 교환하는 소켓 기반 제어 채널입니다.
- memfd_create — 파일시스템 경로 없이 RAM 기반 익명 공유 메모리를 생성하며, sealing으로 불변성을 보장합니다.
- pidfd — PID 재사용 레이스를 근본적으로 해결하는 파일 디스크립터 기반 프로세스 참조입니다.
단계별 이해
- fd 기반 이벤트 통합 — eventfd, signalfd, timerfd를 하나의 epoll 루프에 등록하면 단일 스레드에서 모든 이벤트를 비동기적으로 처리할 수 있습니다.
- epoll 이벤트 루프 —
epoll_create1()으로 인스턴스를 생성하고,epoll_ctl()로 fd를 등록한 뒤,epoll_wait()로 이벤트를 대기합니다. - Netlink 활용 —
ip,ss,tc등의 네트워크 도구가 내부적으로 Netlink를 사용합니다.AF_NETLINK소켓으로 커널 서브시스템과 직접 통신할 수 있습니다. - 안전한 공유 메모리 —
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은 ioeventfd와 irqfd를 통해 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: 확장 가능한 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의 핵심은 콜백(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_wait와 sigprocmask 사이의 경쟁 조건(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 Sockets
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 소켓 생성과 바인딩
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 메시지 포맷
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))
Generic Netlink (genetlink)
기존 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 멀티캐스트
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);
}
Unix Domain Sockets
Unix Domain Socket(AF_UNIX / AF_LOCAL)은 같은 호스트 내 프로세스 간 통신에 최적화된 소켓 기반 IPC입니다. TCP/IP 프로토콜 스택(체크섬(Checksum), 라우팅(Routing), 시퀀스 번호 등)을 완전히 우회하여 커널 내부에서 직접 sk_buff를 전달하므로, localhost TCP 대비 2~3배 높은 처리량(Throughput)과 현저히 낮은 레이턴시를 제공합니다.
세 가지 소켓 타입을 지원합니다:
SOCK_STREAM— 연결 기반 바이트 스트림 (TCP와 유사)SOCK_DGRAM— 비연결 데이터그램 (UDP와 유사, 단 순서 보장)SOCK_SEQPACKET— 연결 기반 + 메시지 경계 보존
일반 소켓 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
| 특성 | Binder | Unix Domain Socket |
|---|---|---|
| 복사 횟수 | 1회 (mmap 활용) | 2회 (유저→커널→유저) |
| 호출자 인증 | 커널이 PID/UID 자동 첨부 | SCM_CREDENTIALS (opt-in) |
| 객체 수명 관리 | 참조 카운팅, 사망 통지 | 없음 |
| 통신 모델 | 동기 RPC (기본) | 스트림 / 데이터그램 |
| 보안 | SELinux Binder 훅 | 파일 권한 + SCM |
| 오버헤드 | ioctl + mmap 설정 비용 | 단순 소켓 생성 |
| 사용 범위 | Android 전용 | 모든 Unix 계열 OS |
Binder 단일 복사 메커니즘:
- 송신: copy_from_user()로 유저 데이터 → 커널 binder_buffer
- 커널이 binder_buffer의 물리 페이지를 수신 측 mmap 영역에 매핑
- 수신: mmap된 가상 주소에서 직접 데이터 접근 (추가 복사 없음)
UDS는 send()→커널 버퍼 복사 → recv()→유저 버퍼 복사로 2회 복사가 필요합니다.
Android는 3개의 Binder 도메인을 사용합니다:
/dev/binder— Framework ↔ App/dev/hwbinder— Framework ↔ HAL (HIDL/AIDL)/dev/vndbinder— 벤더 내부 IPC
Binder 자료구조, 프로토콜, mmap 메커니즘, SELinux 정책 등 내용은 Android 커널 — Binder IPC 섹션을 참고합니다.
참고 링크
- eventfd(2) — eventfd 파일 디스크립터를 통한 이벤트 알림 메커니즘입니다
- signalfd(2) — 시그널을 파일 디스크립터로 수신하는 인터페이스입니다
- timerfd_create(2) — 타이머 만료를 파일 디스크립터로 전달하는 인터페이스입니다
- epoll(7) — epoll I/O 이벤트 다중화 인터페이스의 동작 모드(ET/LT)를 설명합니다
- netlink(7) — Netlink 소켓 프로토콜 패밀리와 멀티캐스트 그룹을 다룹니다
- unix(7) — Unix 도메인 소켓의 주소 체계, ancillary 데이터, SCM_RIGHTS를 설명합니다
- memfd_create(2) — 익명 파일을 생성하여 프로세스 간 공유하는 시스템 콜입니다
- pidfd_open(2) — PID 파일 디스크립터를 통한 레이스 프리 프로세스 관리 시스템 콜입니다
- process_vm_readv(2) — Cross Memory Attach를 통한 원격 프로세스 메모리 직접 읽기입니다
- LWN: eventfd and its uses — eventfd의 설계 의도와 KVM, VFIO 등에서의 활용 사례를 설명합니다
- LWN: Completing the pidfd API — pidfd 인터페이스의 설계 과정과 레이스 조건 해결 방법을 다룹니다
- LWN: Sealed files — memfd_create()와 파일 실링(sealing) 메커니즘의 보안 모델을 설명합니다
fs/eventfd.c— eventfd 파일 디스크립터의 커널 구현부입니다fs/eventpoll.c— epoll 서브시스템의 커널 구현부입니다net/unix/af_unix.c— Unix 도메인 소켓의 프로토콜 구현부입니다net/netlink/af_netlink.c— Netlink 소켓의 커널 구현부입니다drivers/android/binder.c— Android Binder IPC 드라이버 구현부입니다
관련 문서
- IPC (Inter-Process Communication) - 파이프, 시그널, System V IPC, POSIX IPC, futex 등 전통적 IPC 메커니즘
- Unix Domain Socket - AF_UNIX 커널 구현, SCM_RIGHTS, GC, 성능 최적화
- 네트워크 스택 - Netlink 심층 구현
- io_uring - epoll을 넘어선 차세대 비동기 I/O 인터페이스
- 동기화 기법 - spinlock, mutex, rwlock 등 커널 내부 동기화 프리미티브
- 네임스페이스 - IPC namespace를 포함한 커널 네임스페이스 격리
- 프로세스 관리 - task_struct, 시그널 핸들링, fork/exec