上一讲我们探讨了时间函数与时区陷阱,强调了统一使用UTC进行存储、展示时再转换本地时区,夏令时处理、线程安全API(如localtime_r)、以及时区数据库更新的重要性。
1. 主题原理与细节逐步讲解
C语言及POSIX提供多种定时器机制,包括:
- 信号定时器(如
alarm()、setitimer()、timer_create()等) - 轮询定时器(如在循环体内用
gettimeofday(),sleep()或usleep()实现) - 定时信号(如
SIGALRM,SIGVTALRM,SIGPROF)
信号定时器的核心思想:在指定时间后,由系统向进程发送信号(如SIGALRM),用户可以通过信号处理函数(signal handler)响应定时事件。
典型API:
alarm(seconds):seconds 秒后发送 SIGALRM 信号,仅支持一次性定时。setitimer():支持微秒级精度和周期性定时,可指定 ITIMER_REAL(真实时间),ITIMER_VIRTUAL(进程运行时间),ITIMER_PROF(进程加上系统时间)。timer_create()/timer_settime():POSIX高精度定时器,支持多定时器和信号通知。
2. 典型陷阱/缺陷及成因剖析
2.1 定时器被信号干扰或丢失
成因:
- 信号处理是异步的,如果多个定时器信号短时间内叠加,可能有信号被合并或丢失。
- 信号处理函数不可重入,若未正确保护,可能造成竞态或死锁。
2.2 重复设置定时器导致前一个定时器被取消
成因:
alarm()和setitimer()每次调用会覆盖之前的定时器,只能有一个同类定时器生效。- 多模块代码互相调用
alarm(),导致定时器混乱。
2.3 信号处理函数内调用非异步安全函数
成因:
- 在信号处理函数内调用如
printf,malloc,free,exit等非异步安全函数,可能导致崩溃或行为未定义。
2.4 定时精度不保证
成因:
- 定时器到期受系统调度、信号排队影响,实际回调时间可能有显著延迟。
- 微秒级定时器在高负载或虚拟化环境下精度下降。
2.5 多定时器信号混淆
成因:
- 使用
SIGALRM等通用信号,多个定时器无法区分来源,导致不同逻辑混淆。
2.6 进程/线程与定时器生命周期不一致
成因:
- 信号定时器与主线程生命周期绑定,fork/exec后定时器状态不一致,子进程、线程与主进程可能混乱。
3. 规避方法与最佳实践
- 优先选用
timer_create()等POSIX定时器,支持多个独立定时器和带参数信号,避免混淆。 - 信号处理函数只做最简处理,如设置标志位,主循环定期检查并处理业务逻辑。
- 避免多模块直接调用
alarm()/setitimer(),统一由主控调度。 - 信号处理函数内只用异步安全函数(见man 7 signal-safety),不可malloc/printf等。
- 考虑定时精度需求,关键业务应使用高分辨率定时器(如
clock_gettime()结合主循环轮询)。 - 多线程场景推荐使用专用线程和条件变量、事件fd等机制实现定时,而不是信号。
- 定时器事件参数化设计,使用
siginfo_t传递定时器标识,避免事件混淆。
4. 错误代码与优化代码对比
错误示例1:信号处理函数内做复杂操作
void sigalrm_handler(int signo) {
printf("Timer expired!\n"); // 非异步安全
// ... 复杂业务逻辑
}
int main() {
signal(SIGALRM, sigalrm_handler);
alarm(5);
pause();
}
优化后:只设置标志位/写pipe/使用eventfd,主循环处理业务
volatile sig_atomic_t timer_expired = 0;
void sigalrm_handler(int signo) {
timer_expired = 1; // 仅设置标志位
}
int main() {
signal(SIGALRM, sigalrm_handler);
alarm(5);
while (!timer_expired) {}
printf("Timer expired!\n");
}
错误示例2:多个alarm覆盖导致定时器丢失
alarm(10);
alarm(5); // 覆盖上一个定时器,只有5秒生效
优化后:用POSIX定时器,每个定时器独立
#include <signal.h>
#include <time.h>
void timer_handler(union sigval sv) {
printf("Timer ID: %d expired\n", *(int*)sv.sival_ptr);
}
int main() {
struct sigevent sev = {0};
timer_t tid[2];
int ids[2] = {1, 2};
sev.sigev_notify = SIGEV_THREAD; // 线程方式回调
sev.sigev_value.sival_ptr = &ids[0];
timer_create(CLOCK_REALTIME, &sev, &tid[0]);
struct itimerspec its = { {0,0}, {10,0} };
timer_settime(tid[0], 0, &its, NULL);
sev.sigev_value.sival_ptr = &ids[1];
timer_create(CLOCK_REALTIME, &sev, &tid[1]);
its.it_value.tv_sec = 5;
timer_settime(tid[1], 0, &its, NULL);
pause(); // 等待
}
错误示例3:信号定时器多线程混用
// 多线程环境下直接用 setitimer/alarm,可能引发混乱
setitimer(ITIMER_REAL, ...);
优化后:用专用定时线程+事件fd/条件变量,线程安全且可扩展
5. 底层原理补充
- 信号定时器由内核定时,到期后通过信号框架异步通知进程;信号仅能排队一次,短时间内多次到期只收到一个信号。
timer_create()可创建多个定时器,并通过siginfo_t传递定时器标识和参数,适合复杂场景。- 信号处理函数受异步安全约束,见
man 7 signal-safety。 - 轮询定时器不依赖信号,适合高精度和多线程场景,但需合理调度避免资源浪费。
6. 示意:定时器事件流

7. 总结与实际建议
- 信号定时器易丢失、合并信号、不支持多定时器并发,主流多线程/高并发建议用POSIX定时器或自定义轮询机制。
- 信号处理函数只做最简标志设置,避免异步安全问题。
- 定时器事件要参数化管理,避免多业务混淆。
- 高精度/高可靠需求场景用专用线程和事件通知机制实现定时。
- 合理选择定时器类型和API,理解实际精度和异步特性,保证业务稳定性。
核心建议:信号定时器虽便捷但隐藏诸多陷阱,实际工程应选用更安全、可扩展的定时机制,信号处理逻辑务必简化并异步安全。
公众号 | FunIO
微信搜一搜 “funio”,发现更多精彩内容。
个人博客 | blog.boringhex.top
2349

被折叠的 条评论
为什么被折叠?



