好的,我们来深入学习 timerfd 相关的系统调用 (timerfd_create, timerfd_gettime, timerfd_settime),从 Linux 编程小白的角度出发。

1. 函数介绍 链接到标题

在 Linux 系统编程中,处理定时事件是一个常见需求。传统的定时方法包括:

  1. alarm()setitimer() + 信号 (SIGALRM):当定时器到期时,内核会发送一个信号给进程。这属于异步通知方式。信号处理函数有诸多限制(只能调用异步安全函数),并且容易引入竞态条件。
  2. sleep() 系列函数:让进程休眠指定时间。这会阻塞进程,不够灵活。

timerfd 是 Linux 2.6.25 引入的一种基于文件描述符 (File Descriptor) 的定时器接口。它巧妙地将定时器“变成”了一个可以像文件一样操作的描述符。

  • 你调用 timerfd_create() 创建一个定时器,它会返回一个文件描述符
  • 你调用 timerfd_settime() 来启动或修改这个定时器的超时时间和间隔。
  • 当定时器到期时,这个文件描述符就会变为可读状态。
  • 你可以在程序的主循环中使用 read()poll()select()epoll_wait() 等 I/O 多路复用函数来监听这个文件描述符。当 read() 成功时,就意味着定时器超时了。

简单来说,timerfd 就是把定时器“变成”了可以像文件一样读取的数据流,让你可以用处理文件 I/O 或网络 I/O 的方式来处理定时事件。

典型应用场景

  • 事件驱动服务器:将定时器集成到 epoll/poll 的主循环中,统一处理网络 I/O 和定时事件。
  • 避免信号处理的复杂性:用同步的 read() 替代异步的信号处理函数。
  • 精确控制定时:可以创建一次性定时器或周期性定时器。

2. 函数原型 链接到标题

#include <sys/timerfd.h> // 包含 timerfd 相关函数和结构体

// 创建一个定时器并返回文件描述符
int timerfd_create(int clockid, int flags);

// 获取定时器的当前设置
int timerfd_gettime(int fd, struct itimerspec *curr_value);

// 启动或重新设置定时器
int timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value);

3. 功能 链接到标题

  • timerfd_create: 创建一个新的定时器对象,并返回一个与之关联的文件描述符。
  • timerfd_gettime: 查询指定定时器文件描述符的当前定时设置(下次超时时间和间隔)。
  • timerfd_settime: 启动、停止或修改指定定时器文件描述符的定时设置。

4. 参数详解 链接到标题

timerfd_create(int clockid, int flags) 链接到标题

  • clockid:
    • int 类型。
    • 指定定时器基于哪个时钟源。常用的有:
      • CLOCK_REALTIME: 系统实时时钟,表示从 Unix 纪元(1970-01-01 00:00:00 UTC)开始的时间。如果系统时间被手动修改,这个时钟会受到影响。
      • CLOCK_MONOTONIC: 单调时钟,表示从某个未指定起点开始的时间,不会受到系统时间调整的影响,是测量间隔的首选。
  • flags:
    • int 类型。
    • 用于修改定时器行为的标志位。常用的有:
      • TFD_CLOEXEC: 在执行 exec() 系列函数时自动关闭该文件描述符。
      • TFD_NONBLOCK: 使 read() 操作变为非阻塞模式。如果当前没有定时器到期事件可读,read() 会立即返回 -1 并设置 errnoEAGAINEWOULDBLOCK
  • 返回值:
    • 成功: 返回一个有效的定时器文件描述符(非负整数)。
    • 失败: 返回 -1,并设置 errno

timerfd_gettime(int fd, struct itimerspec *curr_value) 链接到标题

  • fd:
    • int 类型。
    • 一个有效的定时器文件描述符。
  • curr_value:
    • struct itimerspec * 类型。
    • 一个指向 struct itimerspec 结构体的指针。函数调用成功后,会将定时器的当前设置填充到这个结构体中。
  • 返回值:
    • 成功: 返回 0。
    • 失败: 返回 -1,并设置 errno

timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value) 链接到标题

  • fd:
    • int 类型。
    • 一个有效的定时器文件描述符。
  • flags:
    • int 类型。
    • 控制 new_value 中时间的解释方式。主要标志:
      • 0 (默认): new_value->it_value 指定的是从当前时间点开始的相对超时时间。
      • TFD_TIMER_ABSTIME: new_value->it_value 指定的是一个绝对的超时时间点(基于创建定时器时指定的 clockid)。
  • new_value:
    • const struct itimerspec * 类型。
    • 指向一个 struct itimerspec 结构体,定义了新的定时器设置。
  • old_value:
    • struct itimerspec * 类型。
    • 如果不为 NULL,函数调用成功后,会将定时器在设置前的旧设置填充到这个结构体中。如果不需要旧值,可以传 NULL
  • 返回值:
    • 成功: 返回 0。
    • 失败: 返回 -1,并设置 errno

struct itimerspec 结构体 链接到标题

这个结构体用于定义定时器的时间设置:

struct timespec {
    time_t tv_sec;  /* 秒 */
    long   tv_nsec; /* 纳秒 */
};

struct itimerspec {
    struct timespec it_interval; /* 定时器的时间间隔 (周期性定时器) */
    struct timespec it_value;    /* 首次超时的相对/绝对时间 */
};
  • it_value: 指定首次超时的时间。
    • 如果 tv_sectv_nsec 都为 0,则定时器被停止
    • 如果非 0,则指定定时器下次到期的时间。
  • it_interval: 指定定时器到期后,重复的间隔时间。
    • 如果 tv_sectv_nsec 都为 0,则定时器是一次性的
    • 如果非 0,则定时器在首次到期后,会按此间隔周期性地重复到期。

5. 如何使用定时器文件描述符 链接到标题

  1. 创建: 使用 timerfd_create() 创建定时器,获得 fd
  2. 设置: 使用 timerfd_settime() 配置定时器的超时和间隔。
  3. 等待: 在主循环中,使用 poll(), select(), epoll_wait() 或直接 read() (如果设置为阻塞) 来等待 fd 变为可读。
  4. 读取: 当 fd 可读时,调用 read(fd, &expirations, sizeof(expirations))read() 会成功,并且读取到一个 uint64_t 类型的整数,表示自上次 read() 以来定时器到期的次数
  5. 查询/修改: 可以随时使用 timerfd_gettime() 查询当前设置,或再次调用 timerfd_settime() 修改设置。

6. 错误码 (errno) 链接到标题

这些函数共享一些常见的错误码:

  • EINVAL: 参数无效(例如 clockid 无效,flags 无效,itimerspec 字段值无效)。
  • EMFILE: 进程已打开的文件描述符数量达到上限 (RLIMIT_NOFILE)。
  • ENFILE: 系统已打开的文件描述符数量达到上限。
  • ENOMEM: 内核内存不足。
  • EBADF: fd 不是有效的文件描述符。
  • EFAULT: new_value, old_value, 或 curr_value 指向无效内存。

7. 相似函数或关联函数 链接到标题

  • alarm / setitimer: 传统的基于信号的定时器。
  • poll, select, epoll_wait: I/O 多路复用函数,可以监听 timerfd 文件描述符的可读事件。
  • read: 用于从 timerfd 中读取到期次数。
  • close: 关闭 timerfd 文件描述符。
  • clock_gettime / clock_settime: 获取和设置不同类型的系统时钟。
  • POSIX 定时器 (timer_create, timer_settime, timer_gettime): 另一套定时器 API,也使用信号通知。

8. 示例代码 链接到标题

下面的示例演示了如何使用 timerfd 创建一次性定时器和周期性定时器,并将其集成到 poll 系统调用中。

#define _GNU_SOURCE // 启用 GNU 扩展
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/timerfd.h> // timerfd 相关
#include <poll.h>       // poll 相关
#include <string.h>
#include <errno.h>
#include <inttypes.h>   // 包含 PRIu64 宏,用于 printf uint64_t

