3、POSIX定时器接口:timer_create/timer_settime/timer_gettime/timer_delete、信号驱动与线程驱动模式对比、POSIX定时器在VxWorks中的实现差异

好,咱们今天聊聊POSIX定时器。说实话,这个接口在VxWorks里是个好东西,但也是个容易踩坑的地方。我最早接触它是在一个多传感器数据采集项目里,当时需要同时管理七八个不同周期的定时任务,用传统的watchdog方式根本搞不定。后来换成POSIX定时器,整个世界清静了。

3.1 核心API:四个函数搞定一切

POSIX定时器的接口非常简洁,就四个函数。你想想看,一个定时器的完整生命周期,从创建到设置、查询、删除,全包了。

3.1.1 timer_create — 创建定时器

这个函数负责创建一个定时器对象。原型长这样:

#include <signal.h>
#include <time.h>

int timer_create(clockid_t clockid, 
                 struct sigevent *evp, 
                 timer_t *timerid);

参数说明:

  • clockid:时钟源。常用的是CLOCK_REALTIME(系统实时时钟)和CLOCK_MONOTONIC(单调递增时钟)。我个人习惯用CLOCK_MONOTONIC,因为它不受系统时间调整影响,做周期性任务更靠谱。
  • evp:事件通知方式。这是关键,决定了定时器到期后怎么通知你。后面会细讲。
  • timerid:输出参数,返回定时器句柄。
注意:VxWorks里timer_t类型是个指针,不是整数。别拿它跟Linux下的实现混为一谈。我曾经见过有人把timer_t当int用,结果传参传错了,定时器根本跑不起来。

3.1.2 timer_settime — 启动/修改定时器

创建完定时器,得告诉它什么时候触发、要不要重复。这就是timer_settime的活:

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

itimerspec结构体包含两个成员:

  • it_interval:周期间隔。如果为0,就是单次定时器。
  • it_value:首次到期时间。如果为0,停止定时器。

举个例子,创建一个每100毫秒触发一次的定时器:

struct itimerspec ts;
ts.it_interval.tv_sec = 0;
ts.it_interval.tv_nsec = 100000000;  // 100ms
ts.it_value.tv_sec = 0;
ts.it_value.tv_nsec = 100000000;     // 首次也是100ms后触发

timer_settime(my_timer, 0, &ts, NULL);
小技巧:如果你想要定时器立即触发一次,可以把it_value设成0。但注意,设成0其实是停止定时器。正确的做法是设一个极小的值,比如1纳秒。

3.1.3 timer_gettime — 查询剩余时间

这个函数用来看看定时器还有多久到期:

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

返回的it_value就是距离下次到期还剩多少时间。如果返回0,说明定时器已经停止了。我在调试的时候经常用这个函数来确认定时器是不是按预期在跑。

3.1.4 timer_delete — 销毁定时器

用完记得销毁,别造成资源泄漏:

int timer_delete(timer_t timerid);

嗯,这里要注意:如果定时器正在运行,timer_delete会先停止它再销毁。但如果有未处理的信号或回调,可能会出问题。我建议在删除前先调用timer_settime把定时器停掉,确保安全。

3.2 信号驱动 vs 线程驱动:两种模式怎么选?

这是POSIX定时器最核心的设计选择。说白了,就是定时器到期后,系统怎么通知你。

3.2.1 信号驱动模式

这是POSIX标准默认的方式。创建定时器时,sigevent结构体里指定信号编号:

struct sigevent ev;
ev.sigev_notify = SIGEV_SIGNAL;
ev.sigev_signo = SIGRTMIN;  // 使用实时信号
ev.sigev_value.sival_ptr = my_timer;

timer_create(CLOCK_MONOTONIC, &ev, &my_timer);

定时器到期时,内核会向你的任务发送一个信号。你需要注册信号处理函数来响应。

优点:

  • 开销小,不需要额外线程
  • 响应快,信号是异步通知

缺点:

  • 信号处理函数里能干的事有限。不能调用printf、malloc等非重入函数
  • 多个定时器共用一个信号时,你得自己区分是哪个定时器触发的
  • 信号可能丢失。如果信号来得太频繁,内核会合并或丢弃
我的经验:信号驱动模式适合简单的超时处理,比如网络重传超时。但如果你要做复杂的周期性任务,我建议你慎重。我曾经在一个项目里用信号驱动做10ms周期的数据采集,结果信号处理函数里调用了锁,直接死锁了。排查了两天才找到原因。

3.2.2 线程驱动模式

