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:eventCreate 和 eventDelete。
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提供了 eventInfoGet 和 eventInfoSet 这两个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 避坑指南与经验总结
最后,我把自己这些年踩过的坑总结一下,希望能帮你少走弯路。
- 事件ID要保护好:事件ID本质上是一个指针。如果多个任务共享同一个事件,一定要确保事件ID的传递是安全的。我习惯用全局变量或者通过消息队列传递。
- 不要重复删除:删除一个已经删除的事件,结果是未定义的。轻则返回错误,重则直接宕机。所以删除后记得把事件ID置为
NULL。 - 注意内存泄漏:每个事件对象都会占用内存。如果频繁创建和删除事件,记得检查是否有泄漏。VxWorks的
memShow命令可以查看内存使用情况。 - 事件回调要谨慎:前面说了,回调函数在中断上下文中执行。不要在回调里做阻塞操作、不要调
printf(某些BSP下会死锁)、不要调malloc。能不用回调就不用。 - 手动复位事件要手动清理:如果你用了手动复位类型,事件触发后一定要记得调
eventClear。否则下次等待这个事件的任务会立即返回,因为事件一直处于触发状态。
嗯,这一章的内容就到这儿。事件控制块和事件对象是事件机制的基石,理解透了,后面讲事件发送和接收的时候你就会觉得顺理成章。下一章我们聊聊 eventSend 和 eventReceive 的具体用法,以及它们背后的调度逻辑。