第3章:消息队列(Message Queue):System V消息队列与POSIX消息队列对比
消息队列这东西,说白了就是进程间传递数据的一块「公共黑板」。一个进程往上面贴消息,另一个进程来取。我在早年做嵌入式网关项目时,就靠它来协调多个传感器采集进程和数据处理进程之间的通信。今天咱们就聊聊两种主流的消息队列——System V和POSIX,看看它们到底有啥区别,怎么用,以及实战中怎么搭一个多生产者-单消费者的模型。
3.1 System V消息队列 vs POSIX消息队列
这两种队列,名字听着像两兄弟,但脾气秉性差不少。我刚开始接触时也犯过混,后来踩了几个坑才摸清楚。
| 对比维度 | System V消息队列 | POSIX消息队列 |
|---|---|---|
| 标识方式 | 使用 key(键值)和 msgid(队列ID) | 使用名字(以 / 开头),类似文件路径 |
| 创建/打开 | msgget(),需要指定 key 和标志 | mq_open(),直接传名字和标志 |
| 消息类型 | 支持按消息类型(长整型)接收 | 不支持类型,只能按优先级接收 |
| 最大消息数 | 系统级限制,通常较小 | 可配置,通常更灵活 |
| 异步通知 | 不支持,只能阻塞或轮询 | 支持 mq_notify() 注册信号或线程通知 |
| 清理方式 | 需要手动 ipcrm 或程序删除 | 进程退出时自动清理(引用计数) |
我个人习惯是:如果项目需要按消息类型精确分发,我会选System V。如果追求易用性和异步通知,POSIX更顺手。但要注意,POSIX队列在老旧内核上可能不支持,我在一个CentOS 6的项目里就吃过这个亏。
3.2 消息队列的创建与销毁
创建队列其实不复杂,但细节决定成败。咱们先看System V的创建方式:
#include <sys/msg.h>
// 创建或获取一个消息队列
int msgid = msgget(IPC_PRIVATE, IPC_CREAT | 0666);
// 或者用固定 key
key_t key = ftok("/tmp/msgqueue", 'A');
int msgid = msgget(key, IPC_CREAT | 0666);
// 销毁队列
msgctl(msgid, IPC_RMID, NULL);
这里有个坑:ftok() 生成的 key 可能冲突。我曾经在一个集群环境中,两个服务用了同一个路径和项目ID,结果消息串了,排查了一整天。所以建议用 IPC_PRIVATE 配合进程间传递 msgid,或者用 ftok() 时选一个独特的路径。
再看POSIX的创建:
#include <mqueue.h>
// 创建或打开
struct mq_attr attr;
attr.mq_flags = 0;
attr.mq_maxmsg = 10; // 最大消息数
attr.mq_msgsize = 1024; // 每条消息最大字节
attr.mq_curmsgs = 0;
mqd_t mqd = mq_open("/myqueue", O_CREAT | O_RDWR, 0666, &attr);
// 关闭(不是销毁)
mq_close(mqd);
// 销毁队列
mq_unlink("/myqueue");
注意:POSIX队列的名字必须以
/ 开头,且长度有限制(通常不超过255字符)。我见过有人忘了加斜杠,结果 mq_open 一直返回失败,还以为是权限问题。
3.3 实战:多生产者-单消费者模型
这个模型在实际中太常见了。比如日志收集系统,多个工作线程产生日志,一个写入线程负责落盘。咱们用POSIX消息队列来实现,因为它支持异步通知,写起来更优雅。
生产者代码(多个进程/线程可同时运行)
#include <mqueue.h>
#include <string.h>
#include <unistd.h>
void producer(const char *data) {
mqd_t mqd = mq_open("/logqueue", O_WRONLY);
if (mqd == (mqd_t)-1) {
perror("mq_open");
return;
}
// 发送消息
if (mq_send(mqd, data, strlen(data) + 1, 0) == -1) {
perror("mq_send");
}
mq_close(mqd);
}
消费者代码(单线程循环接收)
#include <mqueue.h>
#include <stdio.h>
#include <signal.h>
mqd_t mqd;
void handle_msg(union sigval sv) {
char buf[1024];
unsigned int prio;
// 非阻塞接收
ssize_t n = mq_receive(mqd, buf, sizeof(buf), &prio);
if (n > 0) {
printf("收到消息: %s (优先级: %u)\n", buf, prio);
}
}
int main() {
struct mq_attr attr;
attr.mq_maxmsg = 20;
attr.mq_msgsize = 1024;
mqd = mq_open("/logqueue", O_CREAT | O_RDONLY, 0666, &attr);
// 注册异步通知
struct sigevent sev;
sev.sigev_notify = SIGEV_THREAD;
sev.sigev_notify_function = handle_msg;
sev.sigev_notify_attributes = NULL;
mq_notify(mqd, &sev);
// 主线程做其他事,或者 sleep
pause();
mq_close(mqd);
mq_unlink("/logqueue");
return 0;
}
小技巧:如果消费者处理不过来,可以设置队列的最大消息数来反压生产者。我在一个实时数据采集项目中,把 mq_maxmsg 设为100,当队列满了,生产者 mq_send 会阻塞,自然就限流了。
3.4 避坑指南
做消息队列开发,有几个坑我不得不提:
- 消息大小限制:System V 的 msgmax 通常只有 8KB,POSIX 可以设大一些,但也不能超过内核限制。我曾经传一个 10KB 的结构体,结果 msgrcv 一直返回 E2BIG,查了半天才发现是大小超了。
- 队列残留:System V 队列不会随进程退出而删除。我有个同事,开发时频繁创建队列但不清理,最后系统报 "No space left on device",其实是消息队列占满了内核内存。
- 优先级反转:POSIX 队列按优先级接收,如果高优先级消息不断涌入,低优先级消息可能永远得不到处理。我在一个监控系统里就遇到过,告警消息优先级高,把普通日志消息饿死了。
3.5 选型建议
说了这么多,到底选哪个?我个人建议:
- 如果你需要按消息类型精确路由(比如类型1给A进程,类型2给B进程),选 System V。
- 如果你需要异步通知、自动清理、或者跨平台(POSIX 标准更统一),选 POSIX。
- 如果性能要求极高,两个都差不多,但 System V 在极端情况下略快一点点(少一层抽象)。
嗯,消息队列就聊到这儿。下一章咱们聊聊共享内存,那又是另一番天地了。