// 辅助函数:打印 itimerspec 结构
void print_itimerspec(const char* prefix, const struct itimerspec *ts) {
    printf("%s: ", prefix);
    if (ts->it_value.tv_sec == 0 && ts->it_value.tv_nsec == 0) {
        printf("Timer is STOPPED\n");
    } else {
        printf("Next expiration in %ld.%09ld seconds\n", ts->it_value.tv_sec, ts->it_value.tv_nsec);
    }
    printf("     Interval: %ld.%09ld seconds\n", ts->it_interval.tv_sec, ts->it_interval.tv_nsec);
}

int main() {
    int tfd_one_shot, tfd_periodic;
    struct itimerspec one_shot_time, periodic_time;
    struct pollfd fds[3]; // 监听两个 timerfd 和 标准输入
    uint64_t expirations;
    ssize_t s;

    printf("--- Demonstrating timerfd ---\n");
    printf("PID: %d\n", getpid());

    // 1. 创建两个 timerfd
    // 一次性定时器,基于单调时钟
    tfd_one_shot = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);
    if (tfd_one_shot == -1) {
        perror("timerfd_create one-shot");
        exit(EXIT_FAILURE);
    }
    printf("Created one-shot timerfd: %d\n", tfd_one_shot);

    // 周期性定时器,基于单调时钟,非阻塞
    tfd_periodic = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC | TFD_NONBLOCK);
    if (tfd_periodic == -1) {
        perror("timerfd_create periodic");
        close(tfd_one_shot);
        exit(EXIT_FAILURE);
    }
    printf("Created periodic timerfd: %d (non-blocking)\n", tfd_periodic);

    // 2. 设置一次性定时器:3秒后到期
    one_shot_time.it_value.tv_sec = 3;     // 3 秒后
    one_shot_time.it_value.tv_nsec = 0;
    one_shot_time.it_interval.tv_sec = 0;  // 一次性,不重复
    one_shot_time.it_interval.tv_nsec = 0;
    if (timerfd_settime(tfd_one_shot, 0, &one_shot_time, NULL) == -1) {
        perror("timerfd_settime one-shot");
        close(tfd_one_shot);
        close(tfd_periodic);
        exit(EXIT_FAILURE);
    }
    printf("Set one-shot timer to expire in 3 seconds.\n");

    // 3. 设置周期性定时器:立即启动,每 2 秒重复一次
    periodic_time.it_value.tv_sec = 0;     // 立即启动
    periodic_time.it_value.tv_nsec = 1;    // 纳秒设为1,确保立即触发第一次
    periodic_time.it_interval.tv_sec = 2;  // 每 2 秒重复
    periodic_time.it_interval.tv_nsec = 0;
    if (timerfd_settime(tfd_periodic, 0, &periodic_time, NULL) == -1) {
        perror("timerfd_settime periodic");
        close(tfd_one_shot);
        close(tfd_periodic);
        exit(EXIT_FAILURE);
    }
    printf("Set periodic timer to start immediately and repeat every 2 seconds.\n");

    // 4. 查询并打印初始定时器状态
    struct itimerspec curr_time;
    if (timerfd_gettime(tfd_one_shot, &curr_time) == 0) {
        print_itimerspec("One-shot timer initial state", &curr_time);
    }
    if (timerfd_gettime(tfd_periodic, &curr_time) == 0) {
        print_itimerspec("Periodic timer initial state", &curr_time);
    }

    // 5. 设置 poll 的文件描述符数组
    fds[0].fd = tfd_one_shot;
    fds[0].events = POLLIN;
    fds[1].fd = tfd_periodic;
    fds[1].events = POLLIN;
    fds[2].fd = STDIN_FILENO;
    fds[2].events = POLLIN;

    printf("\nEntering main loop. Waiting for timers or input (type 'quit' to exit)...\n");

    // 6. 主循环:使用 poll 等待事件
    int one_shot_done = 0;
    while (!one_shot_done) { // 循环直到一次性定时器完成
        int poll_num = poll(fds, 3, -1); // -1 表示无限期等待
        if (poll_num == -1) {
            if (errno == EINTR) {
                continue; // poll 被信号中断,继续等待
            } else {
                perror("poll");
                break;
            }
        }

        if (poll_num > 0) {
            // 检查一次性定时器
            if (fds[0].revents & POLLIN) {
                s = read(tfd_one_shot, &expirations, sizeof(expirations));
                if (s == sizeof(expirations)) {
                    printf("\n[ONE-SHOT TIMER] Expired! Expirations counted: %" PRIu64 "\n", expirations);
                    one_shot_done = 1; // 设置标志退出循环

                    // 再次查询状态,确认已停止
                    if (timerfd_gettime(tfd_one_shot, &curr_time) == 0) {
                        print_itimerspec("One-shot timer final state", &curr_time);
                    }
                } else {
                    perror("read one-shot timerfd");
                }
            }

            // 检查周期性定时器
            if (fds[1].revents & POLLIN) {
                s = read(tfd_periodic, &expirations, sizeof(expirations));
                if (s == sizeof(expirations)) {
                    printf("\n[PERIODIC TIMER] Expired! Expirations counted: %" PRIu64 "\n", expirations);
                    // 可以在这里处理周期性任务
                } else if (s == -1) {
                    if (errno != EAGAIN && errno != EWOULDBLOCK) {
                         perror("read periodic timerfd"); // 非 EAGAIN 的错误
                    } // EAGAIN/EWOULDBLOCK 是正常的,因为我们设置了 NONBLOCK
                }
            }

            // 检查标准输入
            if (fds[2].revents & POLLIN) {
                char buf[16];
                ssize_t nread = read(STDIN_FILENO, buf, sizeof(buf) - 1);
                if (nread > 0) {
                    buf[nread] = '\0';
                    printf("Read from stdin: %s", buf);
                    if (strncmp(buf, "quit\n", 5) == 0) {
                        printf("Exiting due to 'quit' command.\n");
                        break;
                    }
                } else if (nread == 0) {
                    printf("EOF on stdin (Ctrl+D). Exiting.\n");
                    break;
                }
            }
        }
    }

    // 7. 清理资源
    printf("\nClosing timerfds...\n");
    close(tfd_one_shot);
    close(tfd_periodic);
    printf("Program finished.\n");

    return 0;
}

