消息队列:进程间通信的“邮局”
消息队列,说白了就是内核帮我们维护的一个消息链表。每个消息都有自己的类型,进程可以往队列里扔消息,也可以按类型取消息。这就像邮局——你寄信时写上地址(消息类型),收信人按地址取信。
我在做嵌入式网关项目时,多个模块需要交换配置数据。共享内存虽然快,但同步问题让人头疼;管道又太死板,只能单向流。最后选了消息队列,嗯,恰到好处。
核心API:四个函数搞定一切
System V消息队列就四个函数:msgget、msgsnd、msgrcv、msgctl。我习惯把它们记成“创建-发送-接收-控制”四步曲。
msgget — 创建或获取队列
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
// 返回:成功返回队列ID,失败返回-1
key是队列的“身份证号”,不同进程用同一个key就能找到同一个队列。msgflg传IPC_CREAT | 0666表示创建,权限0666表示所有人可读写。
避坑指南:我曾经在项目里忘了检查msgget的返回值,结果队列没创建成功,后面msgsnd一直报错。排查了半天才发现是key冲突了。记住:msgget失败时errno会告诉你原因——EACCES是权限不够,EEXIST是队列已存在且你没设IPC_CREAT。
msgsnd — 发送消息
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// 返回:成功返回0,失败返回-1
消息结构必须这样定义:
struct msgbuf {
long mtype; // 消息类型,必须 > 0
char mtext[1]; // 消息数据,大小自定
};
注意mtype必须是正数。0是非法类型,负值也不行。我见过有人传0进去,结果msgsnd直接返回EINVAL。
msgflg传IPC_NOWAIT表示非阻塞——队列满了就立刻返回EAGAIN。不传这个标志,进程会阻塞直到有空间。
msgrcv — 接收消息
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz,
long msgtyp, int msgflg);
// 返回:成功返回接收到的字节数,失败返回-1
msgtyp的取值很有意思:
| msgtyp | 行为 |
|---|---|
| 0 | 取队列中第一条消息(先进先出) |
| >0 | 取类型等于msgtyp的第一条消息 |
| <0 | 取类型小于等于|msgtyp|的最小类型消息 |
我个人最常用的是msgtyp > 0,按类型取消息。比如类型1是控制指令,类型2是数据包,接收方各取所需。
注意:msgsz是消息数据部分的长度,不包括mtype。如果实际消息比msgsz长,且msgflg没设MSG_NOERROR,msgrcv会返回E2BIG并截断消息。我曾经因为这个bug导致数据丢失,排查了一下午。
msgctl — 控制队列
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
// 返回:成功返回0,失败返回-1
三个常用命令:
IPC_STAT:获取队列状态信息IPC_SET:修改队列属性(比如权限)IPC_RMID:删除队列
删除队列时,所有阻塞在msgsnd/msgrcv上的进程会立刻被唤醒,并返回EIDRM。这个特性我在热升级模块里用过——先删队列,所有等待线程收到错误后优雅退出。
消息队列的优缺点
用了这么多年,我总结一下:
| 优点 | 缺点 |
|---|---|
| 支持多对多通信,灵活 | 消息有大小限制(通常8KB) |
| 按类型接收,天然支持优先级 | System V接口偏底层,使用稍复杂 |
| 内核自动管理同步,无需额外锁 | 队列总数有限(cat /proc/sys/kernel/msgmni可查) |
| 消息持久化在内核,进程退出不丢失 | POSIX消息队列更现代,但System V更通用 |
说白了,消息队列适合“异步、松耦合”的场景。比如A模块发配置,B模块慢慢处理,两者不需要同时运行。但如果你追求极致性能,共享内存+信号量可能更合适。
完整示例:一个简单的消息收发
下面是我写的一个demo,发送方发两种类型的消息,接收方按类型取:
// msg_send.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <errno.h>
#define MSG_KEY 1234
struct msgbuf {
long mtype;
char mtext[256];
};
int main() {
int msqid = msgget(MSG_KEY, IPC_CREAT | 0666);
if (msqid == -1) {
perror("msgget");
exit(1);
}
struct msgbuf msg;
// 发送类型1的消息
msg.mtype = 1;
strcpy(msg.mtext, "Hello, this is type 1");
if (msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0) == -1) {
perror("msgsnd type 1");
exit(1);
}
printf("Sent type 1: %s\n", msg.mtext);
// 发送类型2的消息
msg.mtype = 2;
strcpy(msg.mtext, "Hi, this is type 2");
if (msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0) == -1) {
perror("msgsnd type 2");
exit(1);
}
printf("Sent type 2: %s\n", msg.mtext);
return 0;
}
// msg_recv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <errno.h>
#define MSG_KEY 1234
struct msgbuf {
long mtype;
char mtext[256];
};
int main() {
int msqid = msgget(MSG_KEY, IPC_CREAT | 0666);
if (msqid == -1) {
perror("msgget");
exit(1);
}
struct msgbuf msg;
// 只接收类型1的消息
if (msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0) == -1) {
perror("msgrcv type 1");
exit(1);
}
printf("Received type 1: %s\n", msg.mtext);
// 再接收类型2的消息
if (msgrcv(msqid, &msg, sizeof(msg.mtext), 2, 0) == -1) {
perror("msgrcv type 2");
exit(1);
}
printf("Received type 2: %s\n", msg.mtext);
// 清理队列
if (msgctl(msqid, IPC_RMID, NULL) == -1) {
perror("msgctl");
exit(1);
}
return 0;
}
先跑发送方,再跑接收方。你会看到接收方只取到了类型匹配的消息。这就是消息队列的“按类型过滤”能力。
核心要点:消息队列是内核级的异步通信机制。它不像管道那样只能单向,也不像共享内存那样需要同步。你只需要定义好消息类型,发送方和接收方各司其职。但要注意——消息队列资源有限,用完记得msgctl(IPC_RMID)清理,否则系统会慢慢耗尽队列。
知识体系图
下面这张图帮你理清消息队列的核心脉络:
这张图把四个API、消息结构、优缺点串在了一起。你想想看,msgget是入口,msgsnd和msgrcv是核心操作,msgctl负责收尾。消息结构里的mtype是关键——它决定了消息的“收件地址”。
我的习惯:在实际项目中,我会把消息类型定义成枚举,比如MSG_CMD、MSG_DATA、MSG_ACK。这样代码可读性高,也不容易写错类型值。另外,我建议在msgrcv时用IPC_NOWAIT配合轮询,或者用信号驱动I/O,避免进程长时间阻塞。
消息队列虽然古老,但在很多场景下依然好用。尤其是当你需要“异步、按类型、多对多”通信时,它比管道和共享内存都顺手。嗯,今天就聊到这儿,下一节我们看看POSIX消息队列和System V的区别。
公众号:蓝海资料掘金营,微信deep3321