一、POSIX消息队列:到底是个什么东西?
各位同学,咱们今天聊POSIX消息队列。说实话,我第一次接触这个概念时,脑子里全是问号——这玩意儿和System V消息队列有啥区别?为什么Linux要搞两套?
后来在实际项目中用多了,我才慢慢品出味道来。POSIX消息队列,说白了就是一个能让进程之间传递"带类型的数据块"的机制。你想想看,两个完全独立的进程,一个发消息,一个收消息,中间不需要共享内存,不需要信号,多清爽。
我个人习惯把消息队列比作邮局的信箱。发信人把信投进去,收信人啥时候有空来取都行。信在信箱里待着,不会丢,也不会被其他人拿走。
核心特点:
- 消息有优先级(紧急的信先送)
- 消息有最大长度限制
- 队列本身有容量上限
- 支持异步通知(来新消息了告诉你)
二、创建与关闭:mq_open 和 mq_close
2.1 创建消息队列
创建消息队列用的是 mq_open() 函数。嗯,这里要注意,它的用法和 open() 有点像,但参数更复杂一些。
#include <fcntl.h> // 文件控制选项
#include <sys/stat.h> // 权限位
#include <mqueue.h> // 消息队列头文件
mqd_t mq_open(const char *name, int oflag, ...);
// 返回值:成功返回消息队列描述符,失败返回 (mqd_t)-1
参数说明:
| 参数 | 含义 |
|---|---|
| name | 消息队列名字,必须以斜杠开头,比如 "/myqueue" |
| oflag | O_RDONLY、O_WRONLY、O_RDWR,可组合 O_CREAT、O_EXCL |
| mode | 权限位,比如 0666 |
| attr | 指向 mq_attr 结构体,传 NULL 则使用默认属性 |
我曾经踩过一个坑:名字忘记加斜杠,结果 mq_open 一直返回失败。查了半天才发现是命名规范问题。所以大家记住——名字必须以 '/' 开头,这是硬性规定。
2.2 关闭与删除
关闭队列用 mq_close(),删除队列用 mq_unlink()。这两个的区别我刚开始也搞混过。
int mq_close(mqd_t mqdes); // 关闭当前进程对队列的引用
int mq_unlink(const char *name); // 删除队列本身
关键区别:
mq_close只是关闭当前进程的"门",其他进程还能继续用mq_unlink是真正把队列从系统中删除,但如果有其他进程还开着,队列会等到最后一个引用关闭后才真正销毁
避坑指南:我曾经在项目中忘记调用 mq_unlink,导致系统里残留了大量废弃的消息队列。用 ipcs -q 一看,好家伙,几十个僵尸队列。所以养成好习惯——程序退出前记得清理。
三、发送与接收:mq_send 和 mq_receive
这两个函数是消息队列的核心操作。发消息用 mq_send,收消息用 mq_receive。
int mq_send(mqd_t mqdes, const char *msg_ptr,
size_t msg_len, unsigned int msg_prio);
// 成功返回0,失败返回-1
ssize_t mq_receive(mqd_t mqdes, char *msg_ptr,
size_t msg_len, unsigned int *msg_prio);
// 成功返回消息字节数,失败返回-1
关于优先级:
- 优先级范围:0 到 sysconf(_SC_MQ_PRIO_MAX) - 1,通常是 0~32767
- 数值越大,优先级越高
- 接收时总是取优先级最高的消息
- 同优先级按先进先出顺序
我记得有个项目需要处理紧急告警,就把告警消息的优先级设成 255,普通日志设成 0。这样不管队列里积压了多少条日志,告警总能第一时间被处理。这个设计思路大家可以参考。
小技巧:如果队列满了,mq_send 会阻塞。你可以用 O_NONBLOCK 标志让它在队列满时立即返回 EAGAIN。同理,队列空时 mq_receive 也会阻塞。
四、队列属性:mq_getattr 和 mq_setattr
每个消息队列都有一组属性,用 mq_getattr 查看,用 mq_setattr 修改。
struct mq_attr {
long mq_flags; // 标志:0 或 O_NONBLOCK
long mq_maxmsg; // 队列最大消息数
long mq_msgsize; // 每条消息的最大字节数
long mq_curmsgs; // 当前队列中的消息数(只读)
};
int mq_getattr(mqd_t mqdes, struct mq_attr *attr);
int mq_setattr(mqd_t mqdes, const struct mq_attr *newattr,
struct mq_attr *oldattr);
实际应用场景:
- 创建队列前,先设置 mq_maxmsg 和 mq_msgsize
- 运行时用 mq_getattr 查看队列是否快满了
- 动态切换阻塞/非阻塞模式(修改 mq_flags)
我个人习惯在创建队列时就把属性设好,而不是用默认值。默认值通常很小(比如最大10条消息,每条消息最大8192字节),很容易在生产环境出问题。
五、消息通知机制:mq_notify
这是POSIX消息队列最强大的特性之一。你可以注册一个通知,当队列从空变为非空时,内核会通知你。
int mq_notify(mqd_t mqdes, const struct sigevent *notification);
// 成功返回0,失败返回-1
通知方式有两种:
| 方式 | 说明 |
|---|---|
| SIGEV_SIGNAL | 发送信号给进程,比如 SIGUSR1 |
| SIGEV_THREAD | 启动一个新线程来执行回调函数 |
我曾经在一个数据采集项目里用过 SIGEV_THREAD 方式。传感器数据通过消息队列传给处理进程,每次有新数据到达,自动触发一个线程来处理。这样主线程可以专心做其他事情,不用轮询队列。
注意:每个队列同时只能有一个注册的通知。如果你注册了新的通知,旧的通知会被覆盖。而且通知是一次性的——触发一次后需要重新注册。
六、知识体系总览
下面这张图是我自己整理的POSIX消息队列知识体系,大家可以对照着复习:
七、实际应用场景
说了这么多理论,咱们看看实际项目中怎么用。我总结了几种典型场景:
- 生产者-消费者模型:一个进程采集数据,多个进程处理数据。消息队列天然支持这种模式。
- 事件驱动架构:用 mq_notify 实现事件触发,避免轮询浪费CPU。
- 跨进程日志系统:所有进程把日志发到同一个队列,由一个专门的日志进程统一写入文件。
- 任务分发系统:主进程把任务按优先级排队,工作进程按优先级取任务执行。
总结一下:POSIX消息队列是Linux进程间通信的利器。它比管道灵活(支持优先级),比共享内存安全(不需要同步机制),比信号可靠(不会丢失消息)。我个人在嵌入式项目中几乎离不开它。
好了,这一章的内容就到这里。记住我强调的几个关键点:命名规范、属性设置、通知机制的一次性特性。下一章我们会深入代码实战,手把手教大家写一个完整的消息队列程序。