消息队列 (Message Queue)
概述
消息队列是 System V IPC 机制之一, 允许进程通过消息的形式进行通信. 消息队列中的每条消息都有一个类型字段, 接收进程可以根据类型选择性地接收消息. 消息队列在系统内核中维护, 具有持久性 (直到被显式删除).
通信原理
基本概念
消息队列的特点:
- 消息结构: 每条消息包含类型和正文两部分
- 类型选择: 接收进程可以根据消息类型选择接收
- 持久性: 消息队列在系统中存在, 直到被显式删除
- 任意进程通信: 不要求进程间有亲缘关系
- 消息边界: 每条消息作为一个整体传输
- 消息消费: 消息一旦被某个进程接收就会从队列中删除, 其他进程无法再接收同一条消息 (一对一消费模式)
实现机制
-
创建/获取消息队列:
- 使用
msgget()系统调用创建或获取消息队列 - 通过唯一的键值 (key) 标识消息队列
- 返回消息队列标识符 (msqid)
- 使用
-
消息结构:
cstruct msgbuf { long mtype; // 消息类型 (必须>0) char mtext[1]; // 消息正文 (可变长度) }; -
发送消息:
- 使用
msgsnd()将消息发送到队列 - 消息按照 FIFO 顺序排队
- 可以指定阻塞或非阻塞模式
- 使用
-
接收消息:
- 使用
msgrcv()从队列接收消息 - 可以根据消息类型选择接收
- 可以指定阻塞或非阻塞模式
- 重要: 消息一旦被接收就会从队列中删除, 无法被多个进程同时接收
- 使用
-
数据流向:
进程A --> [消息队列] --> 进程B 进程C --> --> 进程C
使用 msgtyp 实现 1 对多通信
通过 msgtyp 可以实现某种形式的 1 对多通信, 但需要注意以下限制和方案:
可行方案
方案 1: 发送多条消息 (推荐)
- 发送方为每个接收方发送一条消息, 每条消息使用不同的
mtype - 每个接收方通过
msgtyp指定接收自己对应的消息类型 - 优点: 每个接收方都能收到消息, 实现真正的 1 对多
- 缺点: 需要发送多条消息, 占用更多队列空间
示例:
c
// 发送方: 向 3 个接收方发送相同内容
msgsnd(msqid, &msg1, size, 0); // mtype = 1, 接收方 A
msgsnd(msqid, &msg2, size, 0); // mtype = 2, 接收方 B
msgsnd(msqid, &msg3, size, 0); // mtype = 3, 接收方 C
// 接收方 A
msgrcv(msqid, &msg, size, 1, 0); // 只接收 mtype = 1 的消息
方案 2: 使用消息类型范围
- 发送方发送一条消息,
mtype设置为公共类型 (如 100) - 接收方使用
msgtyp < 0接收小于等于 |msgtyp| 的消息 - 限制: 只有第一个接收方会收到消息, 消息随即被删除, 无法实现真正的广播
不可行方案
单条消息广播:
- 发送方只发送一条消息, 期望多个接收方都能收到
- 不可行: 消息一旦被第一个接收方接收就会从队列中删除, 其他接收方无法再接收
总结
- ✅ 可行: 通过发送多条消息 (不同 mtype) 实现 1 对多
- ❌ 不可行: 单条消息被多个进程同时接收 (广播)
- 💡 建议: 如果需要真正的广播功能, 考虑使用共享内存 + 信号量, 或 POSIX 消息队列的某些特性
API 说明
msgget()
c
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
- 功能: 创建或获取消息队列
- 参数 :
key: 消息队列的键值。注意键值冲突问题 :如果不同程序或实例使用相同的 key(如ftok()结果或固定的整数),会获得同一个队列,导致数据混淆或权限冲突。常见避免冲突的方法有:- 使用
ftok()并确保 pathname 唯一,例如用不同目录下的文件作为参数,或不同 proj_id; - 自定义 key 时采用进程独占,如用
IPC_PRIVATE创建专用队列,适用于父子进程或线程中通信; - 对于多人共享系统,建议提前约定并管理好 key 分配,防止不同项目间冲突;
- 实际部署时可通过环境变量、配置文件指定 key 路径和编号,避免硬编码。
- 使用
msgflg: 标志位, 可以是以下值的组合:IPC_CREAT: 如果队列不存在则创建, 存在则获取IPC_EXCL: 与 IPC_CREAT 一起使用, 如果队列已存在则返回错误- 权限位: 低 9 位指定权限 (类似文件权限, 如 0666 表示所有用户可读写)
- 返回值: 成功返回消息队列标识符, 失败返回 -1
- 错误码 :
EACCES: 权限不足, 无法访问消息队列EEXIST: 使用 IPC_CREAT | IPC_EXCL 时, 队列已存在ENOENT: 队列不存在且未指定 IPC_CREATENOMEM: 内存不足, 无法创建队列ENOSPC: 系统消息队列数量已达上限
msgsnd()
c
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
- 功能: 向消息队列发送消息
- 参数 :
msqid: 消息队列标识符msgp: 指向消息结构的指针msgsz: 消息正文的大小msgflg: 标志位, 可以是以下值:0: 阻塞模式, 队列满时阻塞等待IPC_NOWAIT: 非阻塞模式, 队列满时立即返回 EAGAIN 错误
- 返回值: 成功返回 0, 失败返回 -1
- 错误码 :
EACCES: 权限不足, 无法写入消息队列EAGAIN: 队列满且指定了 IPC_NOWAITEFAULT:msgp指向的地址无效EIDRM: 消息队列已被删除EINTR: 被信号中断EINVAL: 参数无效 (msqid 无效、msgsz 无效、mtype < 0 等)ENOMEM: 内存不足, 无法分配消息空间
msgrcv()
c
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
- 功能: 从消息队列接收消息
- 参数 :
msqid: 消息队列标识符msgp: 指向消息缓冲区的指针msgsz: 缓冲区大小msgtyp: 消息类型 (0=接收任意类型, >0=接收指定类型, <0=接收小于等于|msgtyp|的类型)msgflg: 标志位, 可以是以下值的组合:0: 阻塞模式, 无匹配消息时阻塞等待IPC_NOWAIT: 非阻塞模式, 无匹配消息时立即返回 ENOMSG 错误MSG_NOERROR: 如果消息正文大于msgsz, 截断消息而不返回错误
- 返回值: 成功返回接收的字节数, 失败返回 -1
- 重要说明 :
- 消息删除 :
msgrcv()成功接收消息后, 该消息会从队列中永久删除, 其他进程无法再接收同一条消息 - 多端接收: 多个进程无法同时接收同一条消息, 消息一旦被某个进程接收就会从队列中移除, 这是消息队列与共享内存的重要区别
- 消息分发: 如果多个进程同时等待接收消息, 系统会根据消息类型和接收顺序选择一个进程接收, 其他进程继续等待下一条消息
- 消息删除 :
- 错误码 :
E2BIG: 消息正文大小超过msgsz且未指定 MSG_NOERROREACCES: 权限不足, 无法读取消息队列EAGAIN: 队列中无匹配消息且指定了 IPC_NOWAITEFAULT:msgp指向的地址无效EIDRM: 消息队列已被删除EINTR: 被信号中断EINVAL: 参数无效 (msqid 无效、msgsz < 0 等)ENOMSG: 队列中无匹配类型的消息且指定了 IPC_NOWAIT
msgctl()
c
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
- 功能: 控制消息队列 (删除、获取信息等)
- 参数 :
msqid: 消息队列标识符cmd: 命令 (IPC_RMID 删除队列, IPC_STAT 获取状态等)buf: 状态缓冲区
- 返回值: 成功返回 0, 失败返回 -1
- 错误码 :
EACCES: 权限不足, 无法执行指定操作EFAULT:buf指向的地址无效 (IPC_SET 或 IPC_STAT 时)EIDRM: 消息队列已被删除EINVAL: 参数无效 (msqid 无效、cmd 无效等)EPERM: 权限不足, 无法执行 IPC_SET 或 IPC_RMID 操作
示例代码
下面给出一个基于 System V 消息队列的简单收发消息示例:
1. 定义消息结构体:
c
struct msgbuf {
long mtype; // 消息类型,必须 >0
char mtext[128]; // 消息正文
};
2. 发送方示例(sender.c):
c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
struct msgbuf {
long mtype;
char mtext[128];
};
int main(void) {
key_t key = ftok(".", 'm'); // 生成唯一键值
int msqid = msgget(key, IPC_CREAT | 0666); // 创建或获取队列
if(msqid == -1) return 1;
struct msgbuf msg;
msg.mtype = 1;
strcpy(msg.mtext, "Hello, message queue!");
if(msgsnd(msqid, &msg, strlen(msg.mtext)+1, 0) == -1) {
perror("msgsnd");
return 1;
}
printf("消息已发送。\n");
return 0;
}
3. 接收方示例(receiver.c):
c
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <string.h>
struct msgbuf {
long mtype;
char mtext[128];
};
int main(void) {
key_t key = ftok(".", 'm'); // 和发送方相同
int msqid = msgget(key, 0666); // 获取队列
if(msqid == -1) return 1;
struct msgbuf msg;
if(msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0) == -1) {
perror("msgrcv");
return 1;
}
printf("收到消息: %s\n", msg.mtext);
// 用完后删除队列
msgctl(msqid, IPC_RMID, NULL);
return 0;
}
4. 编译与运行
sh
gcc sender.c -o sender
gcc receiver.c -o receiver
# 首开终端A,先运行接收方(阻塞等待消息)
./receiver
# 新开终端B,运行发送方
./sender
这样就可以完成简单的进程间消息收发。
性能评价
优点
- 消息边界: 每条消息作为整体传输, 保持消息边界
- 类型选择: 可以根据消息类型选择性接收
- 持久性: 消息队列在系统中持久存在
- 任意进程通信: 不要求进程间有亲缘关系
- 阻塞/非阻塞: 支持阻塞和非阻塞模式
缺点
- 系统限制 : 系统对消息队列数量和单条消息大小有限制,例如单个系统允许的消息队列总数(
msgmni)、每个队列的最大字节数(msgmnb)、单条消息的最大字节数(msgmax)等。可以通过ipcs -l或cat /proc/sys/kernel/msg*查看相关限制。要修改这些参数,可以使用sysctl命令(如sysctl -w kernel.msgmax=8192),或临时修改/proc/sys/kernel/下对应参数(如echo 8192 > /proc/sys/kernel/msgmax)。需要管理员权限。 - 性能开销: 需要系统调用, 有一定开销
- 复杂性: API 相对复杂, 需要管理键值
- 系统资源: 占用系统资源, 需要显式删除
性能特点
- 延迟: 中等 (需要系统调用)
- 吞吐量: 中等 (受系统限制影响)
- CPU 占用: 中等
- 内存占用: 中等 (内核维护队列)
适用场景
- ✅ 需要消息边界的通信
- ✅ 需要根据类型选择消息的场景
- ✅ 多对多进程通信
- ✅ 需要持久化消息的场景
- ✅ 1 对多通信 (通过发送多条不同 mtype 的消息实现)
- ❌ 对性能要求极高的场景
- ❌ 简单的单向通信 (管道更合适)
- ❌ 单条消息广播 (一条消息被多个进程同时接收)
注意事项
- 键值管理 : 使用
ftok()生成键值或使用 IPC_PRIVATE - 消息大小限制: 注意系统对消息大小的限制
- 资源清理 : 使用完毕后应该删除消息队列 (
msgctl(IPC_RMID)) - 权限设置: 注意设置合适的权限, 避免权限问题
- 错误处理: 注意处理队列满、队列空等错误情况
- 系统限制 : 注意系统的消息队列数量限制 (
ipcs -l查看) - 消息消费模式: 消息一旦被接收就会从队列中删除, 无法实现多进程同时接收同一条消息; 如果需要广播或多播功能, 应考虑其他 IPC 机制 (如共享内存 + 信号量)
扩展阅读
man 2 msggetman 2 msgsndman 2 msgrcvman 2 msgctlman 7 mq_overview