QUESTION
EPOLLIN을 받았으면 read가 원하는 길이만큼 반드시 성공하는가?
epoll은 등록한 file의 poll callback과 wait queue를 이용해 상태 변화를 관심 목록에 모은다. EPOLLIN은 read가 적어도 일부 진행되거나 EOF/error를 관찰할 수 있다는 뜻이며 message 전체가 도착했다는 뜻이 아니다.
edge-triggered mode에서는 준비되지 않음에서 준비됨으로 바뀌는 edge를 놓치지 않도록 fd를 nonblocking으로 만들고 EAGAIN이 나올 때까지 drain한다. 일부만 읽고 event loop로 돌아가면 buffer에 데이터가 남아도 새 edge가 오지 않을 수 있다.
STRUCTURE
구조 그림
interest set은 등록 관계를 보관하고 ready list는 현재 전달 후보를 보관한다. 하나의 fd가 여러 epoll instance에 등록될 수도 있다.
CALL PATH
호출 흐름
readiness 관찰과 실제 I/O 사이에는 다른 thread/process가 상태를 바꿀 수 있다. 모든 syscall 반환값을 다시 판정해야 한다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| fs/eventpoll.c | do_epoll_ctl(), ep_poll_callback() | interest item과 target wait queue 연결 |
| fs/eventpoll.c | do_epoll_wait(), ep_send_events() | ready list를 userspace event array로 복사 |
| fs/pipe.c | pipe_poll(), pipe_read() | readiness와 실제 read 결과를 비교하기 쉬운 target |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 epoll_pipe.c -o epoll_pipe01#define _GNU_SOURCE
02#include <errno.h>
03#include <fcntl.h>
04#include <stdio.h>
05#include <sys/epoll.h>
06#include <unistd.h>
07
08int main(void)
09{
10 int p[2];
11 if (pipe2(p, O_NONBLOCK | O_CLOEXEC) < 0)
12 return 1;
13 int ep = epoll_create1(EPOLL_CLOEXEC);
14 struct epoll_event add = { .events = EPOLLIN | EPOLLET, .data.fd = p[0] };
15 if (ep < 0 || epoll_ctl(ep, EPOLL_CTL_ADD, p[0], &add) < 0)
16 return 1;
17
18 if (write(p[1], "abcdef", 6) != 6)
19 return 1;
20 struct epoll_event event;
21 if (epoll_wait(ep, &event, 1, -1) != 1)
22 return 1;
23
24 char buffer[4];
25 for (;;) {
26 ssize_t n = read(p[0], buffer, sizeof(buffer));
27 if (n > 0)
28 fwrite(buffer, 1, (size_t)n, stdout);
29 else if (n < 0 && errno == EINTR)
30 continue;
31 else if (n < 0 && errno == EAGAIN)
32 break;
33 else
34 return n < 0;
35 }
36 putchar('\n');
37 close(p[0]); close(p[1]); close(ep);
38 return 0;
39}
CODE NOTES
코드 조각별 설명
O_NONBLOCK | O_CLOEXECEPOLLET drain loop가 마지막 read에서 잠들지 않고 EAGAIN으로 event loop에 제어를 돌려주게 한다.
EPOLLIN | EPOLLETreadable 상태의 level 반복이 아니라 상태 변화 edge를 요청한다. one-shot과는 다른 flag다.
epoll_wait(epready event를 최대 한 개 가져오지만 target 상태를 잠그지는 않는다. event.data에는 등록 때 넣은 application key가 돌아온다.
char buffer[4]6 byte보다 작은 buffer로 여러 read가 필요한 상황을 일부러 만든다.
errno == EAGAIN현재 pipe를 모두 비운 완료 조건이다. 다음 producer write가 새 edge를 만들 수 있다.
DETAILS
세부 동작
level, edge, one-shot을 구분한다
level-triggered는 상태가 계속 ready이면 epoll_wait마다 다시 보고할 수 있다. edge-triggered는 변화 시점을 줄여 event 수를 줄이지만 drain 의무가 생긴다. EPOLLONESHOT은 한 event 뒤 명시적 MOD로 rearm할 때까지 비활성화한다.
worker pool이 같은 fd를 처리할 때 one-shot으로 동시 소비를 제한할 수 있다.
HUP와 ERR는 데이터와 함께 올 수 있다
peer close로 EPOLLRDHUP/HUP가 와도 socket receive buffer에 마지막 데이터가 남아 있을 수 있다. HUP를 보자마자 fd를 닫지 말고 read가 0이 될 때까지 처리한다.
ERR/HUP는 interest mask에 넣지 않아도 보고될 수 있으므로 항상 분기한다.
fd 번호 재사용과 event data
close한 fd 번호가 새 file에 재사용될 수 있다. 비동기 event object에 숫자 fd만 저장하면 stale event가 새 connection state에 잘못 적용될 수 있다.
generation을 포함한 connection object pointer/index를 data.u64에 넣고 close/reuse protocol을 둔다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
eventpoll | epoll_create1에서 생기고 epoll fd close에서 해제된다 | interest tree, ready list, wait queue |
epitem | EPOLL_CTL_ADD에서 target file과 연결되고 DEL/close에서 제거된다 | event mask, user data |
ready event | callback에서 ready list에 연결되고 epoll_wait가 전달한다 | level 재검사, one-shot state |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| EPOLLET 연결이 멈춤 | EAGAIN까지 drain하지 않음 | 마지막 read/write 반환값 |
| EOF data 손실 | HUP에서 즉시 close | read 0 전 남은 buffer 확인 |
| 엉뚱한 connection 처리 | fd 재사용 stale event | generation/owner와 close 순서 |
LAB
직접 확인
- read loop를 한 번만 읽도록 바꿔 남은 2 byte에 새 EPOLLET event가 오지 않는지 확인한다.
- EPOLLET를 제거해 level-triggered에서 남은 데이터가 다시 보고되는지 비교한다.
- socketpair와 EPOLLRDHUP를 사용해 peer close 전 전송한 마지막 data와 HUP 순서를 기록한다.
./epoll_pipestrace -e trace=pipe2,epoll_ctl,epoll_wait,read,write,close ./epoll_pipePRIMARY REFERENCES