血泪教训:Linux 定时器踩坑指南,看完少走三年弯路

大家好,我是小康。

朋友们,今天要跟大家聊个让无数程序员头疼的话题——Linux定时器。别看这玩意儿平时不起眼,但真要用起来,坑多得你想哭😭

一、写在前面的话

你有没有遇到过这样的场景?

写个网络程序,需要定期发送心跳包做个游戏服务器,要每秒更新玩家状态搞个监控系统,定时检查服务是否正常甚至只是想让程序延时几秒再执行某个操作

如果你点头了,那恭喜你——定时器绝对是你绕不开的技能点!

我记得刚开始写Linux程序的时候,遇到需要定时执行任务的场景,第一反应就是Google一下"Linux定时器怎么用"。结果搜出来一堆alarm()、setitimer()、timerfd_create()...看得我一头雾水。

到底该用哪个?它们有什么区别?为什么有这么多种定时器?

相信很多小伙伴都有过同样的困惑。今天咱们就来彻底搞懂Linux定时器的前世今生,保证看完之后你也能成为定时器专家!

二、第一代:古老而经典的alarm()

1. 最简单的开始

话说回来,Linux最早的定时器就是alarm(),简单到爆:

复制
#include <unistd.h> #include <signal.h> #include <stdio.h> void timeout_handler(int sig) { printf("时间到!该起床搬砖了!\n"); } int main() { signal(SIGALRM, timeout_handler); alarm(5); // 5秒后触发 pause(); // 等待信号 return 0; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.

看起来挺简单的对吧? 但是兄弟,这里面的坑可不少:

只能精确到秒 - 你想要毫秒级定时?不好意思,做不到全局只能有一个 - 你在一个地方调用了alarm(10),另一个地方又调用alarm(5),前面那个就被覆盖了容易被系统调用中断 - sleep()、read()这些函数被SIGALRM打断后会提前返回2. 真实踩坑经历

我当年就因为不知道alarm()是全局唯一的,在一个多模块的项目里用了好几个alarm(),结果定时器莫名其妙地不按预期工作。调试了好久才发现是被互相覆盖了。

三、第二代:更灵活的setitimer()

1. 进步在哪里?

既然alarm()这么局限,Linux就推出了升级版——setitimer():

复制
#include <sys/time.h> #include <signal.h> #include <stdio.h> void timer_handler(int sig) { staticint count = 0; printf("第%d次定时触发!\n", ++count); } int main() { struct itimerval timer; signal(SIGALRM, timer_handler); // 设置定时器:1秒后开始,每0.5秒触发一次 timer.it_value.tv_sec = 1; // 首次触发时间 timer.it_value.tv_usec = 0; timer.it_interval.tv_sec = 0; // 重复间隔 timer.it_interval.tv_usec = 500000; // 0.5秒 = 500000微秒 setitimer(ITIMER_REAL, &timer, NULL); while(1) { pause(); // 等待信号 } return 0; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.

这就厉害多了!

支持微秒级精度可以设置周期性触发有三种定时器类型(REAL、VIRTUAL、PROF)2. 但是...新的问题来了

虽然setitimer()比alarm()强大,但还是有些让人头疼的地方:

还是基于信号 - 信号处理的那些坑一个都没少每个进程还是只能有一个ITIMER_REAL - 多个定时器?也不支持信号可能丢失 - 在信号处理函数执行期间,新的信号可能被丢弃

四、第三代:专业级的POSIX定时器

1. 更加专业的选择

在timerfd出现之前,还有一个重要的过渡产品——POSIX定时器(timer_create系列)。这玩意儿是POSIX标准定义的,比setitimer()更专业,但又没有timerfd()那么现代化。

复制
#include <time.h> #include <signal.h> #include <stdio.h> timer_t timerid; int timer_count = 0; void timer_handler(int sig, siginfo_t *si, void *uc) { timer_t *tidp = si->si_value.sival_ptr; printf("第%d次POSIX定时器触发!timer_id: %p\n", ++timer_count, tidp); } int main() { struct sigevent sev; struct itimerspec its; struct sigaction sa; // 设置信号处理函数 sa.sa_flags = SA_SIGINFO; sa.sa_sigaction = timer_handler; sigemptyset(&sa.sa_mask); sigaction(SIGUSR1, &sa, NULL); // 创建定时器 sev.sigev_notify = SIGEV_SIGNAL; sev.sigev_signo = SIGUSR1; sev.sigev_value.sival_ptr = &timerid; if (timer_create(CLOCK_REALTIME, &sev, &timerid) == -1) { perror("timer_create failed"); return-1; } // 设置定时器参数:1秒后开始,每500ms触发一次 its.it_value.tv_sec = 1; its.it_value.tv_nsec = 0; its.it_interval.tv_sec = 0; its.it_interval.tv_nsec = 500000000; // 500ms timer_settime(timerid, 0, &its, NULL); printf("POSIX定时器启动,按Ctrl+C退出\n"); while (1) { pause(); } timer_delete(timerid); return 0; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.