9. 编译和运行 链接到标题

# 假设代码保存在 timerfd_example.c 中
gcc -o timerfd_example timerfd_example.c

# 运行程序
./timerfd_example
# 程序会启动定时器,并在终端输出信息。
# 等待 3 秒后一次性定时器触发。
# 每 2 秒周期性定时器会触发。
# 输入 'quit' 并回车可以退出。

10. 预期输出 链接到标题

--- Demonstrating timerfd ---
PID: 12345
Created one-shot timerfd: 3
Created periodic timerfd: 4 (non-blocking)
Set one-shot timer to expire in 3 seconds.
Set periodic timer to start immediately and repeat every 2 seconds.
One-shot timer initial state: Next expiration in 2.999999000 seconds
     Interval: 0.000000000 seconds
Periodic timer initial state: Next expiration in 0.000000001 seconds
     Interval: 2.000000000 seconds

Entering main loop. Waiting for timers or input (type 'quit' to exit)...

[PERIODIC TIMER] Expired! Expirations counted: 1

[PERIODIC TIMER] Expired! Expirations counted: 1

[ONE-SHOT TIMER] Expired! Expirations counted: 1
One-shot timer final state: Timer is STOPPED

[PERIODIC TIMER] Expired! Expirations counted: 1
...
# (继续每2秒打印,直到输入 'quit')
Read from stdin: quit
Exiting due to 'quit' command.

Closing timerfds...
Program finished.

11. 总结 链接到标题

timerfd 提供了一种现代化、同步化、且易于与事件驱动模型集成的定时器机制。

  • 核心优势:将定时事件转换为文件描述符的可读事件,可以方便地融入 poll/select/epoll 等 I/O 多路复用框架。
  • 避免信号:解决了传统信号定时器的异步处理复杂性和竞态条件问题。
  • 灵活配置:通过 timerfd_settime 可以轻松创建一次性或周期性定时器,并支持相对和绝对时间。
  • 信息丰富read() 返回的到期次数 (uint64_t) 可以帮助处理定时器“堆积”(例如程序忙于处理其他任务导致多次到期)的情况。

掌握 timerfd 对于编写高性能、事件驱动的 Linux 应用程序(尤其是服务器)非常有帮助。