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