← System Programming DUJINLABS.COM

Signal / Event / IPC · Linux userspace / kernel ABI

timerfd와 eventfd

시간 만료와 thread 간 wakeup을 8-byte counter fd로 표현해 epoll loop에 합치고, counter 누적과 overflow 규칙을 살펴봅니다.

Series
23 / 37
Build
cc -std=c17 -Wall -Wextra -O2 fd_counters.c -o fd_counters
Run
./fd_counters
Kernel
Linux 6.18.37 LTS

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를 한 번의 사건으로만 해석하면 누적 정보를 잃는다.

구조 그림

그림 1. periodic timer의 만료 누적과 8-byte read
0 ms timer arm interval=100 ms
100 tick counter=1
200 tick counter=2
300 tick counter=3
400 tick counter=4
500 tick counter=5
550 read uint64 returns 5 · counter=0

event loop가 550 ms 동안 읽지 않으면 100 ms timer 만료가 counter에 누적된다. read 한 번이 누적값을 소비한다.

호출 흐름

그림 2. 사용자 코드에서 관찰 가능한 결과까지
timer/event write counter 증가
fd readable counter > 0
epoll_wait 다른 I/O와 합류
read uint64 누적값 소비
policy catch-up 또는 skip

readiness 횟수와 실제 event 횟수를 분리한다. 누적 counter를 읽은 뒤 application이 모든 tick을 따라잡을지 최신 상태만 계산할지 정한다.

그림 3. 커널 내부에서 지나가는 주요 지점
hrtimer/eventfd counter/state 변경
wake_up poll wait queue
ep_poll_callback ready 연결
read 8-byte copy
counter reset/sub read mode 적용

함수 이름을 외우기 위한 그림이 아니다. 반환값, 파일 디스크립터, 메모리 매핑, 대기 큐 가운데 무엇이 다음 단계로 전달되는지 확인한다.

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

실행 예제 원본

아래 코드는 설명을 위해 중간 줄을 생략한 의사 코드가 아니다. 파일로 빌드해 실행할 수 있는 최소 예제다.

빌드cc -std=c17 -Wall -Wextra -O2 fd_counters.c -o fd_counters
01#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}

코드 조각별 설명

실제 코드 11행CLOCK_MONOTONIC

wall clock 변경과 무관한 경과 시간 timer를 만든다. calendar deadline이면 CLOCK_REALTIME과 cancel-on-set 정책을 검토한다.

실제 코드 16행.it_interval

0이 아니므로 첫 만료 뒤 같은 주기로 재무장되는 periodic timer다.

실제 코드 22행nanosleep(&delay

여러 100ms tick이 쌓이도록 550ms 동안 event를 읽지 않는다.

실제 코드 24행poll(&pfd, 1, 0)

timeout 0으로 현재 readable 여부만 확인한다. counter가 0보다 크면 POLLIN이다.

실제 코드 28행read(fd, &expirations

반드시 uint64_t 크기로 읽고 누적 expiration 수를 얻는다. read 뒤 counter는 현재까지 소비된다.

세부 동작

01

periodic tick을 모두 실행할지는 application 정책이다

expirations가 100이면 handler를 100번 실행해 catch-up할 수도 있고, 현재 monotonic time으로 상태를 한 번 재계산해 오래된 tick을 건너뛸 수도 있다. 제어 loop와 UI refresh는 보통 다른 선택을 한다.

callback 실행 시간을 interval보다 짧게 유지한다는 전제를 계측한다.

02

eventfd는 wakeup과 값 전달을 분리한다

eventfd write 값은 counter에 더해지고 일반 read는 누적 전체를 반환해 0으로 만든다. EFD_SEMAPHORE mode에서는 read마다 1을 반환하고 counter를 1 줄인다.

복잡한 payload는 별도 queue에 넣고 eventfd는 queue가 비지 않았다는 wakeup credit로 사용한다.

03

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 시각을 보장하는 것은 아니다.

객체와 수명

대상언제 생기고 없어지는가확인할 값
timerfd_ctxtimerfd_create에서 생기고 fd close에서 해제된다clockid, ticks, interval
hrtimer/alarmsettime에서 arm되고 만료/cancel에서 상태가 바뀐다expires, interval, callback
eventfd countereventfd 생성부터 close까지 유지되고 write/read로 증감한다count, semaphore flag, wait queue

실패 조건과 오해하기 쉬운 부분

겉으로 보이는 현상실제 원인 후보확인 방법
tick 횟수 누락readiness 1회를 expiration 1회로 해석uint64 반환값 log
timer drifthandler 완료 뒤 상대 재armperiodic timer 또는 absolute deadline 사용
eventfd write EAGAINcounter가 최대값 근처consumer 진행과 counter protocol

직접 확인

  1. delay를 1.05초로 바꿔 expiration count가 약 10으로 누적되는지 본다.
  2. timerfd를 epoll에 eventfd와 함께 등록해 worker wakeup과 periodic tick을 한 loop에서 처리한다.
  3. TFD_TIMER_ABSTIME으로 monotonic 절대 deadline을 사용해 상대 재arm drift를 비교한다.
실행./fd_counters
추적strace -e trace=timerfd_create,timerfd_settime,eventfd2,poll,read,write ./fd_counters

원문