TimerFd & Epoll

  1. "传统 Reactor:用 select(2)/poll(2)等待时间实现定时";
  2. "现代 Linux:用 timerfd 把定时当作可读 fd纳入同一事件循环"。

方案 A:用 select(2) 的超时实现定时

要点:把"下一次到期时间"换算成 struct timeval timeout 传入 selectselect 返回后先处理 I/O,再检查"当前时间 ≥ 到期时间"来触发定时回调,并据此计算下一次的超时。

c 复制代码
// build: gcc -Wall -O2 select_timer.c -o select_timer
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <unistd.h>
#include <sys/time.h>
#include <errno.h>

static long long now_ms(void) {
    struct timeval tv; gettimeofday(&tv, NULL);
    return (long long)tv.tv_sec * 1000 + tv.tv_usec / 1000;
}

int main(void) {
    // 模拟:每 1000ms 触发一次"定时任务"
    long long interval = 1000;
    long long next_expire = now_ms() + interval;

    // 示意:监听 0 号 fd(stdin)作为 I/O 事件源
    int maxfd = 0;

    for (;;) {
        long long n = now_ms();
        long long remain = next_expire - n;
        if (remain < 0) remain = 0;

        struct timeval tv;
        tv.tv_sec  = remain / 1000;
        tv.tv_usec = (remain % 1000) * 1000;

        fd_set rfds;
        FD_ZERO(&rfds);
        FD_SET(0, &rfds); // 监听 stdin 可读

        int rc = select(maxfd + 1, &rfds, NULL, NULL, &tv);
        if (rc < 0) {
            if (errno == EINTR) continue; // 被信号中断,重来
            perror("select");
            break;
        }

        // 1) 先处理 I/O
        if (rc > 0 && FD_ISSET(0, &rfds)) {
            char buf[256];
            ssize_t nr = read(0, buf, sizeof(buf));
            if (nr > 0) {
                write(1, "read stdin\n", 11);
            }
        }

        // 2) 再处理定时(可能一次到期多个,视设计而定)
        long long now = now_ms();
        if (now >= next_expire) {
            // 触发"定时回调"
            write(1, "timer fired\n", 12);
            // 重新安排下一次
            next_expire = now + interval;
        }
    }
    return 0;
}

若用 poll(2),思路一致:把 remain 毫秒放进 poll(timeout_ms),返回后同样先处理 I/O,再检查到期。区别仅在 API 形式,这里就不重复放代码了。


方案 B:用 timerfd + epoll 把定时纳入 I/O 事件流

要点:创建 timerfd(推荐 CLOCK_MONOTONIC),把它加入 epoll;设置一次性到期(one-shot);到期时 timerfd 变为可读 ,在读回 8 字节计数后执行回调,然后重新设置下一次到期。这样,代码路径与普通套接字 I/O 完全一致。

c 复制代码
// build: gcc -Wall -O2 epoll_timerfd.c -o epoll_timerfd
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/timerfd.h>
#include <string.h>
#include <errno.h>
#include <time.h>
#include <fcntl.h>