看起来是不是比setitimer()复杂多了? 但功能也更强大:

2. POSIX定时器的优势支持多个定时器 - 终于可以创建多个了!每个都有独立的timer_t标识纳秒级精度 - 和timerfd一样精确灵活的通知方式 - 不仅可以发信号,还可以创建线程或者什么都不做更好的信息传递 - 可以通过siginfo_t传递额外信息3. 三种通知方式

POSIX定时器最酷的地方是支持三种通知方式:

(1) 信号通知(最常用)

复制
sev.sigev_notify = SIGEV_SIGNAL; sev.sigev_signo = SIGUSR1;1.2.

(2) 线程通知(高级用法)

复制
sev.sigev_notify = SIGEV_THREAD; sev.sigev_notify_function = thread_handler; sev.sigev_notify_attributes = NULL;1.2.3.

(3) 无通知(轮询模式)

复制
sev.sigev_notify = SIGEV_NONE; // 然后用timer_gettime()主动查询1.2.
4. 我的使用心得

POSIX定时器我在一个服务器监控项目中用过,需要同时监控多个不同的指标,每个指标的检查频率都不一样。用setitimer()根本搞不定,但POSIX定时器就很合适:

复制
timer_t cpu_timer, memory_timer, disk_timer, network_timer; // CPU使用率:每秒检查一次 create_posix_timer(&cpu_timer, SIGUSR1, 1000); // 内存使用率:每30秒检查一次 create_posix_timer(&memory_timer, SIGUSR2, 300000); // 磁盘IO:每分钟检查一次 create_posix_timer(&disk_timer, SIGRTMIN, 600000); // 网络连接:每分钟检查一次 create_posix_timer(&network_timer, SIGRTMIN+1, 600000);1.2.3.4.5.6.7.8.9.10.11.12.13.

这样每个监控任务都有自己独立的定时器,互不干扰,代码逻辑也很清晰。

但是...POSIX定时器也有它的问题:

还是基于信号 - 信号处理的坑一个都没少代码复杂 - 比alarm()和setitimer()复杂多了移植性问题 - 有些老系统支持不够好

所以虽然功能强大,但在现代Linux开发中,大家更倾向于直接用timerfd。

五、第四代:现代化的timerfd

1. 革命性的改变

到了Linux 2.6.25,终于迎来了真正的现代化定时器——timerfd!

这东西彻底改变了游戏规则:把定时器变成了文件描述符!

复制
#include <sys/timerfd.h> #include <unistd.h> #include <stdio.h> #include <stdint.h> int main() { int timer_fd; struct itimerspec timer_spec; uint64_t expirations; // 创建定时器文件描述符 timer_fd = timerfd_create(CLOCK_REALTIME, 0); if (timer_fd == -1) { perror("timerfd_create failed"); return-1; } // 设置定时器:2秒后开始,每1秒触发一次 timer_spec.it_value.tv_sec = 2; timer_spec.it_value.tv_nsec = 0; timer_spec.it_interval.tv_sec = 1; timer_spec.it_interval.tv_nsec = 0; timerfd_settime(timer_fd, 0, &timer_spec, NULL); printf("定时器启动,等待触发...\n"); for (int i = 0; i < 5; i++) { // 就像读文件一样读取定时器 ssize_t bytes = read(timer_fd, &expirations, sizeof(expirations)); if (bytes == sizeof(expirations)) { printf("定时器触发了%llu次\n", expirations); } } close(timer_fd); return 0; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.

这简直是质的飞跃!

2. 为什么timerfd这么香?文件描述符 - 可以用select()、poll()、epoll()监听,完美融入事件循环纳秒级精度 - 想要多精确有多精确无限个定时器 - 想创建多少个就创建多少个不依赖信号 - 再也不用担心信号处理的各种坑更好的并发支持 - 在事件驱动的程序中表现出色3. 配合epoll使用更香
复制
#include <stdio.h> #include <unistd.h> #include <sys/timerfd.h> #include <sys/epoll.h> #include <stdint.h> int main() { int timerfd1, timerfd2, epollfd; struct itimerspec its; struct epoll_event ev, events[10]; uint64_texp; // 创建两个定时器 timerfd1 = timerfd_create(CLOCK_REALTIME, 0); timerfd2 = timerfd_create(CLOCK_REALTIME, 0); // 创建epoll实例 epollfd = epoll_create1(0); // 将定时器加入epoll监听 ev.events = EPOLLIN; ev.data.fd = timerfd1; epoll_ctl(epollfd, EPOLL_CTL_ADD, timerfd1, &ev); ev.data.fd = timerfd2; epoll_ctl(epollfd, EPOLL_CTL_ADD, timerfd2, &ev); // 设置定时器1:每1秒触发 its.it_value.tv_sec = 1; its.it_value.tv_nsec = 0; its.it_interval.tv_sec = 1; its.it_interval.tv_nsec = 0; timerfd_settime(timerfd1, 0, &its, NULL); // 设置定时器2:每2秒触发 its.it_value.tv_sec = 2; its.it_interval.tv_sec = 2; timerfd_settime(timerfd2, 0, &its, NULL); printf("高性能定时器系统启动!\n"); while (1) { int nfds = epoll_wait(epollfd, events, 10, -1); for (int n = 0; n < nfds; n++) { int fd = events[n].data.fd; read(fd, &exp, sizeof(uint64_t)); if (fd == timerfd1) { printf("⚡ 快速定时器触发 (1秒间隔)\n"); } elseif (fd == timerfd2) { printf("🐌 慢速定时器触发 (2秒间隔)\n"); } } } return 0; }1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.25.26.27.28.29.30.31.32.33.34.35.36.37.38.39.40.41.42.43.44.45.46.47.48.49.50.51.52.53.54.55.56.57.58.

