Day 70:定时器与信号定时器的问题

上一讲我们探讨了时间函数与时区陷阱,强调了统一使用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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值