消息队列高级特性:紧急消息、广播消息、消息队列与中断服务程序(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)。管道本质上也是消息队列,但它支持多个任务同时等待接收。

具体做法是:

  1. 创建一个管道(用 pipeDevCreate()
  2. 多个任务同时调用 read() 等待数据
  3. 发送端写一次数据,所有等待的任务都能收到

看代码:

/* 创建管道,最大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 交互让你能在中断里安全通信。这三个特性用好了,很多复杂场景都能轻松应对。下一章咱们聊聊消息队列的性能调优,到时候我会分享一些实测数据。