4、事件等待与超时处理:阻塞机制、Ticks设置与错误处理

好,咱们接着聊事件等待。上一节讲了怎么发事件,这一节重点说说怎么等事件。

说白了,事件等待就是任务在那里「蹲着」,等某个条件满足。但蹲多久?蹲不到怎么办?这就是本节要解决的问题。

4.1 阻塞机制:任务是怎么「睡」过去的?

当一个任务调用 taskEventPend() 等待事件时,它会被挂起。这个挂起不是简单的死循环,而是有讲究的。

我打个比方:你派了个哨兵去等信号。哨兵不能一直瞪着眼睛不眨眼吧?VxWorks 的做法是——让这个任务进入阻塞态,把 CPU 让给其他任务。等事件来了,内核再把它唤醒。

具体来说,阻塞分两种:

  • 无条件阻塞:等不到就一直等,直到事件发生
  • 有条件阻塞:设置一个超时时间,时间到了还没等到,就醒过来

我个人习惯,除非是系统初始化这种必须死等的场景,否则都会加超时。为什么?你想想看,万一发事件的那个任务崩了,你这边的任务不就永远睡下去了?

核心机制:事件等待的阻塞,本质上是任务状态从 READY 变为 PEND。事件到达后,内核会检查等待队列,把匹配的任务重新置为 READY。

4.2 超时参数 Ticks:这个数字怎么设?

VxWorks 的超时单位是 tick(系统时钟节拍)。1 tick 对应多长时间?取决于你的系统配置。默认一般是 1ms 或 10ms。

代码里是这样写的:

/* 等待事件,超时 100 ticks */
STATUS status = taskEventPend(eventId, 
                              EVENT_MASK_1, 
                              EVENTS_WAIT_ANY, 
                              100);  /* 超时参数 */

这个 100 是什么意思?如果系统 tick 是 10ms,那就是等 1 秒。

这里有个坑——tick 不是精确的时间。我在项目中遇到过,某次把超时设成 50 ticks,以为等 500ms。结果系统 tick 被改成了 1ms,实际只等了 50ms。业务逻辑直接崩了。

所以我建议:

  • sysClkRateGet() 获取当前 tick 频率
  • 用宏定义把时间换算成 ticks,别写死数字
/* 推荐写法:用宏定义换算 */
#define TIMEOUT_MS  500          /* 500毫秒 */
#define TICKS_PER_MS (sysClkRateGet() / 1000)
#define TIMEOUT_TICKS (TIMEOUT_MS * TICKS_PER_MS)

STATUS status = taskEventPend(eventId, 
                              EVENT_MASK_1, 
                              EVENTS_WAIT_ANY, 
                              TIMEOUT_TICKS);

小技巧:如果你想让任务永远等下去,超时参数传 WAIT_FOREVER(也就是 0)。但除非你有十足的把握,否则别这么干。我曾经见过一个产品,因为某个驱动初始化失败,结果三个任务都在 WAIT_FOREVER,系统直接「死机」。

4.3 超时后的错误处理:别让异常变成灾难

超时了怎么办?taskEventPend() 会返回 ERROR,然后你可以通过 errnoGet() 拿到具体的错误码。

常见的错误码就两个:

错误码 含义 典型场景
S_objLib_TIMEOUT 超时了,没等到事件 对方任务处理太慢,或者根本没发事件
S_objLib_OBJ_UNAVAILABLE 事件对象被删了 其他任务把 eventId 给删了

处理代码一般长这样:

STATUS status = taskEventPend(eventId, 
                              EVENT_MASK_DATA_READY, 
                              EVENTS_WAIT_ANY, 
                              TIMEOUT_TICKS);

if (status == ERROR) {
    int errCode = errnoGet();
    
    if (errCode == S_objLib_TIMEOUT) {
        /* 超时处理:重试?报错?降级? */
        logMsg("事件等待超时,重试第%d次\n", retryCount++, 0,0,0,0,0);
        
        if (retryCount < MAX_RETRY) {
            /* 重试 */
            goto retry;
        } else {
            /* 彻底失败,走错误恢复流程 */
            handleFatalError();
        }
    } 
    else if (errCode == S_objLib_OBJ_UNAVAILABLE) {
        /* 事件对象被删了,这属于严重错误 */
        logMsg("严重错误:事件对象已被删除!\n", 0,0,0,0,0,0);
        taskDelete(0);  /* 自杀 */
    }
}

警告:千万不要在超时后什么都不做就直接重试!我曾经接手过一个项目,代码里超时后直接 taskDelay(1) 然后重试,结果因为事件永远不会来,这个任务就在那里空转,CPU 占用率直接飙到 30%。

4.4 实战中的几个决策点

嗯,这里我要多说几句。超时处理不是简单的「等不到就报错」,你得根据业务场景来:

  1. 重试策略:是立即重试?还是等一会再重试?我一般用指数退避,第一次等 10 ticks,第二次等 20,第三次等 40……
  2. 降级处理:如果超时了,能不能用旧数据?能不能走另一条路径?
  3. 记录现场:超时的时候,把当时的任务栈、事件状态都打出来,方便事后分析

举个例子,我在做某个通信协议栈的时候,底层收包任务偶尔会超时。一开始我直接报错重启,后来发现其实是网络瞬间拥塞。改成重试 3 次后,系统稳定性从 99.9% 提升到了 99.99%。

所以你看,超时处理不是简单的 if-else,它关系到整个系统的健壮性。

4.5 小结

这一节的核心就三句话:

  • 事件等待是阻塞的,但别让它变成死等
  • 超时参数用宏定义换算,别写死 ticks 数
  • 超时后一定要有处理逻辑,哪怕是打印一行日志

下一节我们聊聊事件通知的另一种方式——回调。那个更灵活,但也更容易踩坑。