static int set_nonblock(int fd) {
    int fl = fcntl(fd, F_GETFL, 0);
    if (fl < 0) return -1;
    return fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

static void arm_timerfd(int tfd, int ms) {
    struct itimerspec its;
    memset(&its, 0, sizeof(its));
    // one-shot:只设置 it_value,不设置 it_interval
    its.it_value.tv_sec  = ms / 1000;
    its.it_value.tv_nsec = (ms % 1000) * 1000000;
    if (timerfd_settime(tfd, 0, &its, NULL) < 0) {
        perror("timerfd_settime");
        exit(1);
    }
}

int main(void) {
    // 1) 创建 epoll
    int ep = epoll_create1(EPOLL_CLOEXEC);
    if (ep < 0) { perror("epoll_create1"); return 1; }

    // 2) 创建 timerfd(MONOTONIC 避免系统时间跳变影响)
    int tfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
    if (tfd < 0) { perror("timerfd_create"); return 1; }

    // 3) 把 timerfd 加入 epoll
    struct epoll_event ev; memset(&ev, 0, sizeof(ev));
    ev.events = EPOLLIN; // 也可用 EPOLLET,看你整体风格
    ev.data.u32 = 1;     // 简单标识
    if (epoll_ctl(ep, EPOLL_CTL_ADD, tfd, &ev) < 0) {
        perror("epoll_ctl add timerfd"); return 1;
    }

    // 4) 也把 stdin(0) 放进来,演示 I/O & 定时"同路由"
    int stdin_fd = 0;
    set_nonblock(stdin_fd);
    memset(&ev, 0, sizeof(ev));
    ev.events = EPOLLIN;
    ev.data.u32 = 2;
    if (epoll_ctl(ep, EPOLL_CTL_ADD, stdin_fd, &ev) < 0) {
        perror("epoll_ctl add stdin"); return 1;
    }

    // 5) 设定首次 1000ms 后触发
    int interval_ms = 1000;
    arm_timerfd(tfd, interval_ms);

    // 6) 事件循环
    struct epoll_event events[16];
    for (;;) {
        int n = epoll_wait(ep, events, 16, -1);
        if (n < 0) {
            if (errno == EINTR) continue;
            perror("epoll_wait"); break;
        }
        for (int i = 0; i < n; ++i) {
            if (events[i].data.u32 == 1 && (events[i].events & EPOLLIN)) {
                // timerfd 就绪:必须 read 8 字节计数
                uint64_t cnt;
                ssize_t r = read(tfd, &cnt, sizeof(cnt));
                if (r == sizeof(cnt)) {
                    // 触发"定时回调"
                    write(1, "timer fired\n", 12);
                    // 重新安排下一次(one-shot 设计)
                    arm_timerfd(tfd, interval_ms);
                } else if (r < 0 && errno == EAGAIN) {
                    // 非阻塞读完
                } else {
                    perror("read timerfd");
                }
            } else if (events[i].data.u32 == 2 && (events[i].events & EPOLLIN)) {
                // stdin 可读:与 socket/pipe 一样处理
                char buf[256];
                ssize_t nr = read(0, buf, sizeof(buf));
                if (nr > 0) {
                    write(1, "read stdin\n", 11);
                }
            }
        }
    }

    close(tfd);
    close(ep);
    return 0;
}

对照要点(便于你在工程中选型)

  • 一致性

    • select/poll 超时:定时靠"返回值 + 当前时间比较",I/O 走 fd 事件 → 两条路径。
    • timerfd:定时也是一个 fd,与 I/O 完全"同路径"。
  • 易错点

    • select/poll:计算剩余时间与"更新最早到期"的竞态需要谨慎处理。
    • timerfd:记得read 出 8 字节计数 清空可读状态;建议用 one-shot ,回调后再 settime
  • 推荐时钟CLOCK_MONOTONIC(避免系统时间调整导致跳变)。

相关推荐
TiAmo zhang1 天前
SQL Server 2019实验 │ 设计数据库的完整性
数据库·sqlserver
杯莫停丶1 天前
设计模式之:简单工厂模式
java·设计模式·简单工厂模式
Lucky_Turtle1 天前
【Java Xml】dom4j写入XML
xml·java·python
wanhengidc1 天前
如何使用云手机进行游戏挂机?
运维·服务器·游戏·智能手机·云计算
superlls1 天前
(定时任务)接上篇:定时任务的分布式执行与分布式锁使用场景
java·分布式·后端
汪汪大队u1 天前
为什么 filter-policy 仅对 ASBR 的出方向生效,且即使在该生效场景下,被过滤的路由在协议内部(如协议数据库)依然存在,没有被彻底移除?
服务器·前端·网络
无敌的牛1 天前
C++复习(1)
java·开发语言·面试
子沫20201 天前
springboot中server.main.web-application-type=reactive导致的拦截器不生效
java·spring boot·后端
kyle~1 天前
设计模式---观察者模式
服务器·观察者模式·设计模式
Pluchon1 天前
硅基计划4.0 算法 二叉树深搜(DFS)
java·数据结构·算法·leetcode·深度优先·剪枝