4. 消息队列(Message Queue):System V与POSIX消息队列、消息的发送与接收、优先级控制
消息队列,说白了就是进程间传递数据的一块“公共信箱”。
我刚开始做上位机开发时,总觉得共享内存又快又直接,干嘛还要用消息队列?后来在一个多进程的工控项目里,共享内存的数据同步把我折腾得够呛——两个进程同时写一个结构体,加锁加得我头皮发麻。换用消息队列后,问题迎刃而解。嗯,每种IPC都有它的脾气,消息队列的强项就是“解耦”和“异步”。
4.1 System V 消息队列 vs POSIX 消息队列
Linux下有两套消息队列的API,一套是System V的,一套是POSIX的。你想想看,为什么会有两套?
System V是Unix老祖宗传下来的,历史悠久,几乎所有Linux系统都支持。而POSIX是后来为了统一标准搞的,设计上更现代一些。
| 对比项 | System V 消息队列 | POSIX 消息队列 |
|---|---|---|
| 标识方式 | key值(整数) | 名字(字符串,如 /mq_name) |
| 创建/打开 | msgget() | mq_open() |
| 发送消息 | msgsnd() | mq_send() |
| 接收消息 | msgrcv() | mq_receive() |
| 优先级支持 | 通过消息类型(长整型) | 通过优先级参数(无符号整型) |
| 最大消息数 | 系统限制(可调) | 系统限制(可调) |
| 通知机制 | 无(需轮询) | 支持异步通知(mq_notify) |
我个人习惯,在新项目中优先用POSIX消息队列。为什么?因为它的接口更直观,而且支持异步通知,不用像System V那样傻傻地轮询。但如果你要维护老项目,System V的代码你肯定得看得懂。
4.2 消息的发送与接收
消息队列的核心操作就两个:发和收。但这里面的门道可不少。
4.2.1 POSIX 消息队列的发送与接收
先看一个简单的例子。发送端:
#include <mqueue.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>
int main() {
mqd_t mq;
struct mq_attr attr;
// 设置队列属性
attr.mq_flags = 0;
attr.mq_maxmsg = 10; // 最多10条消息
attr.mq_msgsize = 256; // 每条消息最大256字节
attr.mq_curmsgs = 0;
// 创建消息队列
mq = mq_open("/my_queue", O_CREAT | O_WRONLY, 0644, &attr);
if (mq == (mqd_t)-1) {
perror("mq_open");
return 1;
}
char *msg = "Hello, 这是消息队列!";
// 发送消息,优先级设为1
if (mq_send(mq, msg, strlen(msg) + 1, 1) == -1) {
perror("mq_send");
}
mq_close(mq);
return 0;
}
接收端:
#include <mqueue.h>
#include <fcntl.h>
#include <stdio.h>
int main() {
mqd_t mq;
char buffer[256];
unsigned int priority;
// 打开消息队列
mq = mq_open("/my_queue", O_RDONLY);
if (mq == (mqd_t)-1) {
perror("mq_open");
return 1;
}
// 接收消息,会阻塞直到有消息
ssize_t bytes_read = mq_receive(mq, buffer, sizeof(buffer), &priority);
if (bytes_read == -1) {
perror("mq_receive");
} else {
printf("收到消息: %s (优先级: %u)\n", buffer, priority);
}
mq_close(mq);
return 0;
}
这里有个细节要注意:mq_receive的第二个参数是缓冲区大小,必须大于等于队列创建时设置的mq_msgsize。否则会报EMSGSIZE错误。我曾经在这个坑里栽过跟头,调试了半天才发现是缓冲区开小了。
4.2.2 System V 消息队列的发送与接收
System V的用法稍微绕一点。它要求你定义一个消息结构体,第一个成员必须是long mtype:
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
struct msgbuf {
long mtype; // 消息类型,必须大于0
char mtext[256]; // 消息内容
};
int main() {
int msgid;
key_t key;
struct msgbuf msg;
// 生成key
key = ftok("/tmp", 'A');
// 创建消息队列
msgid = msgget(key, IPC_CREAT | 0666);
if (msgid == -1) {
perror("msgget");
return 1;
}
// 发送消息,类型为1
msg.mtype = 1;
strcpy(msg.mtext, "Hello from System V!");
if (msgsnd(msgid, &msg, strlen(msg.mtext) + 1, 0) == -1) {
perror("msgsnd");
}
// 接收类型为1的消息
if (msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0) == -1) {
perror("msgrcv");
} else {
printf("收到: %s\n", msg.mtext);
}
// 删除消息队列
msgctl(msgid, IPC_RMID, NULL);
return 0;
}
msgctl(msgid, IPC_RMID, NULL),它会一直占用系统资源。我见过有人写demo时忘了清理,结果系统消息队列用完了,其他程序全挂了。
4.3 优先级控制
消息队列的优先级,说白了就是让重要的消息先走。
POSIX消息队列的优先级是0到31的数字,数字越大优先级越高。发送时指定优先级,接收时默认会返回优先级最高的那条消息。
System V的优先级是通过消息类型实现的。接收时你可以指定类型参数:
msgtyp = 0:接收队列中第一条消息(先进先出)msgtyp > 0:接收类型等于msgtyp的第一条消息msgtyp < 0:接收类型小于等于msgtyp绝对值的最小类型消息
举个例子,假设队列里有三条消息:类型1、类型2、类型3。如果你用msgrcv(msgid, &msg, size, -2, 0),它会返回类型1的消息,因为1是小于等于2的最小类型。
msgtyp = -1,这样报警永远优先处理。嗯,这个设计让系统的实时性提升了一个档次。
4.4 避坑指南
做消息队列开发,有几个坑我踩过,分享给你:
- 消息大小限制:Linux默认每条消息最大8192字节,队列最多16条。可以通过
/proc/sys/fs/mqueue/下的文件调整。别问我怎么知道的——有一次我传了一张图片,结果消息发不出去。 - 阻塞与非阻塞:发送和接收默认是阻塞的。如果队列满了,
mq_send会一直等。可以用O_NONBLOCK标志改成非阻塞模式,这时候满了会直接返回EAGAIN。 - 权限问题:POSIX消息队列的名字以
/开头,但这不是文件路径。不同进程要用相同的名字才能通信。我曾经因为名字写错,两个进程各开各的队列,数据死活传不过去。 - 资源清理:POSIX消息队列用
mq_unlink()删除,System V用msgctl()。程序退出前一定要清理,否则会留下僵尸队列。
消息队列这东西,用好了是利器,用不好就是坑。我个人建议,新手先从POSIX版本入手,接口简单,不容易出错。等把基本概念搞清楚了,再去看System V的实现,你会发现两者本质是一样的——都是内核维护的一个链表,只不过操作方式不同罢了。