2. 事件控制块与事件对象:WIND_EVENT结构体分析、事件对象的创建与删除、事件属性的配置

好,咱们接着往下聊。上一章我们把事件机制的整体框架搭起来了,这一章要深入到底层去,看看VxWorks到底是怎么用代码来管理事件的。

说白了,事件机制的核心就是那个叫 WIND_EVENT 的结构体。你把它理解成事件的「身份证」就行。每个事件对象,在系统内部都对应着这么一个结构体实例。

2.1 WIND_EVENT 结构体深度解剖

我直接上代码,咱们一行一行看。这是VxWorks内核里真正的定义,我稍微简化了一下,但核心字段都在。

typedef struct wind_event {
    OBJ_CORE   objCore;        /* 对象核心,用于对象管理 */
    UINT8      eventState;     /* 事件状态:等待中/已触发 */
    UINT8      eventType;      /* 事件类型:自动复位/手动复位 */
    UINT16     eventOptions;   /* 事件选项:是否支持广播等 */
    WIND_TCB   *owner;         /* 拥有该事件的任务TCB指针 */
    DL_LIST    waitList;       /* 等待该事件的任务链表 */
    SEM_ID     syncSem;        /* 内部同步信号量 */
} WIND_EVENT;

这个结构体看着不大,但每个字段都有讲究。我一个个说。

objCore:这是VxWorks对象管理体系的根基。所有内核对象——任务、信号量、消息队列、事件——都嵌着这个 OBJ_CORE。它负责对象的创建、删除、命名、查找。嗯,你可以把它想象成每个对象的「户口本」。

eventState:这个字段记录事件当前的状态。取值就两个:EVENT_WAITING 表示事件还没被触发,任务正在等;EVENT_SIGNALED 表示事件已经触发了。我在项目中遇到过一个问题,就是调试时发现事件状态莫名其妙卡在 EVENT_WAITING,后来查出来是某个任务忘记调用 eventSend 了。

eventType:这个决定了事件被触发后的行为。有两种:

  • EVENT_AUTO_CLEAR(自动复位):事件触发后,如果有任务在等,只唤醒一个任务,然后事件自动变回未触发状态。就像一扇单向门,一个人过去,门就关上了。
  • EVENT_MANUAL_CLEAR(手动复位):事件触发后,所有等待的任务都会被唤醒,而且事件保持触发状态,直到你手动调用 eventClear 才复位。这就像一扇双向门,打开了就一直开着,你得亲自去关。

eventOptions:这个字段用来配置一些额外选项。比如 EVENT_OPT_BROADCAST 表示支持广播模式——事件触发时,所有等待的任务都能收到通知。我个人习惯,如果多个任务需要同步响应同一个事件,就用这个选项。

owner:指向创建这个事件的任务。为什么要记这个?因为事件删除时,系统要检查权限——只有拥有者或者系统任务才能删。你想想看,如果随便一个任务都能删别人创建的事件,那系统不乱套了?

waitList:这是一个双向链表,挂着所有正在等待这个事件的任务。每个等待任务把自己的TCB(任务控制块)挂到这个链表上。事件触发时,系统就遍历这个链表,挨个唤醒。

syncSem:内部用的同步信号量。事件机制底层其实依赖信号量来做任务调度和同步。这个字段对外不可见,你不需要直接操作它。

核心要点WIND_EVENT 结构体把事件的所有信息都封装在一起了。你创建事件对象,本质上就是在内存里分配一块空间,然后初始化这个结构体的各个字段。

2.2 事件对象的创建与删除

好,结构体看完了,咱们看看怎么创建和销毁事件对象。VxWorks提供了两个API:eventCreateeventDelete

2.2.1 创建事件对象

函数原型长这样:

EVENT_ID eventCreate
    (
    UINT8      eventType,      /* 事件类型:自动复位或手动复位 */
    UINT16     eventOptions,   /* 事件选项 */
    FUNCPTR    eventRoutine,   /* 事件回调函数,通常传NULL */
    int        arg             /* 回调函数参数 */
    );

返回值是一个 EVENT_ID,其实就是 WIND_EVENT* 的别名。如果创建失败,返回 NULL

我举个例子,创建一个手动复位、支持广播的事件:

EVENT_ID myEvent;

myEvent = eventCreate(
    EVENT_MANUAL_CLEAR,     /* 手动复位 */
    EVENT_OPT_BROADCAST,    /* 支持广播 */
    NULL,                   /* 不需要回调 */
    0                       /* 回调参数,没用 */
);

if (myEvent == NULL) {
    printf("事件创建失败!\n");
    return ERROR;
}

这里有个细节我想强调一下。第三个参数 eventRoutine 是回调函数,事件触发时会自动调用。但我个人建议,除非你有非常特殊的场景,否则传 NULL 就好。为什么?因为回调函数是在中断上下文或者调度器锁定的上下文中执行的,你在这里面能干的事非常有限。我曾经见过一个同事在回调里调 malloc,结果系统直接挂了——中断里不能调 malloc,这是基本常识。

