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_REALTIME 和 CLOCK_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_value 和 it_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 | 未检查返回值 | 读到过期的剩余时间 |
嗯,这三个函数讲完了。说白了,定时器服务就是「创建-设置-查询-删除」四步走。下一节我们讲定时器的中断处理,那才是真正考验功底的地方。