消息队列高级特性:紧急消息、广播消息、消息队列与中断服务程序(ISR)的交互
好,咱们接着聊消息队列的高级玩法。前面讲的那些,说白了都是常规操作——发消息、收消息、排队等。但实际项目中,总会遇到一些「不走寻常路」的场景。比如有个紧急任务要插队,或者一条消息想让多个任务都收到,再或者中断里想发消息怎么办?
这些需求,VxWorks 的消息队列都能搞定。我当年第一次接触这些特性时,心里还嘀咕「有必要搞这么复杂吗?」后来被现实狠狠教育了一顿——嗯,真有必要。
一、紧急消息:插队机制
先说说紧急消息。普通消息是先进先出,这没问题。但有时候,某个消息必须优先处理。比如系统检测到温度过高,需要立刻让风扇任务启动。如果它还在队列里排队,等轮到它时,芯片可能已经冒烟了。
VxWorks 的消息队列支持优先级发送。你可以在 msgQSend() 里指定一个优先级参数。具体来说,有两个选项:
- MSG_PRI_NORMAL:正常优先级,消息排到队尾
- MSG_PRI_URGENT:紧急优先级,消息插到队首
看个例子就明白了:
MSG_Q_ID msgQId;
char urgentMsg[] = "紧急:温度过高!";
char normalMsg[] = "普通:状态更新";
/* 创建消息队列,最多10条消息,每条20字节 */
msgQId = msgQCreate(10, 20, MSG_Q_FIFO);
/* 先发一条普通消息 */
msgQSend(msgQId, normalMsg, strlen(normalMsg) + 1, WAIT_FOREVER, MSG_PRI_NORMAL);
/* 再发一条紧急消息 */
msgQSend(msgQId, urgentMsg, strlen(urgentMsg) + 1, WAIT_FOREVER, MSG_PRI_URGENT);
/* 接收端会先收到紧急消息,再收到普通消息 */
关键点:紧急消息是插到队首,不是替换。如果队列里已经有紧急消息,新来的紧急消息会排在已有紧急消息的后面。也就是说,紧急消息之间还是按 FIFO 顺序。
我在项目中遇到过一个问题:有个同事把所有消息都设成紧急优先级。结果呢?紧急消息堆了一堆,普通消息永远发不出去。这其实违背了设计初衷。我个人习惯是:只有真正需要立即响应的消息才用紧急,比如故障告警、看门狗复位通知等。别滥用。
二、广播消息:一对多通知
接下来聊广播消息。这个特性很有意思。普通消息队列是一对一——一个任务发,一个任务收。但有时候,你需要一条消息通知多个任务。比如系统进入省电模式,所有外设驱动任务都要知道。
VxWorks 的消息队列本身不支持广播。但我们可以用另一种机制——管道(Pipe)。管道本质上也是消息队列,但它支持多个任务同时等待接收。
具体做法是:
- 创建一个管道(用
pipeDevCreate()) - 多个任务同时调用
read()等待数据 - 发送端写一次数据,所有等待的任务都能收到
看代码:
/* 创建管道,最大10条消息,每条100字节 */
pipeDevCreate("/pipe/broadcast", 10, 100);
/* 任务A:等待广播 */
void taskA(void)
{
char buf[100];
int fd = open("/pipe/broadcast", O_RDONLY, 0);
while (1)
{
read(fd, buf, sizeof(buf));
printf("任务A收到:%s\n", buf);
}
}
/* 任务B:同样等待广播 */
void taskB(void)
{
char buf[100];
int fd = open("/pipe/broadcast", O_RDONLY, 0);
while (1)
{
read(fd, buf, sizeof(buf));
printf("任务B收到:%s\n", buf);
}
}
/* 发送端:写一次,两个任务都收到 */
void senderTask(void)
{
int fd = open("/pipe/broadcast", O_WRONLY, 0);
char msg[] = "系统进入省电模式";
write(fd, msg, strlen(msg) + 1);
}
小提示:管道的广播行为是「读一次少一份」。如果三个任务在等,写一次后三个任务各读一份,数据就没了。所以广播消息适合「一次性通知」,不适合持久化存储。
你想想看,如果不用广播,你得给每个任务单独发一条消息。任务多了,代码就变得又臭又长。广播机制让代码简洁不少。我曾经在一个多传感器融合项目里,用管道广播传感器状态变化,所有算法任务同时响应,效果很好。
三、消息队列与中断服务程序(ISR)的交互
这部分是重点,也是容易踩坑的地方。中断服务程序(ISR)里能不能用消息队列?答案是:能,但有条件。
VxWorks 的 msgQSend() 可以在 ISR 中调用。但要注意:
- ISR 里不能阻塞。所以
msgQSend()的 timeout 参数必须设为NO_WAIT(即 0) - ISR 里不能调用
msgQReceive(),因为接收可能阻塞 - 消息队列本身必须是已经创建好的,不能在 ISR 里创建
看一个典型的中断处理流程:
MSG_Q_ID gMsgQId; /* 全局消息队列ID */
/* 中断服务程序 */
void myIsr(void)
{
char data = readHardwareReg();
STATUS status;
/* 在ISR中发送消息,必须用NO_WAIT */
status = msgQSend(gMsgQId, &data, sizeof(data), NO_WAIT, MSG_PRI_URGENT);
if (status == ERROR)
{
/* 队列满了,消息丢失!需要处理 */
logMsg("ISR: 消息队列满,数据丢失!\n", 0, 0, 0, 0, 0);
}
}
/* 普通任务:处理中断数据 */
void handlerTask(void)
{
char data;
while (1)
{
msgQReceive(gMsgQId, &data, sizeof(data), WAIT_FOREVER);
processData(data);
}
}
警告:ISR 里发消息时,如果队列满了,消息会直接丢弃。所以队列大小要留够余量。我建议至少是最大中断频率的 2-3 倍。我曾经因为队列设小了,在高频中断下丢了大量数据,排查了好久才发现是队列溢出。
还有一个细节:ISR 里用 msgQSend() 时,消息内容要尽量短。因为 ISR 执行时间越短越好。我一般只传一个整数或一个指针,具体数据放到任务里再解析。
另外,ISR 里能不能用管道?可以,但同样只能用 write() 且不能阻塞。不过我个人更推荐在 ISR 里用消息队列,因为消息队列的 API 更轻量,开销更小。
四、综合避坑指南
聊了这么多,最后总结几个我踩过的坑:
- 紧急消息别滥用:我曾经在一个项目里,所有消息都用紧急优先级。结果紧急消息之间互相插队,逻辑全乱套了。后来改成只有告警类消息用紧急,其他用普通,世界清净了。
- 广播消息注意接收方数量:管道广播时,如果接收任务比预期少,消息会留在管道里。下次广播时,旧消息和新消息混在一起,容易出问题。我习惯在广播前清空管道。
- ISR 里别做复杂操作:消息队列的发送本身很快,但如果你在 ISR 里还做字符串拷贝、内存分配,那就危险了。ISR 里只做最必要的事,其他交给任务。
- 队列大小要动态评估:别拍脑袋定大小。我一般会在测试阶段打日志,统计队列最大使用量,然后加 20% 余量。
好了,消息队列的高级特性就这些。说白了,紧急消息让你能插队,广播消息让你能群发,ISR 交互让你能在中断里安全通信。这三个特性用好了,很多复杂场景都能轻松应对。下一章咱们聊聊消息队列的性能调优,到时候我会分享一些实测数据。