10、消息队列实战:从入门到精通
消息队列,说白了就是内核帮我们维护的一个「邮局」。进程A往里头扔信,进程B从里头取信。我在做嵌入式项目时,经常用它来解耦不同模块——比如传感器采集和数据处理,各干各的,互不干扰。
今天咱们就把消息队列的方方面面都捋一遍。嗯,内容不少,但都是干货。
10.1 消息队列的权限设置
创建消息队列用的是 msgget() 函数。它的第二个参数就是权限标志,跟文件权限类似。
#include <sys/msg.h>
// 创建或获取一个消息队列
int msgget(key_t key, int msgflg);
// 示例:创建一个权限为 0666 的消息队列
int msgid = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
这里的 0666 表示所有用户都可读写。但我在实际项目中,一般只给 0640——只有自己和同组人能操作。你想想看,万一别的进程乱发消息,调试起来多头疼。
注意:权限设置不是摆设。我曾经在一个多租户系统里,因为偷懒用了 0777,结果两个业务模块的消息互相串了,查了一整天才找到原因。
10.2 消息队列的大小限制
消息队列不是无限大的。内核有几个硬限制:
| 参数 | 默认值 | 说明 |
|---|---|---|
| MSGMNB | 16384 字节 | 单个队列的最大字节数 |
| MSGMNI | 32000 | 系统最大队列数 |
| MSGMAX | 8192 字节 | 单条消息的最大长度 |
这些值可以通过 sysctl 查看和修改:
# 查看当前限制
sysctl kernel.msgmnb
sysctl kernel.msgmni
sysctl kernel.msgmax
# 临时修改(重启后失效)
sysctl -w kernel.msgmnb=65536
我个人习惯在生产环境里把 MSGMAX 调到 65535。为什么?因为有一次我传一张小图片,消息被截断了,数据对不上,排查了好久才发现是默认限制太小。
10.3 阻塞与非阻塞
发送和接收消息时,可以设置阻塞或非阻塞模式。关键在 msgflg 参数:
// 发送消息
int msgsnd(int msgid, const void *msgp, size_t msgsz, int msgflg);
// 接收消息
ssize_t msgrcv(int msgid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
当 msgflg 包含 IPC_NOWAIT 时,就是非阻塞模式。
核心区别:
- 阻塞模式:队列满了就等着,直到有空间
- 非阻塞模式:队列满了直接返回 -1,errno 设为 EAGAIN
我建议在实时性要求高的场景用非阻塞。比如传感器数据采集,丢一帧没关系,但不能卡住整个流程。
10.4 消息类型的使用
消息队列最妙的地方就是支持消息类型。每个消息可以带一个 long 类型的编号:
struct msgbuf {
long mtype; // 消息类型,必须 > 0
char mtext[256]; // 消息内容
};
接收时可以根据类型过滤:
// 接收类型为 1 的消息
msgrcv(msgid, &buf, sizeof(buf.mtext), 1, 0);
// 接收类型为 2 的消息
msgrcv(msgid, &buf, sizeof(buf.mtext), 2, 0);
// 接收所有消息(类型设为 0)
msgrcv(msgid, &buf, sizeof(buf.mtext), 0, 0);
我在项目中常用类型来区分优先级:类型 1 是紧急消息,类型 2 是普通消息,类型 3 是日志。这样接收方可以优先处理紧急任务。
10.5 多进程通信实战
来个完整的例子。父进程发消息,子进程收消息:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <unistd.h>
struct msgbuf {
long mtype;
char mtext[128];
};
int main() {
int msgid = msgget(IPC_PRIVATE, IPC_CREAT | 0640);
if (msgid == -1) {
perror("msgget");
exit(1);
}
pid_t pid = fork();
if (pid == 0) {
// 子进程:接收消息
struct msgbuf buf;
msgrcv(msgid, &buf, sizeof(buf.mtext), 1, 0);
printf("子进程收到: %s\n", buf.mtext);
} else {
// 父进程:发送消息
struct msgbuf buf = {1, "Hello from parent!"};
msgsnd(msgid, &buf, strlen(buf.mtext) + 1, 0);
wait(NULL);
}
msgctl(msgid, IPC_RMID, NULL);
return 0;
}
这个例子虽然简单,但包含了完整的流程:创建队列、发送、接收、清理。嗯,别忘了最后要删除队列,否则内核里会残留垃圾。
10.6 消息队列的优缺点
用了这么多年,我总结了几点:
优点:
- 解耦彻底:发送方和接收方不需要同时运行
- 支持多对多:多个进程可以往同一个队列发消息
- 有类型过滤:接收方可以只关心自己需要的消息
- 内核持久化:进程挂了消息不会丢
缺点:
- 大小有限:不能传大文件或大数据块
- 无优先级反转保护:高类型消息不一定先被处理
- 调试困难:消息队列是内核资源,用
ipcs才能看到 - 跨机器不行:只能用于同一台机器的进程
10.7 应用场景
根据我的经验,消息队列最适合这些场景:
- 日志收集:多个模块往队列里写日志,一个专门的日志进程去处理
- 任务分发:主进程把任务按类型发给不同的工作进程
- 事件通知:某个模块状态变了,发个消息通知其他模块
- 数据采集:传感器数据先入队列,处理程序慢慢取
说白了,只要数据量不大、不需要跨机器、需要解耦的场景,消息队列都是好选择。
10.8 综合案例:简易日志系统
最后来个综合案例。三个进程:一个写日志,一个读日志,一个控制日志级别。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <unistd.h>
#include <time.h>
#define MSG_TYPE_LOG 1
#define MSG_TYPE_CTRL 2
struct msgbuf {
long mtype;
int level; // 0=INFO, 1=WARN, 2=ERROR
char content[256];
};
int main() {
int msgid = msgget(1234, IPC_CREAT | 0640);
if (msgid == -1) {
perror("msgget");
exit(1);
}
pid_t pid1 = fork();
if (pid1 == 0) {
// 进程1:写日志
srand(time(NULL) ^ getpid());
for (int i = 0; i < 5; i++) {
struct msgbuf buf;
buf.mtype = MSG_TYPE_LOG;
buf.level = rand() % 3;
snprintf(buf.content, sizeof(buf.content),
"日志条目 #%d", i);
msgsnd(msgid, &buf, sizeof(buf) - sizeof(long), 0);
sleep(1);
}
exit(0);
}
pid_t pid2 = fork();
if (pid2 == 0) {
// 进程2:读日志并输出
struct msgbuf buf;
while (1) {
ssize_t ret = msgrcv(msgid, &buf,
sizeof(buf) - sizeof(long),
MSG_TYPE_LOG, IPC_NOWAIT);
if (ret > 0) {
const char *level_str[] = {"INFO", "WARN", "ERROR"};
printf("[%s] %s\n", level_str[buf.level], buf.content);
} else {
usleep(100000);
}
}
exit(0);
}
// 父进程:发送控制消息
sleep(3);
struct msgbuf ctrl = {MSG_TYPE_CTRL, 0, "shutdown"};
msgsnd(msgid, &ctrl, sizeof(ctrl) - sizeof(long), 0);
wait(NULL);
wait(NULL);
msgctl(msgid, IPC_RMID, NULL);
return 0;
}
这个案例里,写日志的进程只管发,读日志的进程只管收,控制消息走另一个类型。互不干扰,各司其职。
小技巧:调试消息队列时,多用 ipcs -q 查看队列状态,用 ipcrm -q msgid 清理残留队列。我曾经因为忘记清理,导致系统消息队列用满,新进程创建失败,折腾了半天。
好了,消息队列的核心内容就这些。从权限到大小,从阻塞到类型,再到多进程实战,每一步都有坑,但也都有解法。记住一点:消息队列是工具,用对了地方事半功倍,用错了反而添乱。