VxWorks对POSIX标准做了扩展,支持用线程来处理定时器事件:

struct sigevent ev;
ev.sigev_notify = SIGEV_THREAD;
ev.sigev_notify_function = my_timer_callback;
ev.sigev_notify_attributes = NULL;  // 使用默认线程属性
ev.sigev_value.sival_ptr = my_data;

timer_create(CLOCK_MONOTONIC, &ev, &my_timer);

定时器到期时,系统会创建一个新线程来执行my_timer_callback函数。

优点:

  • 回调函数里可以随便写,没有信号处理函数的限制
  • 每个定时器有自己的线程上下文,互不干扰
  • 代码逻辑清晰,容易维护

缺点:

  • 线程创建有开销。如果定时器频率很高(比如1ms),频繁创建线程会拖垮系统
  • 需要处理线程同步问题。多个定时器回调可能同时访问共享资源
  • 线程栈大小要合理设置,否则容易栈溢出
建议:对于周期大于50ms的定时任务,用线程驱动模式。对于高频定时任务,用信号驱动模式或者干脆用VxWorks原生的watchdog定时器。

3.3 VxWorks中的实现差异

VxWorks的POSIX定时器实现跟标准的Linux实现有几个关键差异,我踩过不少坑,给你总结一下:

特性 标准POSIX VxWorks实现
时钟源支持 CLOCK_REALTIME, CLOCK_MONOTONIC等 额外支持CLOCK_TIMER(VxWorks专用高精度时钟)
线程驱动模式 可选(部分系统支持) 原生支持,且性能优化过
定时器精度 取决于系统时钟滴答 支持纳秒级精度(需硬件支持)
最大定时器数量 由系统限制 可配置,默认通常为256个
信号处理 标准信号机制 VxWorks信号处理有优先级,实时信号优先级更高

第一个差异:时钟源选择

VxWorks提供了CLOCK_TIMER这个专用时钟源。它直接绑定到硬件定时器,精度比CLOCK_MONOTONIC更高。我在做电机控制项目时,要求PWM周期误差不超过1微秒,用CLOCK_TIMER才搞定。

timer_t motor_pwm_timer;
struct sigevent ev;
ev.sigev_notify = SIGEV_THREAD;
ev.sigev_notify_function = pwm_callback;

// 使用高精度时钟源
timer_create(CLOCK_TIMER, &ev, &motor_pwm_timer);

第二个差异:线程驱动模式的实现

VxWorks的SIGEV_THREAD模式跟Linux不太一样。Linux下每次定时器到期都会创建一个新线程,而VxWorks会复用线程池中的线程,减少了创建开销。但要注意,如果回调函数执行时间超过了定时器周期,下一个到期事件可能会被阻塞。

避坑指南:我曾经在VxWorks 6.9上遇到过一个bug——使用SIGEV_THREAD模式时,如果回调函数里调用了taskDelay(),会导致定时器线程池耗尽,其他定时器全部失效。后来我改用信号驱动+任务间通信的方式才解决。所以,回调函数里尽量不要做阻塞操作。

第三个差异:定时器精度与系统配置

VxWorks的定时器精度受系统时钟滴答影响。默认情况下,系统时钟滴答是60Hz(约16.67ms),这意味着你设的1ms定时器实际上只能以16.67ms的粒度工作。要获得高精度,需要做两件事:

  1. 在VxWorks内核配置中启用高精度定时器(INCLUDE_HIGH_RES_TIMER
  2. 使用sysClkRateSet()提高系统时钟频率,或者直接使用硬件定时器
// 提高系统时钟频率到1000Hz
sysClkRateSet(1000);

// 现在定时器精度可以达到1ms
struct itimerspec ts;
ts.it_interval.tv_sec = 0;
ts.it_interval.tv_nsec = 1000000;  // 1ms

第四个差异:定时器资源管理

VxWorks的定时器资源是静态分配的。默认情况下,系统最多支持256个POSIX定时器。如果你需要更多,得在VxWorks镜像配置里修改NUM_TIMERS参数。我建议你在设计阶段就评估好定时器数量,别等到运行时才发现不够用。

好了,POSIX定时器的核心内容就这些。总结一下:

  • 四个API函数,记住它们的用法和参数含义
  • 信号驱动适合简单场景,线程驱动适合复杂场景
  • VxWorks的实现有差异,特别是时钟源和线程模式
  • 高精度定时需要调整系统配置

下一章我们会讲VxWorks原生的看门狗定时器,那又是另一套玩法了。