1.消息队列理论
1.1 什么是“消息”
-
消息(message):进程间传递的数据单位。
-
可以很简单:一个字符串(例如
"hello"
)。 -
也可以很复杂:包含多个字段(id、时间戳、状态码、二进制对象等)。
-
-
语义:消息是“自包含”的数据包——发送者封装好数据,接收者按照约定解析即可。
-
异步传递:发送方把消息放入队列后立即返回,不必等接收方处理(解耦、提高并发)。
1.2 什么是消息队列(抽象层)
-
消息队列 = 内核/中间件维护的容器(链表/队列),用于暂存消息、做路由和调度。
-
类比:快递柜
-
生产者 = 快递员,把快递放进柜子(写入队列)。
-
消费者 = 收件人,从柜子里取快递(读出消息)。
-
如果收件人不在,快递(消息)会被保存在柜子里直到被取走(持久性到队列删除或系统重启)。
-
-
核心价值:解耦 + 缓冲 + 可控路由(定向/优先)。
1.3 消息队列的主要特性(要点)
-
消息有类型(mtype)
-
每条消息带一个
mtype
(System V 要求long mtype
放在结构体首位)。 -
mtype
不是“队列成员/进程 ID”,而是“消息的标签/类别”,接收方可以按类型选择接收。
-
-
消息有格式
-
通常用
struct
表示:long mtype; /*正文*/
。正文可以包含若干字段或 union。
-
-
读取可以按类型选择(不是强制 FIFO)
-
可按
mtype
精确取单一类型,也可取队首(msgtyp=0
)。 -
若选择某类型,队列内部该类型消息仍遵循该类型内的 FIFO。
-
-
支持多写多读(多对多)
-
多个进程可以同时写入,也可以同时读取(并通过类型、权限、阻塞/非阻塞等控制)。
-
-
读出即删除
-
成功
msgrcv()
后该条消息从队列中移除(不像日志那样保留)。
-
-
队列是内核资源(持久)
-
存在于内核(不是进程内存),进程间共享。只有重启内核或显式
msgctl(IPC_RMID)
才会删除(否则一直存在)。
-
-
实现与限制相关(OS/内核/配置)
-
不同系统对单条消息长度、队列总字节数、最大队列数、系统最大消息数有上限(以内核/发行版为准,开发时应查询系统限制或读取
/proc/sys/kernel
相关项)。
-
1.4 System V vs POSIX(简要对比)
-
System V 消息队列
-
通过
ftok
/key
+msgget
获得(或创建)队列。 -
消息结构要求
long mtype
为首字段。
-
-
POSIX 消息队列
-
通过字符串名字
mq_open("/name", ...)
访问。 -
API、特性、权限表示有差异(更现代、跨进程名称更直观)。
(后续章节会详细对比与示例)
-
1.5 常见误区与面试点(快速问答)
-
误区:
mtype
= 接收进程 PID?-
不是。
mtype
是消息标签。接收者可以选择只接收某个mtype
的消息(实现“私聊”或“主题订阅”)。
-
-
误区:一次
msgsnd()
可以放多条消息?-
不能。一次
msgsnd()
发送一条消息。如果想发送多条要多次调用,或把“多条消息”序列化到一条消息的正文中并自行解析。
-
-
误区:要为不同消息写很多 struct?
-
不必。通常用一个包含
union
的正文字段或约定格式即可。只有内容差异巨大或为简洁类型安全时才写多个 struct。
-
-
面试点:
msqid
为什么会一直增?-
msqid
是内核分配的标识符,内核使用递增计数器分配新 id,删除后短时间内一般不复用旧 id。
-
-
面试点:kill 后的子进程是否要回收?
-
要。父进程需
wait()/waitpid()
回收退出状态,否则会产生成僵尸(或通过SIGCHLD
处理/忽略避免僵尸)。
-
2.消息队列的创建与使用
2.1 创建与获取队列
在 System V IPC 里,消息队列由 key 标识。通过 msgget()
来创建或获取。
关键函数
int msgget(key_t key, int msgflg);
-
参数
-
key
:队列的键值,通常通过ftok()
生成。 -
msgflg
:标志位-
IPC_CREAT
:若不存在则创建。 -
IPC_EXCL
:与IPC_CREAT
一起用,若已存在则报错。 -
权限位(如
0664
),类似文件权限。
-
-
-
返回值
-
成功:返回队列标识符
msqid
。 -
失败:返回
-1
,errno
指出原因。
-
例子
key_t key = ftok("/home/liuxudong", 65);
if (key == -1) {
perror("ftok");
exit(1);
}
int msqid = msgget(key, IPC_CREAT | 0664);
if (msqid == -1) {
perror("msgget");
exit(1);
}
printf("队列ID: %d\n", msqid);
2.2 消息结构
System V 要求消息的结构体 第一个字段必须是 long mtype
:
typedef struct {
long mtype; // 消息类型 (必须 > 0)
char data[100]; // 消息正文
} MSG;
注意:
-
mtype
用来标记消息类别,接收时可以按类别选择。 -
data
可以是字符串、结构体、二进制等,只要大小不超过系统限制。
2.3 发送消息
使用 msgsnd()
把消息放入队列。
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
-
参数
-
msqid
:队列ID。 -
msgp
:指向消息结构体的指针。 -
msgsz
:消息正文大小(不包括mtype
)。 -
msgflg
:-
0
:默认阻塞(队列满则等待)。 -
IPC_NOWAIT
:队列满立即返回错误。
-
-
-
返回值
-
成功:0
-
失败:-1
-
例子
MSG msg;
msg.mtype = 1; // 发给类型1的接收者
strcpy(msg.data, "hello from sender");
if (msgsnd(msqid, &msg, sizeof(msg.data), 0) == -1) {
perror("msgsnd");
exit(1);
}
2.4 接收消息
使用 msgrcv()
从队列中取消息。
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
-
参数
-
msqid
:队列ID。 -
msgp
:指向消息结构体的指针。 -
msgsz
:正文大小(不包括mtype
)。 -
msgtyp
:选择接收的消息类型。-
0
:接收队列中的第一条消息(FIFO)。 -
>0
:接收第一个mtype == msgtyp
的消息。 -
<0
:接收第一个mtype <= |msgtyp|
的消息。
-
-
msgflg
:0
阻塞,IPC_NOWAIT
非阻塞。
-
-
返回值
-
成功:接收到的字节数(正文大小)
-
失败:-1
-
例子
MSG msg;
if (msgrcv(msqid, &msg, sizeof(msg.data), 1, 0) == -1) {
perror("msgrcv");
exit(1);
}
printf("接收到消息: %s\n", msg.data);
2.5 完整示例:简单聊天
sender.c
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
long mtype;
char data[100];
} MSG;
int main() {
key_t key = ftok("/home/liuxudong", 65);
int msqid = msgget(key, IPC_CREAT | 0664);
MSG msg;
msg.mtype = 1;
while (1) {
printf("输入消息: ");
scanf("%s", msg.data);
if (strcmp(msg.data, "exit") == 0) break;
msgsnd(msqid, &msg, sizeof(msg.data), 0);
}
}
receiver.c
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
typedef struct {
long mtype;
char data[100];
} MSG;
int main() {
key_t key = ftok("/home/liuxudong", 65);
int msqid = msgget(key, 0664);
MSG msg;
while (1) {
msgrcv(msqid, &msg, sizeof(msg.data), 1, 0);
printf("收到: %s\n", msg.data);
}
}
gcc sender.c -o sender
gcc receiver.c -o receiver
./receiver
./sender
2.6 常见错误
-
结构体定义错误
-
long mtype
必须是第一个字段,否则msgrcv
会出错。
-
-
msgsz 传错
-
msgsnd
/msgrcv
的第三个参数 只写正文大小,不包括 mtype。
-
-
权限不够
-
如果
msgget
权限设置太严格(如 0600),其他用户进程可能无法访问。
-
-
队列泄露
-
如果程序退出前不删除队列,队列会一直存在。用
ipcrm -q msqid
清理。
-
2.7 清理队列
删除队列:
msgctl(msqid, IPC_RMID, NULL);
查看/管理队列:
ipcs -q # 查看所有消息队列
ipcrm -q id # 删除指定队列
第3章 消息队列的管理与控制
3.1 控制函数 msgctl
#include <sys/msg.h>
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
-
参数
-
msqid
:队列标识符(由msgget
返回)。 -
cmd
:操作命令。 -
buf
:一个struct msqid_ds *
指针,用来存储或设置队列属性。
-
-
常见 cmd
-
IPC_STAT
:获取队列的状态,结果写入buf
。 -
IPC_SET
:修改队列的部分属性(权限、最大字节数等)。 -
IPC_RMID
:删除队列。
-
-
返回值
-
成功:0
-
失败:-1
-
3.2 队列描述结构体
struct msqid_ds {
struct ipc_perm msg_perm; // 权限信息
time_t msg_stime; // 最后一次 msgsnd 时间
time_t msg_rtime; // 最后一次 msgrcv 时间
time_t msg_ctime; // 最后一次修改时间
unsigned long __msg_cbytes; // 队列当前字节数
msgqnum_t msg_qnum; // 队列当前消息数量
msglen_t msg_qbytes; // 队列允许的最大字节数
pid_t msg_lspid; // 最后一次 msgsnd 的进程 PID
pid_t msg_lrpid; // 最后一次 msgrcv 的进程 PID
};
-
msg_perm
结构(权限信息):
struct ipc_perm {
key_t __key; // key 值
uid_t uid; // 队列所有者 UID
gid_t gid; // 队列所有者 GID
uid_t cuid; // 创建者 UID
gid_t cgid; // 创建者 GID
unsigned short mode; // 权限(类似文件权限)
unsigned short __seq;
};
3.3 删除队列
删除消息队列最常用的方式:
msgctl(msqid, IPC_RMID, NULL);
例子:
int msqid = msgget(key, 0664);
msgctl(msqid, IPC_RMID, NULL);
printf("消息队列已删除\n");
命令行删除:
ipcrm -q msqid
3.4 获取队列状态
例子:查看消息队列的属性。
#include <sys/ipc.h>
#include <sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
int main() {
key_t key = ftok("/home/liuxudong", 65);
int msqid = msgget(key, 0664);
struct msqid_ds info;
if (msgctl(msqid, IPC_STAT, &info) == -1) {
perror("msgctl");
exit(1);
}
printf("当前消息数: %lu\n", info.msg_qnum);
printf("最大字节数: %lu\n", info.msg_qbytes);
printf("最后发送者PID: %d\n", info.msg_lspid);
printf("最后接收者PID: %d\n", info.msg_lrpid);
return 0;
}
运行结果可能是:
当前消息数: 3 最大字节数: 16384 最后发送者PID: 1234 最后接收者PID: 5678
3.5 修改队列属性
我们可以通过 IPC_SET
修改一些字段,例如 队列容量 或 权限。
例子:修改消息队列的最大容量。
struct msqid_ds info;
msgctl(msqid, IPC_STAT, &info);
info.msg_qbytes = 32768; // 设置为 32KB
if (msgctl(msqid, IPC_SET, &info) == -1) {
perror("msgctl");
}
注意:需要有 root 权限 才能提高容量上限。
3.6 使用 ipcs
工具查看
命令行工具 ipcs
可以查看当前系统中的消息队列。
ipcs -q
输出示例:
------ Message Queues --------
key msqid owner perms used-bytes messages
0x12345678 32769 liuxudong 664 0 0
字段说明:
-
key
:队列 key 值。 -
msqid
:队列标识符。 -
owner
:队列所有者。 -
perms
:权限(类似文件权限)。 -
used-bytes
:已占用字节数。 -
messages
:消息数量。
删除:
ipcrm -q 32769
3.7 小结
-
msgctl
提供了 删除、获取状态、修改属性 三大功能。 -
msqid_ds
保存了队列的详细信息,包括 消息数、容量、权限、最后操作进程 PID。 -
命令行工具
ipcs -q
和ipcrm -q
是日常调试的利器。