Hệ điều hành · 03/10/2026

Linux timerfd thực chiến: Scheduler chính xác trong epoll event loop

Một event loop Linux thường phải chờ socket, signal, yêu cầu nội bộ và deadline cùng lúc. Nếu timer chạy ở thread riêng hoặc signal handler, code đồng bộ hóa nhanh chóng phức tạp. timerfd_create() biến timer của kernel thành file descriptor có thể đưa thẳng vào epoll; mỗi lần đọc còn cho biết bao nhiêu lần hết hạn đã xảy ra, kể cả khi consumer xử lý chậm.

Linux timerfd thực chiến: Scheduler chính xác trong epoll event loop

Một event loop Linux thường phải chờ socket, signal, yêu cầu nội bộ và deadline cùng lúc. Nếu timer chạy ở thread riêng hoặc signal handler, code đồng bộ hóa nhanh chóng phức tạp. timerfd_create() biến timer của kernel thành file descriptor có thể đưa thẳng vào epoll; mỗi lần đọc còn cho biết bao nhiêu lần hết hạn đã xảy ra, kể cả khi consumer xử lý chậm.

Bài viết này hướng dẫn chọn clock, tạo timer nonblocking, dùng deadline tuyệt đối để tránh drift, xử lý missed expirations, phản ứng với thay đổi wall clock và thiết kế scheduler nhiều deadline bằng một timerfd. Nội dung dựa trên Linux man-pages về timerfd_create(2), clock_gettime(2), time(7) và epoll(7).

1. timerfd cung cấp điều gì?

timerfd_create() tạo một timer và trả file descriptor. Ứng dụng arm hoặc disarm bằng timerfd_settime(), kiểm tra trạng thái bằng timerfd_gettime(), chờ bằng poll/epoll và tiêu thụ expiration bằng read().

Mỗi lần đọc thành công trả đúng một số nguyên 64 bit theo byte order của host: số lần timer đã hết hạn kể từ lần arm/thay đổi gần nhất hoặc lần đọc thành công trước. Đây là overrun accounting tích hợp, không cần callback bất đồng bộ.

Timer hết hạn không có nghĩa callback được chạy đúng nanosecond đó. Kernel đánh dấu FD readable; thời điểm xử lý còn phụ thuộc scheduler, tải CPU và event loop.

2. Chọn clock theo semantics, không theo thói quen

ClockQua suspendBị chỉnh wall clockDùng điển hình
CLOCK_MONOTONICKhông tính thời gian suspendKhông nhảy khi set giờ hệ thốngTimeout, retry, heartbeat khi thời gian suspend không tính vào deadline.
CLOCK_BOOTTIMECó tính suspendKhông bị wall-clock jumpLease hoặc deadline elapsed-time vẫn phải trôi khi máy ngủ.
CLOCK_REALTIMETheo giờ lịchCó thể nhảy tiến/lùi“Chạy lúc 09:00 UTC/ngày mai” và lịch gắn thời gian dân sự.
*_ALARMCó thể đánh thức hệ thốngTheo clock tương ứngWake alarm có chủ đích; cần quyền CAP_WAKE_ALARM.

CLOCK_BOOTTIME tính cả thời gian suspend nhưng timer thông thường không nhất thiết đánh thức máy; expiration sẽ được quan sát khi hệ thống resume. Muốn wake system phải dùng alarm clock phù hợp và quyền cần thiết.

3. Tạo timerfd sẵn sàng cho event loop

#include <sys/timerfd.h>

int tfd = timerfd_create(CLOCK_MONOTONIC,
                         TFD_NONBLOCK | TFD_CLOEXEC);
if (tfd == -1)
    fail_errno("timerfd_create");
  • TFD_NONBLOCK cho phép drain đến EAGAIN mà không khóa event loop.
  • TFD_CLOEXEC tránh timer vô tình sống qua execve().

Sau fork(), parent và child có descriptor trỏ tới cùng timer object; đọc ở một phía tiêu thụ expiration counter chung. Nếu hai process cần lịch độc lập, hãy tạo timer riêng sau fork.

4. One-shot tương đối đơn giản

