一、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"
oflagO_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消息队列知识体系,大家可以对照着复习:

POSIX 消息队列 创建与关闭 mq_open() 创建 mq_close() 关闭 mq_unlink() 删除 发送与接收 mq_send() 发送 mq_receive() 接收 属性管理 mq_getattr() 查询 mq_setattr() 修改 消息通知机制 SIGEV_SIGNAL 信号通知 SIGEV_THREAD 线程回调 图:POSIX消息队列核心功能模块

七、实际应用场景

说了这么多理论,咱们看看实际项目中怎么用。我总结了几种典型场景:

  1. 生产者-消费者模型:一个进程采集数据,多个进程处理数据。消息队列天然支持这种模式。
  2. 事件驱动架构:用 mq_notify 实现事件触发,避免轮询浪费CPU。
  3. 跨进程日志系统:所有进程把日志发到同一个队列,由一个专门的日志进程统一写入文件。
  4. 任务分发系统:主进程把任务按优先级排队,工作进程按优先级取任务执行。

总结一下:POSIX消息队列是Linux进程间通信的利器。它比管道灵活(支持优先级),比共享内存安全(不需要同步机制),比信号可靠(不会丢失消息)。我个人在嵌入式项目中几乎离不开它。

好了,这一章的内容就到这里。记住我强调的几个关键点:命名规范、属性设置、通知机制的一次性特性。下一章我们会深入代码实战,手把手教大家写一个完整的消息队列程序。


公众号:蓝海资料掘金营,微信deep3321