第 46 章 System V 消息队列 (System V Message Queues)
核心结论
-
消息队列特征:message-oriented(整条消息原子读,无消息边界)、per-message 整数 type 字段(
mtype,>0)、按 type 选择读(=0 FIFO,>0 精确,<0 优先级取 ≤|msgtyp| 中最低 mtype)、descriptor 是 int id(不是 fd)。 -
msgsnd / msgrcv 协议:消息结构 =
struct { long mtype; char mtext[]; }(mtype > 0,mtext 大小任意);msgsnd写、msgrcv读(返回 mtext 长度);flag 含 IPC_NOWAIT / MSG_NOERROR / MSG_EXCEPT(Linux-specific)。 -
msqid_ds 数据结构:
msg_perm/msg_stime/msg_rtime/msg_ctime/__msg_cbytes/msg_qnum/msg_qbytes/msg_lspid/msg_lrpid——msg_qbytes控制队列容量上限(IPC_SET 改)。 -
Limits:
MSGMNI(队列数)、MSGMAX(单条字节数)、MSGMNB(队列总字节);/proc/sys/kernel/msgmni、msgmax、msgmnb调整。 -
MSG_INFO/STAT:Linux 扩展编程接口;
MSG_INFO返回 maxid + msginfo 结构;MSG_STAT按 index 查 id——用于写自己的ipcs -q。 -
client-server 模式:两种——单 queue for both directions(用 mtype=1 区分 server 收、用 client PID 区分 client 收);or 1 queue per client(推荐用于大消息)——SV_MSG_FILE 风格。
|
本章主旨
SysV 消息队列是「带 type 的消息链」——与管道的字节流相比,消息边界保留 + 类型过滤使其适合结构化 IPC。本章覆盖 (1) msgget/msgsnd/msgrcv/msgctl 的具体语义;(2) 消息结构和 type 字段;(3) msqid_ds 关联结构;(4) msg_qbytes 容量控制;(5) 实际 client-server 设计(file server 实例)。读者应理解 type 字段的妙用——它让多个 client 在同一队列上各取所需消息,不需要像管道那样抢读。 |
一、核心概念
本章围绕 6 个核心概念展开:从消息队列 vs 管道特征、msgsnd/msgrcv 协议、mtype 选消息、msgctl 控制、msqid_ds 字段、client-server 模式。
| 概念 | 定义 + 重要性 | 实现提示 |
|---|---|---|
消息 vs 字节流 |
消息队列保留消息边界(整条原子读),可按 mtype 选择——比管道的字节流更适合结构化协议;不像 POSIX mq 用 mqd_t,这里用 int id |
§46.0;SVID 历史——比 POSIX mq 早,但 API 不如 POSIX 直观 |
msgsnd / msgrcv 协议 |
|
§46.2; |
按 mtype 选消息 |
msgtyp=0 → 第一条;msgtyp>0 → 第一个 mtype 相等的;msgtyp<0 → 优先级取 ≤|msgtyp| 中最低 mtype 的 |
§46.2.2;client 常用「msgtyp = own PID」让 server 定向回复 |
msqid_ds 关联数据 |
|
§46.4;msg_qbytes 默认 = MSGMNB;特权 (CAP_SYS_RESOURCE) 可改任意;非特权改 0..MSGMNB |
MSG_INFO / MSG_STAT |
Linux 扩展;MSG_INFO 返 maxind + msginfo 结构;MSG_STAT 按 entries 数组 index 查 id——自己实现 |
§46.6;定义 MSG_INFO 等常量需 |
client-server 模式 |
单 queue(type 1 给 server、自 PID 给 client)或 1 queue per client(大消息、避免「慢 client 阻塞」)——后者需 IPC_PRIVATE 创建 + 传 id 给 server |
§46.7-8; |
二、详细笔记
46.1 msgget 创建/打开
What:msgget(key, msgflg) 创建或打开消息队列。
Why:所有 SysV IPC 共同的「key → id」入口。
How:
// 摘自《The Linux Programming Interface》 第 46 章
#include <sys/msg.h>
int msgget(key_t key, int msgflg); /* 返回 msqid 或 -1 */
msgflg:
. 9 个低 bit = mode (文件 mode 同义),如 0660 = rw-rw----。
. IPC_CREAT:不存在则创建。
. IPC_EXCL:与 IPC_CREAT 一起用,*已存在*则失败 EEXIST——常用于「old server 检测」。
When:server 用 IPC_CREAT | mode;client 用 mode(不带 CREAT)。
46.2 msgsnd / msgrcv
What:消息 I/O 系统调用。消息结构 = struct { long mtype; char mtext[]; };mtext 是任意大小(但要 ≤ MSGMAX)。
Why:实现消息级 IPC——比字节流有「边界」。
How——结构与调用:
// 摘自《The Linux Programming Interface》 第 46 章
struct mbuf {
long mtype; /* 必须 > 0 */
char mtext[1024];
};
/* 发 */
struct mbuf msg = {1, "hello"};
msgsnd(msqid, &msg, strlen("hello") + 1, 0); /* 阻塞 */
/* 收 */
msgrcv(msqid, &msg, sizeof(msg.mtext), 0, 0); /* 取任意第一条,阻塞 */
printf("type=%ld body=%s\n", msg.mtype, msg.mtext);
msgrcv 的 msgtyp 选择:
-
msgtyp == 0→ 取第一条(FIFO)。 -
msgtyp > 0→ 取第一个 mtype 相等的。 -
msgtyp < 0→ 取 mtype ≤ |msgtyp| 中最低 mtype 的(优先级队列)。
flags:
. IPC_NOWAIT:无消息立即返 ENOMSG(非 EAGAIN;历史)。
. MSG_NOERROR:mtext 太长(> maxmsgsz)截断而非 E2BIG。
. MSG_EXCEPT(Linux):与 msgtyp>0 一起用,取 非 该 type 的——「除特定类型外」。
EINTR:msgsnd/msgrcv 被信号打断一定 EINTR(不自动重启,即使 SA_RESTART)。
When:常用于:单条短消息协议、控制流(PID 通知)、异步事件分发(mtype 区分事件类型)。
46.3 msgctl 控制
What:msgctl(msqid, cmd, buf) 提供 IPC_STAT/SET/RMID/INFO/STAT。
Why:删除 + 调参 + 统计。
How——常见 cmd:
/* 删除——msg/sem 立即;shm 标记 */
msgctl(msqid, IPC_RMID, NULL);
/* 取内核关联数据 */
struct msqid_ds ds;
msgctl(msqid, IPC_STAT, &ds);
/* 改 owner/permissions/msg_qbytes(4 个字段) */
ds.msg_perm.uid = newuid;
ds.msg_qbytes = new_max;
msgctl(msqid, IPC_SET, &ds);
/* Linux 扩展 */
int maxind = msgctl(0, MSG_INFO, (struct msqid_ds *)&msginfo); /* 拿 maxind */
int msqid = msgctl(ind, MSG_STAT, &ds); /* 按 index 拿 id */
When:server 退出时 IPC_RMID 清理;监控/管理工具用 INFO/STAT。
46.4 msqid_ds 关联数据
What:每 msqid 对应一个 struct msqid_ds,14 个字段(§46.4 Listing 46-4)。
Why:决定 IPC_STAT / IPC_SET 行为、决定 capacity(msg_qbytes)。
How——关键字段(来自 §46.4):
// 摘自《The Linux Programming Interface》 第 46 章
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.uid/msg_perm.gid/msg_perm.mode (低 9 bit) 可 IPC_SET;需要 owner/creator 或 CAP_SYS_ADMIN。
msg_qbytes 改值:
. CAP_SYS_RESOURCE:可设 0..INT_MAX。
. 非特权:可设 0..MSGMNB。
. /proc/sys/kernel/msgmnb 改 MSGMNB 全局上限。
When:调优队列容量(生产/消费不均);调大避免生产者阻塞。
46.5 消息队列限制
What:kernel 通过 /proc/sys/kernel/msg* 限制 SysV 消息队列。
Why:防资源耗尽。
How:
$ ipcs -l
$ cat /proc/sys/kernel/msgmni # 最大队列数
748
$ cat /proc/sys/kernel/msgmax # 单条最大字节
8192
$ cat /proc/sys/kernel/msgmnb # 单队列最大字节
16384
Linux 上限(§46.5 Table 46-1): . MSGMNI ≤ 32768 (IPCMNI) . MSGMAX:依赖可用内存 . MSGMNB ≤ 2147483647 (INT_MAX)
Linux 特有:MSGPOOL/MSGTQL(其他 UNIX 有)Linux 没有——0 长度消息也受 msg_qbytes 限制。
When:装高吞吐消息系统时调大 msgmnb。
46.6 显示所有消息队列
What:MSG_INFO + MSG_STAT 编程实现 ipcs -q。
Why:替代 ipcs 命令的程序化扫描。
How——svmsg_ls.c 模式(来自 §46.6):
// 摘自《The Linux Programming Interface》 第 46 章 Listing 46-6
#define _GNU_SOURCE
int maxind = msgctl(0, MSG_INFO, (struct msqid_ds *)&msginfo);
printf("maxind: %d\n", maxind);
for (int ind = 0; ind <= maxind; ind++) {
struct msqid_ds ds;
int msqid = msgctl(ind, MSG_STAT, &ds);
if (msqid == -1) {
if (errno != EINVAL && errno != EACCES) errMsg("...");
continue;
}
printf("%4d %8d 0x%08lx %7ld\n",
ind, msqid, (unsigned long)ds.msg_perm.__key, (long)ds.msg_qnum);
}
When:写 SysV IPC 监控工具;初始化时扫描现有对象(避免 key 撞车)。
46.7-46.8 client-server 模式与文件服务器
What:两种模式——单 queue(用 mtype 区分方向)/ 1 queue per client(用 mqd id 区分 server 给谁)。
Why:单 queue 简单但有「slow client 阻塞 / queue 满 / 抢消息」问题;1 queue per client 隔离但需要 MSGMNI 够。
How——1-per-client 模式(来自 §46.8 Listing 46-7/8/9 svmsg_file_server/client):
// 摘自《The Linux Programming Interface》 第 46 章 Listing 46-7/8
/* 客户端:创建自己的消息队列(用 IPC_PRIVATE) */
clientId = msgget(IPC_PRIVATE, S_IRUSR | S_IWUSR | S_IWGRP);
atexit(removeQueue); /* 退出时删 */
/* 构造请求:包含自己的 clientId + pathname */
req.mtype = 1; /* 给 server 看的 */
req.clientId = clientId;
strncpy(req.pathname, argv[1], sizeof(req.pathname));
msgsnd(serverId, &req, REQ_MSG_SIZE, 0);
/* 等 server 回复(用 mtype=clientId 选自己的) */
while ((msgLen = msgrcv(clientId, &resp, RESP_MSG_SIZE, 0, 0)) > 0) {
if (resp.mtype == RESP_MT_END) break;
if (resp.mtype == RESP_MT_DATA) write(STDOUT, resp.data, msgLen);
if (resp.mtype == RESP_MT_FAILURE) { fprintf(stderr, "%s", resp.data); break; }
}
Server(多并发):
// 摘自《The Linux Programming Interface》 第 46 章 Listing 46-8
/* 监听 serverId 的所有消息;fork 子进程处理每个请求 */
for (;;) {
msgLen = msgrcv(serverId, &req, REQ_MSG_SIZE, 0, 0);
if (msgLen == -1 && errno == EINTR) continue; /* SIGCHLD 中断 */
pid = fork();
if (pid == 0) {
serveRequest(&req); /* 读文件并发 RESP_MT_DATA 消息到 req.clientId */
_exit(EXIT_SUCCESS);
}
}
/* SIGCHLD handler 调用 waitpid 回收 zombie */
When:写一个服务-多客户端 IPC(带响应);选单 queue 还是 1-per-client 取决于消息量与可靠性需求。
46.9 SysV Message Queue 的限制
What:SysV 消息队列有显著设计缺陷——应优先考虑替代品。
Why:FIFO、POSIX mq、socket 都能做到大部分功能;新代码不建议用 SysV msg。
How——限制(来自 §46.9):
-
mtype 字段:是 32-bit(
long),SVID 未规定但实现都是如此;用它当 mqd_t 等替代品时小心 ABI 变化。 -
mtext 大小有限:默认 MSGMAX=8192;大于此分多消息协议。
-
无引用计数:server 决定何时 IPC_RMID;崩溃的 server 会留下 queue 孤儿。
-
rlimit 消息队列:MSGMNI 在容器等场景常被限制。
-
无文件描述符能力:不能用 select/poll/epoll 监控。
When:写新代码——优先 POSIX mq 或 Unix socket;维护老 SysV 代码——理解但不要扩大使用。
三、关键图表
|
非可视化条目(消息队列操作与 flags)
|
四、思维导图
mindmap
root((第 46 章 SysV 消息队列))
特征
消息边界
mtype 选择
int id 而非 fd
msgsnd msgrcv
mtype 大于 0
mtext 任意大小
IPC NOWAIT
MSG NOERROR
MSG EXCEPT
msgctl
IPC RMID 立即删
IPC STAT SET
MSG INFO STAT
msqid_ds
msg_perm
msg_qbytes 上限
msg_lspid lrpid
Limits
MSGMNI MSGMNB MSGMAX
proc sys kernel
client server
单 queue
1 queue per client
mtype = PID
设计缺陷
无引用计数
mtext 8192 上限
不能 select poll
五、重点与易错点
-
消息结构 mtype 必须 > 0——
msgsnd检查;=0 不会被任何msgtyp>0收到。 -
msgrcv mtext 长度限制——mtext > maxmsgsz 默认 E2BIG;要截断用 MSG_NOERROR。
-
msgrcv 的 EINTR 一定——不同于 read/write 的自动重启。
-
msgrcv 返 mtext 字节数——不是整个 struct 大小;可借此判断 mtext 是否空(0 = end-of-message)。
-
MSG_EXCEPT 与 msgtyp>0 配合——msgtyp=0 时 MSG_EXCEPT 无效;Linux 专属。
-
msqid_ds.msg_qbytes 默认 = MSGMNB——创建时复制;可 IPC_SET 改;特权 (CAP_SYS_RESOURCE) 才能改到 MSGMNB 以上。
-
1-per-client 模式需要 IPC_PRIVATE——clientId 是自己私有的;server 收到请求后用
msgsnd(req.clientId, …)定向回复。 -
MSG_INFO/MSG_STAT 是 Linux 扩展——SUSv3 没有;要
_GNU_SOURCE。 -
msgsnd 与 msg_qbytes 关系——如果当前字节数 + 新消息 > msg_qbytes 则阻塞(除非 NOWAIT)。
-
空消息 (mtext 长度 0) 也占容量——用于「事件通知」或「server alive ping」。
-
server 退出必须显式 IPC_RMID——否则孤儿队列一直存在直到 ipcrm 或系统重启。
-
0 长度消息用于「wakeup」——server 收到即可「事件发生」而不用关心内容。
-
MSG_STAT 按 index 查 id——index 即 entries[] 数组下标;id 计算 = index + seq*32768。
-
跨章衔接:第 44 章 管道/FIFO;第 47 章 SysV 信号量;第 48 章 SysV 共享内存;第 52 章 POSIX mq;第 56-61 章 socket。