POSIX 消息队列 (Message Queue)
概述
POSIX 消息队列是 POSIX.1b 标准定义的进程间通信机制, 允许进程通过消息的形式进行通信. POSIX 消息队列中的每条消息都有一个优先级字段, 接收进程可以根据优先级选择性地接收消息. 消息队列在系统内核中维护, 具有持久性(直到被显式删除), 在 /dev/mqueue 文件系统中可见.
通信原理
基本概念
POSIX 消息队列的特点:
- 消息结构: 每条消息包含优先级和正文两部分
- 优先级选择: 接收进程可以根据消息优先级选择接收(高优先级先接收)
- 持久性: 消息队列在系统中存在, 直到被显式删除
- 任意进程通信: 不要求进程间有亲缘关系
- 消息边界: 每条消息作为一个整体传输
- 消息消费: 消息一旦被某个进程接收就会从队列中删除, 其他进程无法再接收同一条消息(一对一消费模式)
- 文件系统可见 : 在
/dev/mqueue文件系统中可见, 便于管理
实现机制
-
创建/打开消息队列:
- 使用
mq_open()系统调用创建或打开消息队列 - 通过名字(如
/my_mq)标识消息队列 - 返回消息队列描述符(
mqd_t)
- 使用
-
消息结构:
- POSIX 消息队列不要求特定的消息结构
- 消息正文是任意字节序列
- 每条消息有一个优先级(
unsigned int)
-
发送消息:
- 使用
mq_send()将消息发送到队列 - 消息按照优先级和 FIFO 顺序排队
- 可以指定阻塞或非阻塞模式
- 使用
-
接收消息:
- 使用
mq_receive()从队列接收消息 - 总是接收优先级最高的消息
- 可以指定阻塞或非阻塞模式
- 重要: 消息一旦被接收就会从队列中删除, 无法被多个进程同时接收
- 使用
-
数据流向:
进程A --> [消息队列] --> 进程B 进程C --> --> 进程C
消息优先级
POSIX 消息队列使用优先级而非类型来区分消息:
- 优先级范围 :
0到sysconf(_SC_MQ_PRIO_MAX)- 1 - 优先级规则: 数值越大, 优先级越高
- 接收规则: 总是接收优先级最高的消息, 同优先级按 FIFO 顺序
与 System V 消息队列的区别:
- System V: 使用
mtype(long) 作为消息类型, 可以按类型选择接收 - POSIX: 使用优先级(unsigned int), 总是接收最高优先级的消息
使用优先级实现 1 对多通信
通过优先级可以实现某种形式的 1 对多通信, 但需要注意以下限制和方案:
可行方案
方案 1: 发送多条消息(推荐)
- 发送方为每个接收方发送一条消息, 每条消息使用不同的优先级
- 每个接收方通过优先级范围选择接收
- 限制: 需要发送多条消息, 占用更多队列空间
示例:
c
// 发送方: 向 3 个接收方发送相同内容
mq_send(mqdes, msg1, size, 1); // 优先级 1, 接收方 A
mq_send(mqdes, msg2, size, 2); // 优先级 2, 接收方 B
mq_send(mqdes, msg3, size, 3); // 优先级 3, 接收方 C
// 接收方 A: 接收优先级 1 的消息
// 注意: POSIX 消息队列总是接收最高优先级消息, 无法直接按优先级过滤
// 需要接收后检查优先级, 不匹配则重新发送或使用其他机制
方案 2: 使用共享内存 + 信号量
- 如果需要真正的广播功能, 考虑使用共享内存 + 信号量
- 共享内存可以同时被多个进程读取
不可行方案
单条消息广播:
- 发送方只发送一条消息, 期望多个接收方都能收到
- 不可行: 消息一旦被第一个接收方接收就会从队列中删除, 其他接收方无法再接收
总结
- ✅ 可行: 通过发送多条消息(不同优先级)实现 1 对多
- ❌ 不可行: 单条消息被多个进程同时接收(广播)
- 💡 建议: 如果需要真正的广播功能, 考虑使用共享内存 + 信号量
API 说明
mq_open()
c
#include <mqueue.h>
mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);
- 功能: 创建或打开消息队列
- 参数 :
name: 消息队列的名字, 必须以/开头(如/my_mq)oflag: 标志位, 可以是以下值的组合:O_CREAT: 如果队列不存在则创建, 存在则打开O_EXCL: 与O_CREAT一起使用, 如果队列已存在则返回错误O_RDONLY: 只读模式O_WRONLY: 只写模式O_RDWR: 读写模式O_NONBLOCK: 非阻塞模式
mode: 权限位(类似文件权限, 如 0666 表示所有用户可读写)attr: 指向mq_attr结构的指针, 用于设置队列属性(创建时), 可以为 NULL 使用默认值
- 返回值 : 成功返回消息队列描述符, 失败返回
(mqd_t)-1 - 错误码 :
EACCES: 权限不足, 无法访问消息队列EEXIST: 使用O_CREAT | O_EXCL时, 队列已存在EINVAL: 参数无效(名字格式错误、属性无效等)EMFILE: 进程打开的消息队列数量已达上限ENAMETOOLONG: 名字太长ENOENT: 队列不存在且未指定O_CREATENOSPC: 系统消息队列数量已达上限
mq_attr 结构:
c
struct mq_attr {
long mq_flags; // 标志位(如 O_NONBLOCK)
long mq_maxmsg; // 队列中最大消息数
long mq_msgsize; // 单条消息最大大小
long mq_curmsgs; // 当前队列中的消息数(只读)
};
mq_send()
c
#include <mqueue.h>
int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio);
- 功能: 向消息队列发送消息
- 参数 :
mqdes: 消息队列描述符msg_ptr: 指向消息正文的指针msg_len: 消息正文的长度msg_prio: 消息优先级(0 到sysconf(_SC_MQ_PRIO_MAX)- 1)
- 返回值: 成功返回 0, 失败返回 -1
- 错误码 :
EAGAIN: 队列满且指定了O_NONBLOCKEBADF: 消息队列描述符无效EINTR: 被信号中断EINVAL: 参数无效(msg_len超过mq_msgsize、msg_prio无效等)EMSGSIZE: 消息大小超过队列限制
mq_receive()
c
#include <mqueue.h>
ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio);
- 功能: 从消息队列接收消息
- 参数 :
mqdes: 消息队列描述符msg_ptr: 指向消息缓冲区的指针msg_len: 缓冲区大小(必须 >=mq_msgsize)msg_prio: 指向unsigned int的指针, 用于返回消息优先级(可以为 NULL)
- 返回值: 成功返回接收的字节数, 失败返回 -1
- 重要说明 :
- 消息删除 :
mq_receive()成功接收消息后, 该消息会从队列中永久删除, 其他进程无法再接收同一条消息 - 多端接收: 多个进程无法同时接收同一条消息, 消息一旦被某个进程接收就会从队列中移除
- 优先级规则: 总是接收优先级最高的消息, 同优先级按 FIFO 顺序
- 消息删除 :
- 错误码 :
EAGAIN: 队列中无消息且指定了O_NONBLOCKEBADF: 消息队列描述符无效EINTR: 被信号中断EINVAL: 参数无效(msg_len<mq_msgsize等)EMSGSIZE: 缓冲区大小小于消息大小
mq_close()
c
#include <mqueue.h>
int mq_close(mqd_t mqdes);
- 功能: 关闭消息队列描述符
- 参数 :
mqdes- 消息队列描述符 - 返回值: 成功返回 0, 失败返回 -1
- 注意 : 关闭描述符不会删除队列, 需要单独调用
mq_unlink()删除
mq_unlink()
c
#include <mqueue.h>
int mq_unlink(const char *name);
- 功能: 删除消息队列
- 参数 :
name- 消息队列的名字 - 返回值: 成功返回 0, 失败返回 -1
- 注意: 删除后, 已打开的进程仍可使用, 但新进程无法再打开该队列
mq_getattr() / mq_setattr()
c
#include <mqueue.h>
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);
- 功能: 获取或设置消息队列属性
- 参数 :
mqdes: 消息队列描述符attr/newattr/oldattr: 指向mq_attr结构的指针
- 返回值: 成功返回 0, 失败返回 -1
- 注意 :
mq_setattr()只能修改mq_flags(如设置/清除O_NONBLOCK), 不能修改mq_maxmsg和mq_msgsize
示例代码
下面给出一个基于 POSIX 消息队列的简单收发消息示例:
1. 发送方示例(sender.c):
c
#include <fcntl.h>
#include <mqueue.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define QUEUE_NAME "/my_mq"
#define MAX_MSG_SIZE 128
#define MSG_PRIORITY 0
int main(int argc, char *argv[]) {
const char *text = (argc > 1) ? argv[1] : "Hello, POSIX message queue!";
mqd_t mq;
struct mq_attr attr;
// 设置消息队列属性
attr.mq_flags = 0; // 阻塞模式
attr.mq_maxmsg = 10; // 最大消息数
attr.mq_msgsize = MAX_MSG_SIZE; // 最大消息大小
attr.mq_curmsgs = 0; // 当前消息数
// 创建或打开消息队列
mq = mq_open(QUEUE_NAME, O_CREAT | O_WRONLY, 0666, &attr);
if (mq == (mqd_t)-1) {
perror("mq_open");
return 1;
}
// 发送消息
if (mq_send(mq, text, strlen(text) + 1, MSG_PRIORITY) == -1) {
perror("mq_send");
mq_close(mq);
return 1;
}
printf("消息已发送: %s\n", text);
// 关闭消息队列
mq_close(mq);
return 0;
}
2. 接收方示例(receiver.c):
c
#include <fcntl.h>
#include <mqueue.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define QUEUE_NAME "/my_mq"
#define MAX_MSG_SIZE 128
int main() {
mqd_t mq;
char buffer[MAX_MSG_SIZE];
unsigned int priority;
ssize_t bytes_read;
// 打开消息队列(只读)
mq = mq_open(QUEUE_NAME, O_RDONLY);
if (mq == (mqd_t)-1) {
perror("mq_open");
printf("消息队列不存在, 请先运行 sender 程序\n");
return 1;
}
// 接收消息
bytes_read = mq_receive(mq, buffer, MAX_MSG_SIZE, &priority);
if (bytes_read == -1) {
perror("mq_receive");
mq_close(mq);
return 1;
}
printf("收到消息: %s (优先级: %u, 大小: %zd 字节)\n",
buffer, priority, bytes_read);
// 关闭消息队列
mq_close(mq);
// 删除消息队列
if (mq_unlink(QUEUE_NAME) == -1) {
perror("mq_unlink");
return 1;
}
printf("消息队列已删除\n");
return 0;
}
3. 编译与运行
sh
gcc sender.c -o sender -lrt
gcc receiver.c -o receiver -lrt
# 首开终端 A, 先运行接收方(阻塞等待消息)
./receiver
# 新开终端 B, 运行发送方
./sender
性能评价
优点
- 消息边界: 每条消息作为整体传输, 保持消息边界
- 优先级支持: 支持消息优先级, 高优先级消息先被接收
- 持久性: 消息队列在系统中持久存在
- 任意进程通信: 不要求进程间有亲缘关系
- 阻塞/非阻塞: 支持阻塞和非阻塞模式
- 文件系统可见 : 在
/dev/mqueue中可见, 便于管理 - 跨平台: POSIX 标准, 跨平台兼容性好
- 名字直观: 使用名字标识, 比键值更直观
缺点
- 系统限制 : 系统对消息队列数量和单条消息大小有限制, 可以通过
cat /proc/sys/fs/mqueue/*查看相关限制 - 性能开销: 需要系统调用, 有一定开销
- 复杂性: API 相对复杂, 需要管理名字
- 系统资源: 占用系统资源, 需要显式删除
- 链接库 : 某些系统需要链接
librt库 - 优先级限制: 无法像 System V 那样按类型选择接收, 只能按优先级
性能特点
- 延迟: 中等(需要系统调用)
- 吞吐量: 中等(受系统限制影响)
- CPU 占用: 中等
- 内存占用: 中等(内核维护队列)
适用场景
- ✅ 需要消息边界的通信
- ✅ 需要消息优先级的场景
- ✅ 多对多进程通信
- ✅ 需要持久化消息的场景
- ✅ 需要跨平台移植的场景
- ❌ 对性能要求极高的场景
- ❌ 简单的单向通信(管道更合适)
- ❌ 单条消息广播(一条消息被多个进程同时接收)
- ❌ 需要按类型选择接收的场景(System V 更适合)
注意事项
- 名字管理: 使用有意义的名字, 避免名字冲突
- 消息大小限制: 注意系统对消息大小的限制
- 资源清理 : 使用完毕后应该删除消息队列(
mq_unlink()) - 权限设置: 注意设置合适的权限, 避免权限问题
- 错误处理: 注意处理队列满、队列空等错误情况
- 系统限制: 注意系统的消息队列数量限制
- 消息消费模式: 消息一旦被接收就会从队列中删除, 无法实现多进程同时接收同一条消息; 如果需要广播或多播功能, 应考虑其他 IPC 机制(如共享内存 + 信号量)
- 文件系统挂载 : 确保
/dev/mqueue已挂载 - 链接库 : 某些系统需要链接
librt库(-lrt) - 名字格式 : 名字必须以
/开头 - 优先级范围 : 注意优先级范围限制(
sysconf(_SC_MQ_PRIO_MAX))
与 System V 消息队列的区别
| 特性 | System V | POSIX |
|---|---|---|
| 标识方式 | 键值(key) | 名字(name) |
| 消息类型 | mtype (long) |
优先级(unsigned int) |
| 接收规则 | 可按类型选择接收 | 总是接收最高优先级 |
| API 设计 | 较老 | 较现代 |
| 文件系统可见 | 否 | 是(/dev/mqueue) |
| 删除方式 | msgctl(IPC_RMID) |
mq_unlink() |
| 管理工具 | ipcs/ipcrm |
ls/rm (文件系统) |
| 跨平台 | 较差 | 较好(POSIX 标准) |
扩展阅读
man 2 mq_openman 2 mq_sendman 2 mq_receiveman 2 mq_closeman 2 mq_unlinkman 7 mq_overview