这就是现代Linux程序的标准写法! 事件驱动,高性能,代码还清晰易懂。

六、实际项目中该选哪个?

1. 快速决策指南

如果你只是想要个简单的定时:

复制
alarm(5); // 够用了,别想太多1.

如果需要周期性定时,而且精度要求不高:

复制
setitimer(ITIMER_REAL, &timer, NULL); // 经典选择1.

如果需要多个定时器,但不想用太新的API:

复制
timer_create() + timer_settime(); // POSIX标准,兼容性好1.

如果是现代项目,特别是网络服务器:

复制
timerfd_create() + epoll(); // 这就对了!1.
2. 性能对比

我之前做过一个简单的功能测试,看看各种定时器的支持能力:

alarm(): 全局只能有1个,新的会覆盖旧的setitimer(): 每种类型只能1个(REAL、VIRTUAL、PROF),最多3个POSIX定时器: 支持多个,具体数量受系统限制(通常几百个),但信号处理开销较大timerfd(): 支持多个,数量主要受文件描述符限制

实际项目中的选择建议:

如果只需要1-2个定时器:setitimer()够用如果需要多个定时器:POSIX定时器和timerfd都可以,但timerfd在事件驱动程序中更高效如果是高并发网络程序:timerfd() + epoll()性能最好,因为可以和其他I/O事件统一处理3. 兼容性考虑alarm()/setitimer(): 几乎所有Unix系统都支持POSIX定时器: 理论上是POSIX标准,但实际支持情况复杂:Linux 2.6+原生支持(但可能需要链接 -lrt),macOS/BSD支持有限,Windows需要通过Cygwin等兼容层timerfd(): Linux 2.6.25+专有,其他系统不支持

实际上,跨平台的定时器API是一个普遍难题,每个操作系统都有自己的实现方式。如果你的项目需要真正的跨平台,可能需要:

使用第三方库(如libuv、libevent)或者针对不同平台编写不同的实现

七、进阶技巧分享

1. 高精度定时器

想要更高的精度?试试CLOCK_MONOTONIC:

复制
timer_fd = timerfd_create(CLOCK_MONOTONIC, 0);1.

CLOCK_MONOTONIC不受系统时间调整影响,更适合做精确的间隔定时。

2. 一次性定时器

有时候你只想要一个一次性的延时:

复制
timer_spec.it_value.tv_sec = 5; // 5秒后触发 timer_spec.it_value.tv_nsec = 0; timer_spec.it_interval.tv_sec = 0; // 不重复 timer_spec.it_interval.tv_nsec = 0;1.2.3.4.
3. 定时器管理器

在复杂项目中,你可能需要管理很多定时器。我一般会封装一个定时器管理器:

复制
typedef struct { int fd; void (*callback)(void *data); void *data; } Timer; // 创建定时器 Timer* create_timer(int interval_ms, void (*callback)(void*), void *data); // 删除定时器 void destroy_timer(Timer *timer); // 在主事件循环中处理定时器事件 void handle_timer_event(Timer *timer);1.2.3.4.5.6.7.8.9.10.11.12.

这样管理起来就清爽多了。

八、总结:定时器进化的启示

从alarm()到timerfd(),Linux定时器的进化史其实反映了整个系统编程的发展趋势:

从简单到复杂 - 功能越来越强大从单一到多元 - 支持更多使用场景从同步到异步 - 更好地融入事件驱动架构从信号到文件描述符 - 统一的编程模型

如果你是新手,建议从alarm()开始理解基本概念,了解一下POSIX定时器的功能特性,然后直接跳到timerfd()学习现代用法。

如果你是老手,是时候把那些老旧的alarm()和setitimer()代码重构了。如果项目只在Linux上运行,直接用timerfd();如果需要跨平台,考虑使用成熟的第三方库。

选择建议总结:

学习路径: alarm() → POSIX定时器概念 → timerfd()实践跨平台项目: 使用libuv、libevent等成熟库,别自己造轮子Linux专项目: 直接用timerfd() + epoll()简单脚本: alarm()够用,别过度设计

THE END