好的,我们来深入学习 timerfd
相关的系统调用 (timerfd_create
, timerfd_gettime
, timerfd_settime
),从 Linux 编程小白的角度出发。
1. 函数介绍 链接到标题
在 Linux 系统编程中,处理定时事件是一个常见需求。传统的定时方法包括:
alarm()
和setitimer()
+ 信号 (SIGALRM
):当定时器到期时,内核会发送一个信号给进程。这属于异步通知方式。信号处理函数有诸多限制(只能调用异步安全函数),并且容易引入竞态条件。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 并设置errno
为EAGAIN
或EWOULDBLOCK
。
- 返回值:
- 成功: 返回一个有效的定时器文件描述符(非负整数)。
- 失败: 返回 -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_sec
和tv_nsec
都为 0,则定时器被停止。 - 如果非 0,则指定定时器下次到期的时间。
- 如果
it_interval
: 指定定时器到期后,重复的间隔时间。- 如果
tv_sec
和tv_nsec
都为 0,则定时器是一次性的。 - 如果非 0,则定时器在首次到期后,会按此间隔周期性地重复到期。
- 如果
5. 如何使用定时器文件描述符 链接到标题
- 创建: 使用
timerfd_create()
创建定时器,获得fd
。 - 设置: 使用
timerfd_settime()
配置定时器的超时和间隔。 - 等待: 在主循环中,使用
poll()
,select()
,epoll_wait()
或直接read()
(如果设置为阻塞) 来等待fd
变为可读。 - 读取: 当
fd
可读时,调用read(fd, &expirations, sizeof(expirations))
。read()
会成功,并且读取到一个uint64_t
类型的整数,表示自上次read()
以来定时器到期的次数。 - 查询/修改: 可以随时使用
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 应用程序(尤其是服务器)非常有帮助。