第 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 改)。

      • LimitsMSGMNI(队列数)、MSGMAX(单条字节数)、MSGMNB(队列总字节);/proc/sys/kernel/msgmnimsgmaxmsgmnb 调整。

      • 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 协议

      struct { long mtype; char mtext[]; };mtype > 0;msgsnd 阻塞写满、msgrcv 阻塞等消息;mtext 长度 ≤ MSGMAX

      §46.2;IPC_NOWAIT 不阻塞;MSG_NOERROR 截断过长 mtext;MSG_EXCEPT (Linux) 取 ≠msgtyp 的消息

      按 mtype 选消息

      msgtyp=0 → 第一条;msgtyp>0 → 第一个 mtype 相等的;msgtyp<0 → 优先级取 ≤|msgtyp| 中最低 mtype 的

      §46.2.2;client 常用「msgtyp = own PID」让 server 定向回复

      msqid_ds 关联数据

      msg_perm (uid/gid/mode)、时间戳、__msg_cbytes (当前字节)、msg_qnum (消息数)、msg_qbytes (容量上限)、msg_lspid/msg_lrpid (最后 send/recv PID)

      §46.4;msg_qbytes 默认 = MSGMNB;特权 (CAP_SYS_RESOURCE) 可改任意;非特权改 0..MSGMNB

      MSG_INFO / MSG_STAT

      Linux 扩展;MSG_INFO 返 maxind + msginfo 结构;MSG_STAT 按 entries 数组 index 查 id——自己实现 ipcs -q 的关键

      §46.6;定义 MSG_INFO 等常量需 _GNU_SOURCEsvmsg_ls.c 是标准实现

      client-server 模式

      单 queue(type 1 给 server、自 PID 给 client)或 1 queue per client(大消息、避免「慢 client 阻塞」)——后者需 IPC_PRIVATE 创建 + 传 id 给 server

      §46.7-8;svmsg_file_server.c + svmsg_file_client.c 是典型 1-per-client 模式

      二、详细笔记

      46.1 msgget 创建/打开

      Whatmsgget(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 选择

      1. msgtyp == 0 → 取第一条(FIFO)。

      2. msgtyp > 0 → 取第一个 mtype 相等的。

      3. 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 控制

      Whatmsgctl(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 显示所有消息队列

      WhatMSG_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):

      1. mtype 字段:是 32-bit(long),SVID 未规定但实现都是如此;用它当 mqd_t 等替代品时小心 ABI 变化。

      2. mtext 大小有限:默认 MSGMAX=8192;大于此分多消息协议。

      3. 无引用计数:server 决定何时 IPC_RMID;崩溃的 server 会留下 queue 孤儿。

      4. rlimit 消息队列:MSGMNI 在容器等场景常被限制。

      5. 无文件描述符能力:不能用 select/poll/epoll 监控。

      When:写新代码——优先 POSIX mq 或 Unix socket;维护老 SysV 代码——理解但不要扩大使用。

      三、关键图表

      非可视化条目(消息队列操作与 flags)
      描述

      msgget(key, mode[, IPC_CREAT][, IPC_EXCL])

      创建/打开;返 msqid

      struct { long mtype; char mtext[]; }

      消息结构;mtype > 0

      msgsnd(id, &msg, len, 0)

      阻塞写;满则阻塞

      msgsnd(…​, IPC_NOWAIT)

      满则 EAGAIN

      msgrcv(id, &msg, max, msgtyp, 0)

      阻塞读;返 mtext 字节数

      msgrcv(…​, IPC_NOWAIT)

      无消息返 ENOMSG

      msgrcv(…​, MSG_NOERROR)

      mtext 太长截断

      msgrcv(…​, MSG_EXCEPT)

      Linux 扩展:取非 msgtyp 的

      msgtyp=0 / >0 / <0

      FIFO / 精确 / 优先级

      msgctl(id, IPC_RMID, NULL)

      立即删除队列

      msgctl(id, IPC_STAT, &ds)

      读 msqid_ds

      msgctl(id, IPC_SET, &ds)

      改 uid/gid/mode/msg_qbytes

      msgctl(0, MSG_INFO, …​)

      Linux 扩展:拿 maxind

      msgctl(ind, MSG_STAT, …​)

      Linux 扩展:按 index 拿 id

      __msg_cbytes / msg_qnum / msg_qbytes

      当前字节/消息数/上限

      MSGMAX (默认 8192)

      单条 mtext 最大字节

      MSGMNI (默认 ~1K)

      系统级最大队列数

      MSGMNB (默认 16K)

      单队列最大字节

      四、思维导图

      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

      五、重点与易错点

      1. 消息结构 mtype 必须 > 0——msgsnd 检查;=0 不会被任何 msgtyp>0 收到。

      2. msgrcv mtext 长度限制——mtext > maxmsgsz 默认 E2BIG;要截断用 MSG_NOERROR。

      3. msgrcv 的 EINTR 一定——不同于 read/write 的自动重启。

      4. msgrcv 返 mtext 字节数——不是整个 struct 大小;可借此判断 mtext 是否空(0 = end-of-message)。

      5. MSG_EXCEPT 与 msgtyp>0 配合——msgtyp=0 时 MSG_EXCEPT 无效;Linux 专属。

      6. msqid_ds.msg_qbytes 默认 = MSGMNB——创建时复制;可 IPC_SET 改;特权 (CAP_SYS_RESOURCE) 才能改到 MSGMNB 以上。

      7. 1-per-client 模式需要 IPC_PRIVATE——clientId 是自己私有的;server 收到请求后用 msgsnd(req.clientId, …​) 定向回复。

      8. MSG_INFO/MSG_STAT 是 Linux 扩展——SUSv3 没有;要 _GNU_SOURCE

      9. msgsnd 与 msg_qbytes 关系——如果当前字节数 + 新消息 > msg_qbytes 则阻塞(除非 NOWAIT)。

      10. 空消息 (mtext 长度 0) 也占容量——用于「事件通知」或「server alive ping」。

      11. server 退出必须显式 IPC_RMID——否则孤儿队列一直存在直到 ipcrm 或系统重启。

      12. 0 长度消息用于「wakeup」——server 收到即可「事件发生」而不用关心内容。

      13. MSG_STAT 按 index 查 id——index 即 entries[] 数组下标;id 计算 = index + seq*32768。

      14. 跨章衔接:第 44 章 管道/FIFO;第 47 章 SysV 信号量;第 48 章 SysV 共享内存;第 52 章 POSIX mq;第 56-61 章 socket。