小技巧:创建事件时,尽量在系统初始化阶段就完成。不要在中断服务程序里创建事件对象,因为 eventCreate 内部会申请内存,而中断里申请内存是不安全的。

2.2.2 删除事件对象

有创建就有删除。函数原型:

STATUS eventDelete
    (
    EVENT_ID eventId    /* 要删除的事件ID */
    );

用法很简单:

STATUS ret;
ret = eventDelete(myEvent);
if (ret != OK) {
    printf("事件删除失败!\n");
}

但这里有个大坑,我必须提醒你。

警告:删除事件时,如果有任务正在等待这个事件,系统会把这些任务全部唤醒,并返回一个错误码 ERROR 给它们。这些任务需要检查返回值,否则它们会傻傻地以为事件已经触发了,然后继续往下执行——这可能导致数据错乱。

我曾经在一个数据采集项目中遇到过这个问题。一个采集任务在等事件通知,结果另一个任务不小心把事件删了,采集任务被唤醒后没检查返回值,直接去读缓冲区,读到的全是脏数据。那次排查花了我整整两天。

所以,删除事件前,最好确保没有任务在等它。或者,让等待任务的代码做好错误处理:

/* 等待事件 */
status = eventReceive(myEvent, ...);
if (status == ERROR) {
    /* 事件被删除了,或者发生了其他错误 */
    /* 不要继续执行依赖事件结果的代码 */
    return;
}

2.3 事件属性的配置

事件创建之后,有些属性是可以动态修改的。VxWorks提供了 eventInfoGeteventInfoSet 这两个API。

2.3.1 获取事件属性

STATUS eventInfoGet
    (
    EVENT_ID       eventId,    /* 事件ID */
    EVENT_INFO     *pInfo      /* 输出参数,存放事件信息 */
    );

EVENT_INFO 结构体长这样:

typedef struct event_info {
    UINT8      eventType;      /* 事件类型 */
    UINT16     eventOptions;   /* 事件选项 */
    UINT8      eventState;     /* 当前状态 */
    int        tasksWaiting;   /* 正在等待的任务数 */
} EVENT_INFO;

这个API在调试时特别有用。你可以用它来查看事件当前的状态,有多少任务在等,类型是什么。我经常在调试代码里加这么一段:

EVENT_INFO info;
eventInfoGet(myEvent, &info);
printf("事件状态: %s, 等待任务数: %d\n",
       info.eventState == EVENT_SIGNALED ? "已触发" : "等待中",
       info.tasksWaiting);

2.3.2 修改事件属性

STATUS eventInfoSet
    (
    EVENT_ID       eventId,    /* 事件ID */
    EVENT_INFO     *pInfo      /* 输入参数,新的事件信息 */
    );

注意,不是所有属性都能改。能改的主要是 eventOptions。比如,你可以在运行时把广播选项打开或关闭:

EVENT_INFO newInfo;
eventInfoGet(myEvent, &newInfo);
newInfo.eventOptions |= EVENT_OPT_BROADCAST;  /* 打开广播 */
eventInfoSet(myEvent, &newInfo);

eventType 是不能改的。为什么?因为事件类型决定了内核的行为逻辑——自动复位和手动复位的处理流程完全不同。你想想看,如果允许在运行时切换类型,那正在等待的任务怎么办?它们的行为会变得不可预测。所以VxWorks干脆禁止了这种操作。

记住:事件类型在创建时确定,之后不能修改。事件选项可以在运行时修改。事件状态由系统管理,你不需要手动设置。

2.4 避坑指南与经验总结

最后,我把自己这些年踩过的坑总结一下,希望能帮你少走弯路。

  1. 事件ID要保护好:事件ID本质上是一个指针。如果多个任务共享同一个事件,一定要确保事件ID的传递是安全的。我习惯用全局变量或者通过消息队列传递。
  2. 不要重复删除:删除一个已经删除的事件,结果是未定义的。轻则返回错误,重则直接宕机。所以删除后记得把事件ID置为 NULL
  3. 注意内存泄漏:每个事件对象都会占用内存。如果频繁创建和删除事件,记得检查是否有泄漏。VxWorks的 memShow 命令可以查看内存使用情况。
  4. 事件回调要谨慎:前面说了,回调函数在中断上下文中执行。不要在回调里做阻塞操作、不要调 printf(某些BSP下会死锁)、不要调 malloc。能不用回调就不用。
  5. 手动复位事件要手动清理:如果你用了手动复位类型,事件触发后一定要记得调 eventClear。否则下次等待这个事件的任务会立即返回,因为事件一直处于触发状态。

嗯,这一章的内容就到这儿。事件控制块和事件对象是事件机制的基石,理解透了,后面讲事件发送和接收的时候你就会觉得顺理成章。下一章我们聊聊 eventSendeventReceive 的具体用法,以及它们背后的调度逻辑。