4、QNX定时器服务:TimerCreate()、TimerSettime()、TimerGettime() 函数详解

好,咱们接着聊定时器。上一节我们把定时器的数据结构捋了一遍,这一节直接上手干活——三个核心API:TimerCreate()TimerSettime()TimerGettime()。这三个函数,说白了就是定时器的「生老病死」:创建、设置、查询。

我个人习惯,写驱动前先把这几个API的签名背下来。不是死记硬背,而是理解每个参数背后的硬件行为。你想想看,定时器一旦跑起来,出问题就是实时性事故,轻则丢数据,重则系统崩溃。

4.1 TimerCreate() —— 定时器的出生

先看原型:

int TimerCreate(clockid_t clock_id, struct sigevent *event, timer_t *timerid);

三个参数,一个返回值。我来拆开讲。

clock_id:时钟源。QNX里常用的就两个:CLOCK_REALTIMECLOCK_MONOTONIC。前者是墙上时间,系统时间变了它也跟着变;后者是单调递增的,不受NTP或手动调时间影响。

我的经验:做嵌入式驱动,99%的场景用 CLOCK_MONOTONIC。为什么?你想想,如果系统时间被回拨了,CLOCK_REALTIME 的定时器可能直接「穿越」到过去,超时事件永远不会触发。我在一个车载项目中就踩过这个坑,后来全改成单调时钟了。

event:这是个 struct sigevent,告诉内核定时器到期后怎么通知你。常见方式有三种:

  • SIGEV_NONE:到期了啥也不干,纯查询用
  • SIGEV_SIGNAL:发信号,适合简单场景
  • SIGEV_PULSE:发脉冲,QNX消息传递的经典方式,驱动里最常用

timerid:输出参数,内核分配给你的定时器ID。后面所有操作都靠它。

返回值:成功返回0,失败返回-1并设置errno。

小技巧:创建定时器时,我建议把 timerid 初始化为 NULL,这样调试时一眼就能看出哪些定时器还没创建。

4.2 TimerSettime() —— 让定时器跑起来

创建完定时器,它只是个空壳。得用 TimerSettime() 给它上发条:

int TimerSettime(timer_t timerid, int flags, 
                 const struct itimerspec *value, 
                 struct itimerspec *ovalue);

四个参数,每个都有讲究。

timerid:刚才创建的那个ID。

flags:就两个值有用——0 表示绝对时间,TIMER_ABSTIME 表示相对时间。嗯,这里要注意,QNX的命名有点反直觉:TIMER_ABSTIME 其实是「绝对时间」的意思,别搞反了。

value:核心参数,struct itimerspec 结构体:

struct itimerspec {
    struct timespec it_interval;  // 周期时间
    struct timespec it_value;     // 首次超时时间
};
  • it_value 设成0,定时器立即停止
  • it_interval 设成0,就是单次定时器
  • 两个都非0,就是周期定时器

ovalue:输出参数,返回之前定时器的设置。不需要就传NULL。

我曾经犯过一个低级错误:把 it_valueit_interval 搞反了。结果定时器第一次到期后就不跑了,排查了半天才发现是周期设到了首次超时上。记住:it_value 是「第一响」,it_interval 是「后续响」。

4.3 TimerGettime() —— 看看定时器还剩多少

这个函数相对简单:

int TimerGettime(timer_t timerid, struct itimerspec *value);

两个参数:定时器ID和输出结构体。返回后,value->it_value 就是距离下次到期还剩多少时间。如果返回0,说明定时器已经到期或者没启动。

这个函数在调试时特别有用。我记得有一次,客户反馈说某个外设周期性中断不稳定。我就在中断处理函数里加了个 TimerGettime() 打印,发现定时器每次到期后重新加载的时间有几十微秒的抖动。后来查出来是内核调度延迟导致的,调整了线程优先级就解决了。

4.4 完整示例:一个周期定时器

光说不练假把式。来看一个完整的周期定时器示例:

#include <stdio.h>
#include <time.h>
#include <sys/siginfo.h>

int main() {
    timer_t timer_id;
    struct sigevent event;
    struct itimerspec timer_spec;

    // 1. 创建定时器,用脉冲通知
    event.sigev_notify = SIGEV_PULSE;
    event.sigev_coid = ConnectAttach(0, 0, 0, 0, 0);
    event.sigev_priority = 1;
    event.sigev_code = 0;

    if (TimerCreate(CLOCK_MONOTONIC, &event, &timer_id) == -1) {
        perror("TimerCreate failed");
        return 1;
    }

    // 2. 设置周期:100ms首次,100ms周期
    timer_spec.it_value.tv_sec = 0;
    timer_spec.it_value.tv_nsec = 100000000;  // 100ms
    timer_spec.it_interval.tv_sec = 0;
    timer_spec.it_interval.tv_nsec = 100000000;

    if (TimerSettime(timer_id, 0, &timer_spec, NULL) == -1) {
        perror("TimerSettime failed");
        return 1;
    }

    // 3. 查询剩余时间
    struct itimerspec remaining;
    TimerGettime(timer_id, &remaining);
    printf("剩余时间: %ld.%09ld 秒\n", 
           remaining.it_value.tv_sec, 
           remaining.it_value.tv_nsec);

    // 4. 清理
    TimerDelete(timer_id);
    return 0;
}

避坑指南ConnectAttach() 返回的通道ID记得在程序退出时 ConnectDetach(),否则会造成资源泄漏。我曾经在一个长期运行的服务里忘了释放,结果跑了三天后系统报「No such channel」错误。

4.5 三个函数的常见陷阱

函数 常见错误 后果
TimerCreate clock_id 选错 定时器受系统时间影响
TimerSettime flags 参数搞反 定时器立即到期或永不触发
TimerGettime 未检查返回值 读到过期的剩余时间

嗯,这三个函数讲完了。说白了,定时器服务就是「创建-设置-查询-删除」四步走。下一节我们讲定时器的中断处理,那才是真正考验功底的地方。