struct itimerspec spec = {
    .it_value = { .tv_sec = 2, .tv_nsec = 500000000 },
    .it_interval = { .tv_sec = 0, .tv_nsec = 0 }
};

if (timerfd_settime(tfd, 0, &spec, NULL) == -1)
    fail_errno("timerfd_settime");

Không có TFD_TIMER_ABSTIME, it_value là khoảng thời gian tính từ lúc gọi. it_interval = 0 tạo one-shot. Đặt cả hai field của it_value về 0 sẽ disarm timer.

Relative one-shot phù hợp cho timeout đơn lẻ. Vấn đề xuất hiện khi một tác vụ định kỳ cứ “xử lý xong rồi arm thêm 1 giây”: thời gian xử lý cộng dồn vào chu kỳ và tạo drift.

5. Deadline tuyệt đối chống drift

static void add_ns(struct timespec *t, uint64_t ns)
{
    t->tv_sec += (time_t)(ns / 1000000000ULL);
    t->tv_nsec += (long)(ns % 1000000000ULL);
    if (t->tv_nsec >= 1000000000L) {
        t->tv_sec++;
        t->tv_nsec -= 1000000000L;
    }
}

struct timespec next;
if (clock_gettime(CLOCK_MONOTONIC, &next) == -1)
    fail_errno("clock_gettime");

add_ns(&next, 1000000000ULL);

struct itimerspec spec = {
    .it_value = next,
    .it_interval = {0, 0}
};

if (timerfd_settime(tfd, TFD_TIMER_ABSTIME, &spec, NULL) == -1)
    fail_errno("timerfd_settime absolute");

Sau mỗi lần chạy, tăng next theo period từ deadline trước, không lấy “now + period”. Lịch vì vậy bám vào timeline ban đầu. Nếu tác vụ trễ nhiều chu kỳ, policy phải quyết định bỏ qua, chạy bù có giới hạn hay báo overload.

Timer định kỳ qua it_interval cũng giữ nhịp trong kernel và counter cho biết số expiration bị lỡ. Absolute one-shot hữu ích hơn khi scheduler cần deadline thay đổi hoặc quản lý heap công việc.

6. Đưa timerfd vào epoll và đọc đúng

struct epoll_event ev = {
    .events = EPOLLIN | EPOLLET,
    .data.fd = tfd
};

if (epoll_ctl(epfd, EPOLL_CTL_ADD, tfd, &ev) == -1)
    fail_errno("epoll_ctl");

static uint64_t read_expirations(int tfd)
{
    uint64_t total = 0;

    for (;;) {
        uint64_t count;
        ssize_t n = read(tfd, &count, sizeof(count));

        if (n == (ssize_t)sizeof(count)) {
            total += count;
            continue;
        }
        if (n == -1 && errno == EINTR)
            continue;
        if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK))
            return total;
        fail_errno("read timerfd");
    }
}

Buffer nhỏ hơn 8 byte làm read() thất bại với EINVAL. Với EPOLLET, drain tới EAGAIN. Trong thực tế một lần đọc thành công thường tiêu thụ counter hiện tại, nhưng loop vẫn là mẫu an toàn khi readiness và rearm có thể giao nhau.

7. Missed expirations: đừng tạo catch-up storm

Nếu timer 100 ms trả count = 37, event loop đã không đọc trong khoảng 3,7 giây. Chạy callback nặng 37 lần liên tiếp có thể làm hệ thống càng trễ. Chọn policy theo nghiệp vụ:

  • Coalesce: chạy một lần với state mới nhất, phù hợp refresh cache/metric snapshot.
  • Bounded catch-up: xử lý tối đa N tick rồi bỏ phần còn lại và ghi metric.
  • Exact accounting: cộng count vào counter nghiệp vụ nhưng xử lý theo batch.
  • Fail/degrade: nếu mỗi tick là deadline an toàn, chuyển degraded mode hoặc restart có kiểm soát.

Theo dõi count > 1, event-loop lag, callback duration và độ dài hàng đợi. Missed expiration là tín hiệu capacity, không chỉ là chi tiết API.

8. Wall-clock timer và TFD_TIMER_CANCEL_ON_SET

Với lịch gắn giờ dân sự, dùng absolute CLOCK_REALTIME. Nhưng NTP step, quản trị viên hoặc VM restore có thể thay đổi giờ đột ngột. Kết hợp:

