3. 消息发送与接收:msgQSend()与msgQReceive()函数详解

好,咱们直接进入正题。消息队列这东西,说白了就是任务之间传纸条用的。你发一条,我收一条,大家按规矩来。今天我就把 msgQSend()msgQReceive() 这两个核心函数掰开揉碎了讲清楚。

3.1 msgQSend() —— 把消息扔进队列

这个函数负责把消息发出去。原型长这样:

STATUS msgQSend
    (
    MSG_Q_ID     msgQId,      /* 队列ID,创建时拿到的那个句柄 */
    char *       buffer,      /* 要发送的数据缓冲区指针 */
    UINT         nBytes,      /* 要发送的字节数 */
    int          timeout,     /* 超时时间,单位是tick */
    int          priority     /* 发送优先级,MSG_PRI_NORMAL或MSG_PRI_URGENT */
    )

嗯,参数看着多,其实用起来很直接。我一个个说。

3.1.1 参数详解

  • msgQId:队列的身份证。你创建队列时返回的那个ID,就是这里用的。
  • buffer:指向你要发送的数据。注意,这里传的是指针,不是数据本身。VxWorks 内部会帮你把数据拷贝到队列的消息缓冲区里。
  • nBytes:发送多少字节。这个值不能超过创建队列时设定的最大消息长度,否则会报错。
  • timeout:等待时间。如果队列满了,你是等一会儿还是直接放弃?单位是系统 tick,WAIT_FOREVER 表示死等,NO_WAIT 表示不等。
  • priority:消息优先级。只有两个选项:MSG_PRI_NORMAL(普通)和 MSG_PRI_URGENT(紧急)。紧急消息会插队到队列最前面。

我的小经验:我个人习惯把 timeout 设成 NO_WAIT 或者一个较小的值。为什么?因为死等容易让任务卡死,万一接收端出问题,发送端也跟着完蛋。我曾经在一个项目中遇到过,发送任务用了 WAIT_FOREVER,结果接收任务崩溃了,整个系统都挂在那。从那以后,我基本不用死等。

3.1.2 返回值

返回值含义
OK发送成功
ERROR发送失败,具体原因看 errno

常见的 errno 有:S_objLib_OBJ_TIMEOUT(超时了)、S_msgQLib_INVALID_MSG_LENGTH(消息太长)。

3.2 msgQReceive() —— 从队列里取消息

有发就有收。接收函数长这样:

int msgQReceive
    (
    MSG_Q_ID     msgQId,      /* 队列ID */
    char *       buffer,      /* 存放接收数据的缓冲区 */
    UINT         maxNBytes,   /* 缓冲区最大容量 */
    int          timeout      /* 超时时间 */
    )

3.2.1 参数详解

  • msgQId:还是那个队列ID。
  • buffer:你准备好的空缓冲区,用来装接收到的数据。
  • maxNBytes:你的缓冲区有多大。如果收到的消息比这个还大,会被截断。嗯,这里要注意,截断后你就拿不到完整数据了。
  • timeout:跟发送一样,等多久。如果队列里没消息,你是等还是走?

警告:我曾经见过有人把 maxNBytes 设得比实际消息小,结果数据被截断,系统跑着跑着就出诡异问题。排查了两天才找到原因。所以,接收缓冲区一定要够大,或者至少跟创建队列时的最大消息长度一致。

3.2.2 返回值

返回值含义
正数实际接收到的字节数
ERROR接收失败,看 errno

常见的 errno:S_objLib_OBJ_TIMEOUT(等超时了,没收到消息)。

3.3 一个完整的例子

光说不练假把式。咱们写个简单的 demo:

#include <vxWorks.h>
#include <msgQLib.h>

MSG_Q_ID myQueue;

void senderTask(void)
{
    char msg[] = "Hello from sender!";
    STATUS status;

    status = msgQSend(myQueue, msg, sizeof(msg), NO_WAIT, MSG_PRI_NORMAL);
    if (status == ERROR)
    {
        printf("发送失败,errno = 0x%x\n", errnoGet());
    }
}

void receiverTask(void)
{
    char buf[100];
    int nBytes;

    nBytes = msgQReceive(myQueue, buf, sizeof(buf), WAIT_FOREVER);
    if (nBytes == ERROR)
    {
        printf("接收失败\n");
    }
    else
    {
        printf("收到 %d 字节: %s\n", nBytes, buf);
    }
}

void init(void)
{
    myQueue = msgQCreate(10, 100, MSG_Q_FIFO);
    taskSpawn("tSender", 100, 0, 2000, (FUNCPTR)senderTask, 0,0,0,0,0,0,0,0,0,0);
    taskSpawn("tReceiver", 100, 0, 2000, (FUNCPTR)receiverTask, 0,0,0,0,0,0,0,0,0,0);
}

这个例子很简单。发送任务发一条消息,接收任务等着收。你想想看,如果接收任务先跑,队列里没消息,它就会在 msgQReceive 那里等着。等发送任务把消息扔进来,它立刻就能收到。

3.4 避坑指南

做嵌入式这么多年,我踩过的坑不少。关于消息收发,有几个点特别容易出问题:

  • 消息长度不匹配:发送的字节数超过了队列允许的最大长度,直接报错。我建议你在代码里加个断言检查。
  • 缓冲区溢出:接收方的缓冲区比消息小,数据被截断。嗯,这个坑我掉进去过,后来学乖了,一律用 sizeof(buf) 作为 maxNBytes。
  • 优先级滥用:紧急消息虽然好用,但别什么都用紧急。我曾经见过一个项目,所有消息都用 MSG_PRI_URGENT,结果紧急消息跟普通消息没区别了,反而破坏了队列的顺序性。
  • 超时设置:发送和接收的超时时间要合理。太短容易丢消息,太长容易卡任务。我个人习惯是设成 10-100 个 tick,具体看你的系统 tick 频率。

核心要点:消息队列的收发,本质上就是一次内存拷贝。发送方把数据拷贝到内核缓冲区,接收方再从内核缓冲区拷贝出来。所以,消息别太大,否则拷贝开销会让你心疼的。

3.5 性能小贴士

最后聊点性能相关的。如果你对实时性要求很高,有几点可以注意:

  • 消息尽量短小精悍。我一般控制在 64 字节以内,超过这个数,拷贝开销就上来了。
  • NO_WAIT 避免上下文切换。如果你确定队列有空位,直接发,别等。
  • 接收任务优先级可以设高一点。这样消息一来,接收任务能立刻被调度到。

好了,msgQSend 和 msgQReceive 就讲这么多。说白了,一个发一个收,中间靠队列缓冲。你只要记住参数含义,注意超时和长度,基本不会出大问题。下一节咱们聊聊消息队列的高级用法,比如广播和紧急消息的实战技巧。