Linux软件定时器回顾
主要实现方式
1. setitimer()
+ 信号
这是传统的POSIX方法:
struct itimerval
{/* Value to put into `it_value' when the timer expires. */struct timeval it_interval;/* Time to the next timer expiration. */struct timeval it_value;
};it_interval:计时器的初始值,一般基于这个初始值来加或者来减,看控制函数的参数配置
it_value:程序跑到这之后,多久启动定时器//结构体
struct timeval
{__time_t tv_sec; /* Seconds. */__suseconds_t tv_usec; /* Microseconds. */
};//函数
int setitimer (__itimer_which_t __which,const struct itimerval *__restrict __new,struct itimerval *__restrict __old)/*setitimer()将value指向的结构体设为计时器的当前值,如果ovalue不是NULL,将返回计时器原有值。
which:三种类型
ITIMER_REAL //数值为0,计时器的值实时递减,发送的信号是SIGALRM。
ITIMER_VIRTUAL //数值为1,进程执行时递减计时器的值,发送的信号是SIGVTALRM。
ITIMER_PROF //数值为2,进程和系统执行时都递减计时器的值,发送的信号是SIGPROF。
很明显,这边需要捕获对应的信号进行逻辑相关处理 signal(SIGALRM,signal_handler);
返回说明:
成功执行时,返回0。失败返回-1*/
#include <sys/time.h>
#include <signal.h>// 设置定时器
struct itimerval timer;
timer.it_value.tv_sec = 1; // 首次触发时间
timer.it_value.tv_usec = 0;
timer.it_interval.tv_sec = 1; // 重复间隔
timer.it_interval.tv_usec = 0;setitimer(ITIMER_REAL, &timer, NULL);// 信号处理函数
void timer_handler(int sig) {// 处理定时任务
}signal(SIGALRM, timer_handler);
特点:
- 使用
ITIMER_REAL
产生SIGALRM
信号 - 精度可达微秒级
- 受信号处理机制限制(异步信号安全)
2. timerfd
(现代推荐方法)
struct itimerspec {struct timespec it_interval; // 定时器周期(0 表示单次触发)struct timespec it_value; // 首次到期时间(0 表示停止定时器)
};it_value:首次触发的延迟时间。
it_interval:周期性触发的间隔时间。如果为 0,则为单次定时器。timerfd_create(int clockid, int flags)
/*
clockid:指定定时器所基于的时钟源。常用值有:
CLOCK_REALTIME:系统实时时间(可被 NTP 调整)。
CLOCK_MONOTONIC:单调递增时间(不受系统时间调整影响,推荐用于定时)。
flags:可选标志位,如 TFD_CLOEXEC(执行 exec 时自动关闭)、TFD_NONBLOCK(非阻塞模式)。
*/timerfd_settime(int fd, int flags, const struct itimerspec *new_value, struct itimerspec *old_value)
/*
fd:由 timerfd_create 返回的文件描述符。
flags:标志位。0 表示 new_value->it_value 是相对时间;TFD_TIMER_ABSTIME 表示是绝对时间。
new_value:指向 itimerspec 结构体,指定新的定时器值。
old_value:可选,用于返回之前的定时器设置。
*/timerfd_gettime(int fd, struct itimerspec *curr_value)
/*
fd:timerfd 文件描述符。
curr_value:指向 itimerspec 结构体,用于接收当前值。
*/
#include <sys/timerfd.h>int timerfd = timerfd_create(CLOCK_REALTIME, 0);struct itimerspec timer;
timer.it_value.tv_sec = 1;
timer.it_value.tv_nsec = 0;
timer.it_interval.tv_sec = 1;
timer.it_interval.tv_nsec = 0;timerfd_settime(timerfd, 0, &timer, NULL);// 在epoll/select中监听
uint64_t exp;
read(timerfd, &exp, sizeof(exp));
优势:
- 基于文件描述符,可与epoll集成
- 避免信号处理的复杂性
- 更精确的控制
3. timer_create()
(POSIX定时器)
#include <signal.h>
#include <time.h>timer_t timerid;
struct sigevent sev;
struct itimerspec timer;// 创建定时器
sev.sigev_notify = SIGEV_THREAD;
sev.sigev_notify_function = timer_callback;
timer_create(CLOCK_REALTIME, &sev, &timerid);// 启动定时器
timer_settime(timerid, 0, &timer, NULL);
关键要点
- 信号安全:在信号处理函数中只能调用异步信号安全的函数
- 精度限制:受系统时钟分辨率影响(通常1-10ms)
- 线程安全:现代应用推荐使用
timerfd
避免信号竞争 - 多种时钟源:
CLOCK_REALTIME
,CLOCK_MONOTONIC
等
工作流程
- 调用
timerfd_create
创建一个 timerfd,得到一个文件描述符fd
。 - 设置
itimerspec
结构体,定义定时器的首次触发时间和周期。 - 调用
timerfd_settime(fd, 0, &timer_spec, NULL)
启动定时器。 - 在事件循环中(如
epoll_wait
),监控该fd
的可读事件。 - 当定时器到期时,
fd
变为可读。读取该fd
(通常读取 8 字节)可以获取到期次数,并清除可读状态。 - 可以再次调用
timerfd_settime
来修改或重启定时器。
#include <sys/timerfd.h>
#include <time.h>
#include <unistd.h>
#include <stdint.h>
#include <stdio.h>int main() {// 1. 创建 timerfdint tfd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC);if (tfd == -1) {perror("timerfd_create");return 1;}// 2. 设置定时器:2秒后首次触发,之后每1秒触发一次struct itimerspec timer_spec;timer_spec.it_value.tv_sec = 2; // 首次延迟 2 秒timer_spec.it_value.tv_nsec = 0;timer_spec.it_interval.tv_sec = 1; // 周期 1 秒timer_spec.it_interval.tv_nsec = 0;if (timerfd_settime(tfd, 0, &timer_spec, NULL) == -1) {perror("timerfd_settime");close(tfd);return 1;}// 3. 模拟事件循环(这里简化为直接 read)uint64_t expirations;while (1) {// read 会阻塞直到定时器到期ssize_t s = read(tfd, &expirations, sizeof(uint64_t));if (s != sizeof(uint64_t)) {// 处理错误break;}printf("Timer expired %lu times\n", expirations);}close(tfd);return 0;
}
setitimer()和timerfd的优缺点对比是什么?
setitimer()
优点:
- 简单易用:API 简单,只需要设置
struct itimerval
并调用setitimer()
。 - POSIX 标准:跨平台兼容性较好(在支持 POSIX 的系统上)。
- 微秒级精度:支持微秒(μs)级别的定时精度。
- 轻量级:不需要额外的系统调用或复杂的设置。
缺点:
- 信号依赖:依赖信号(如
SIGALRM
)进行通知,信号处理复杂且容易出错。 - 异步信号安全限制:在信号处理函数中只能调用异步信号安全的函数(如
write()
、sigprocmask()
),不能调用printf()
、malloc()
等。 - 单一定时器限制:每个进程只能有 三个
setitimer
定时器(ITIMER_REAL
、ITIMER_VIRTUAL
、ITIMER_PROF
),无法创建多个独立定时器。 - 难以集成 I/O 多路复用:信号不是文件描述符,无法直接用于
select()
、poll()
、epoll()
等事件循环。 - 竞态条件:信号可能被阻塞或丢失,处理不当会导致定时不准或逻辑错误。
适用场景:简单的单一定时任务,如心跳检测、超时控制等。
timerfd
优点:
- 基于文件描述符:返回一个文件描述符(fd),可以无缝集成到
epoll
、poll
、select
等 I/O 多路复用机制中,非常适合事件驱动架构(如 Reactor 模式)。 - 无信号干扰:不依赖信号,避免了信号处理的复杂性和安全问题。
- 支持多个定时器:每个
timerfd_create()
调用创建一个独立的定时器,可创建多个并发定时器。 - 高精度和灵活性:支持纳秒级精度(
struct itimerspec
),可设置绝对或相对时间,支持单次和周期性触发。 - 线程安全:多个线程可以安全地读取
timerfd
,适合多线程程序。 - 可读性高:通过
read()
读取超时次数,逻辑清晰,易于调试。
缺点:
- Linux 特有:
timerfd
是 Linux 特有的系统调用,不具备跨平台性。 - 稍微复杂:相比
setitimer()
,需要更多的设置步骤(创建、设置时间、读取等)。 - 需要处理读取:每次定时器到期后需要
read()
一次,否则可能会累积事件。
适用场景:高性能服务器、事件驱动程序、需要多个定时器的复杂应用。
对比总结表
特性 | setitimer() | timerfd |
---|---|---|
通知机制 | 信号(SIGALRM) | 文件描述符(可 epoll) |
定时器数量 | 最多 3 个 | 任意多个 |
精度 | 微秒级 | 纳秒级 |
跨平台性 | POSIX 兼容 | Linux 特有 |
I/O 多路复用集成 | 不支持 | 支持(epoll/poll/select) |
信号安全问题 | 有 | 无 |
复杂度 | 简单 | 中等 |
适用场景 | 简单定时任务 | 高性能、事件驱动系统 |
结论
- 如果你写的是 简单的脚本或工具,且只需要一个定时器,
setitimer()
足够用。 - 如果你写的是 高性能服务器、网络程序或事件驱动框架(如 Redis、Nginx 风格),强烈推荐使用
timerfd
,因为它更现代、更安全、更灵活。