int flags = TFD_TIMER_ABSTIME | TFD_TIMER_CANCEL_ON_SET;

if (timerfd_settime(tfd, flags, &spec, NULL) == -1)
    fail_errno("timerfd_settime realtime");

TFD_TIMER_CANCEL_ON_SET chỉ có ý nghĩa với absolute timer trên CLOCK_REALTIME hoặc CLOCK_REALTIME_ALARM. Khi clock bị thay đổi gián đoạn, read() trả lỗi ECANCELED. Ứng dụng phải đọc lại nguồn lịch, tính deadline mới và rearm; không coi đó là lỗi I/O ngẫu nhiên rồi retry vô hạn.

9. Một timerfd cho nhiều deadline

Tạo một FD cho mỗi request timeout làm tăng file descriptors và chi phí epoll. Scheduler quy mô lớn thường:

  1. giữ deadline trong min-heap hoặc timing wheel;
  2. arm một timerfd tới deadline sớm nhất bằng TFD_TIMER_ABSTIME;
  3. khi timer readable, lấy mọi job có deadline ≤ now;
  4. chạy/batch chúng theo budget;
  5. rearm tới deadline tiếp theo hoặc disarm nếu heap rỗng.

Khi thêm một job sớm hơn đầu heap, rearm ngay. Dùng cùng clock cho toàn bộ phép so sánh; không trộn timestamp realtime và monotonic.

10. Race, lifecycle và lỗi thường gặp

  • Arm tương đối sau callback: tạo drift bằng chính thời gian xử lý.
  • Bỏ qua count: mất thông tin overload và hiểu sai số tick đã trôi.
  • Catch up không giới hạn: tạo vòng xoáy CPU khiến loop không bao giờ bắt kịp.
  • Dùng REALTIME cho timeout: timeout thay đổi khi đồng hồ hệ thống nhảy.
  • Dùng MONOTONIC khi suspend phải tính: lease bị kéo dài ngoài ý muốn.
  • Quên rearm khi heap head đổi: job mới sớm hơn chạy trễ.
  • Đóng/rearm từ nhiều thread không có ownership: tạo race với epoll event cũ.
  • Quên CLOEXEC: timer tiếp tục hoạt động trong chương trình sau exec.

Một event-loop thread nên sở hữu timerfd. Thread khác gửi lệnh cập nhật qua queue + eventfd; owner áp dụng heap change và rearm tuần tự.

Checklist production

  • Timeout dùng MONOTONIC; deadline tính suspend dùng BOOTTIME; giờ lịch dùng REALTIME.
  • Timerfd được tạo với TFD_NONBLOCK | TFD_CLOEXEC.
  • Lịch định kỳ dùng interval kernel hoặc absolute deadline, không “now + period” sau mỗi callback.
  • Đọc đủ 8 byte và quan sát expiration count.
  • EPOLLET được drain đến EAGAIN.
  • Có policy coalesce/catch-up hữu hạn khi count lớn hơn 1.
  • REALTIME absolute timer xử lý ECANCELED và tính lại lịch.
  • Scheduler nhiều job dùng một timerfd + heap/timing wheel khi phù hợp.
  • Ownership, close, rearm và shutdown được tuần tự hóa.
  • Metric có event-loop lag, missed ticks và callback duration.

Kết luận

timerfd() không chỉ là cách thay thế sleep: nó đưa thời gian vào cùng mô hình file descriptor với toàn bộ event loop Linux. Thiết kế tốt bắt đầu từ đúng clock, giữ nhịp bằng deadline tuyệt đối, đọc và diễn giải expiration count, giới hạn catch-up và quản lý nhiều deadline qua một scheduler có ownership rõ ràng. Khi các điểm này được xử lý, timer trở thành một nguồn sự kiện có thể quan sát, kiểm thử và vận hành ổn định.

Thảo luận

Bình luận 0

Đăng nhập để bình luận

Bạn cần có tài khoản để tham gia thảo luận và trả lời độc giả khác.

Đăng nhậpĐăng ký

Chưa có bình luận. Hãy là người đầu tiên chia sẻ ý kiến.