QUESTION
timer event가 여러 번 밀렸을 때 read 한 번은 몇 번의 만료를 뜻하는가?
timerfd read는 event 하나가 아니라 마지막 read 이후 누적된 expiration 횟수를 uint64_t로 반환한다. event loop가 늦게 깨어도 몇 주기를 놓쳤는지 알 수 있다. eventfd도 uint64 counter를 사용해 thread/process의 wakeup credit을 fd readiness로 바꾼다.
둘 다 정확히 8 byte read/write 규칙을 가진다. stream처럼 임의 길이 buffer를 쓰거나 한 번의 readiness를 한 번의 사건으로만 해석하면 누적 정보를 잃는다.
STRUCTURE
구조 그림
event loop가 550 ms 동안 읽지 않으면 100 ms timer 만료가 counter에 누적된다. read 한 번이 누적값을 소비한다.
CALL PATH
호출 흐름
readiness 횟수와 실제 event 횟수를 분리한다. 누적 counter를 읽은 뒤 application이 모든 tick을 따라잡을지 최신 상태만 계산할지 정한다.
함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.
SOURCE COORDINATES
Linux 6.18.37 LTS 소스 위치
glibc 함수에서 멈추지 않고 syscall 구현과 커널 객체가 만나는 파일까지 내려간다. 링크는 동일한 태그의 원본 파일을 가리킨다.
| 파일 | 함수·구조체 | 여기서 볼 것 |
|---|---|---|
| fs/timerfd.c | timerfd_settime(), timerfd_read() | hrtimer 만료 횟수와 8-byte 반환 |
| fs/eventfd.c | eventfd_write(), eventfd_read() | counter 증가/소비와 semaphore mode |
| kernel/time/hrtimer.c | hrtimer_start_range_ns() | monotonic timer scheduling |
COMPLETE PROGRAM
실행 예제 원본
아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.
cc -std=c17 -Wall -Wextra -O2 fd_counters.c -o fd_counters01#define _GNU_SOURCE
02#include <poll.h>
03#include <stdint.h>
04#include <stdio.h>
05#include <sys/timerfd.h>
06#include <time.h>
07#include <unistd.h>
08
09int main(void)
10{
11 int fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
12 if (fd < 0)
13 return 1;
14 struct itimerspec spec = {
15 .it_value = { .tv_sec = 0, .tv_nsec = 100000000 },
16 .it_interval = { .tv_sec = 0, .tv_nsec = 100000000 }
17 };
18 if (timerfd_settime(fd, 0, &spec, NULL) < 0)
19 return 1;
20
21 struct timespec delay = { .tv_sec = 0, .tv_nsec = 550000000 };
22 nanosleep(&delay, NULL);
23 struct pollfd pfd = { .fd = fd, .events = POLLIN };
24 if (poll(&pfd, 1, 0) != 1)
25 return 1;
26
27 uint64_t expirations;
28 if (read(fd, &expirations, sizeof(expirations)) != sizeof(expirations))
29 return 1;
30 printf("expirations=%llu\n", (unsigned long long)expirations);
31 close(fd);
32 return 0;
33}
CODE NOTES
코드 조각별 설명
CLOCK_MONOTONICwall clock 변경과 무관한 경과 시간 timer를 만든다. calendar deadline이면 CLOCK_REALTIME과 cancel-on-set 정책을 검토한다.
.it_interval0이 아니므로 첫 만료 뒤 같은 주기로 재무장되는 periodic timer다.
nanosleep(&delay여러 100ms tick이 쌓이도록 550ms 동안 event를 읽지 않는다.
poll(&pfd, 1, 0)timeout 0으로 현재 readable 여부만 확인한다. counter가 0보다 크면 POLLIN이다.
read(fd, &expirations반드시 uint64_t 크기로 읽고 누적 expiration 수를 얻는다. read 뒤 counter는 현재까지 소비된다.
DETAILS
세부 동작
periodic tick을 모두 실행할지는 application 정책이다
expirations가 100이면 handler를 100번 실행해 catch-up할 수도 있고, 현재 monotonic time으로 상태를 한 번 재계산해 오래된 tick을 건너뛸 수도 있다. 제어 loop와 UI refresh는 보통 다른 선택을 한다.
callback 실행 시간을 interval보다 짧게 유지한다는 전제를 계측한다.
eventfd는 wakeup과 값 전달을 분리한다
eventfd write 값은 counter에 더해지고 일반 read는 누적 전체를 반환해 0으로 만든다. EFD_SEMAPHORE mode에서는 read마다 1을 반환하고 counter를 1 줄인다.
복잡한 payload는 별도 queue에 넣고 eventfd는 queue가 비지 않았다는 wakeup credit로 사용한다.
fd 기반 time은 signal handler를 피한다
POSIX timer signal 대신 timerfd를 쓰면 epoll thread가 순서와 ownership을 통제할 수 있다. signal mask와 async-signal-safe handler 부담이 줄어든다.
다만 kernel timer precision, slack, scheduler latency 때문에 exact execution 시각을 보장하는 것은 아니다.
OBJECTS
객체와 수명
| 대상 | 언제 생기고 없어지는가 | 확인할 값 |
|---|---|---|
timerfd_ctx | timerfd_create에서 생기고 fd close에서 해제된다 | clockid, ticks, interval |
hrtimer/alarm | settime에서 arm되고 만료/cancel에서 상태가 바뀐다 | expires, interval, callback |
eventfd counter | eventfd 생성부터 close까지 유지되고 write/read로 증감한다 | count, semaphore flag, wait queue |
FAILURE PATH
실패 조건과 오해하기 쉬운 부분
| 겉으로 보이는 현상 | 실제 원인 후보 | 확인 방법 |
|---|---|---|
| tick 횟수 누락 | readiness 1회를 expiration 1회로 해석 | uint64 반환값 log |
| timer drift | handler 완료 뒤 상대 재arm | periodic timer 또는 absolute deadline 사용 |
| eventfd write EAGAIN | counter가 최대값 근처 | consumer 진행과 counter protocol |
LAB
직접 확인
- delay를 1.05초로 바꿔 expiration count가 약 10으로 누적되는지 본다.
- timerfd를 epoll에 eventfd와 함께 등록해 worker wakeup과 periodic tick을 한 loop에서 처리한다.
- TFD_TIMER_ABSTIME으로 monotonic 절대 deadline을 사용해 상대 재arm drift를 비교한다.
./fd_countersstrace -e trace=timerfd_create,timerfd_settime,eventfd2,poll,read,write ./fd_countersPRIMARY REFERENCES