Linux进程间通信之消息队列

消息队列 (Message Queue)

概述

消息队列是 System V IPC 机制之一, 允许进程通过消息的形式进行通信. 消息队列中的每条消息都有一个类型字段, 接收进程可以根据类型选择性地接收消息. 消息队列在系统内核中维护, 具有持久性 (直到被显式删除).

通信原理

基本概念

消息队列的特点:

  1. 消息结构: 每条消息包含类型和正文两部分
  2. 类型选择: 接收进程可以根据消息类型选择接收
  3. 持久性: 消息队列在系统中存在, 直到被显式删除
  4. 任意进程通信: 不要求进程间有亲缘关系
  5. 消息边界: 每条消息作为一个整体传输
  6. 消息消费: 消息一旦被某个进程接收就会从队列中删除, 其他进程无法再接收同一条消息 (一对一消费模式)

实现机制

  1. 创建/获取消息队列:

    • 使用 msgget() 系统调用创建或获取消息队列
    • 通过唯一的键值 (key) 标识消息队列
    • 返回消息队列标识符 (msqid)
  2. 消息结构:

    c 复制代码
    struct msgbuf {
        long mtype;      // 消息类型 (必须>0)
        char mtext[1];   // 消息正文 (可变长度)
    };
  3. 发送消息:

    • 使用 msgsnd() 将消息发送到队列
    • 消息按照 FIFO 顺序排队
    • 可以指定阻塞或非阻塞模式
  4. 接收消息:

    • 使用 msgrcv() 从队列接收消息
    • 可以根据消息类型选择接收
    • 可以指定阻塞或非阻塞模式
    • 重要: 消息一旦被接收就会从队列中删除, 无法被多个进程同时接收
  5. 数据流向:

    复制代码
    进程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_CREAT
    • ENOMEM: 内存不足, 无法创建队列
    • 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_NOWAIT
    • EFAULT: 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_NOERROR
    • EACCES: 权限不足, 无法读取消息队列
    • EAGAIN: 队列中无匹配消息且指定了 IPC_NOWAIT
    • EFAULT: 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

这样就可以完成简单的进程间消息收发。

性能评价

优点

  1. 消息边界: 每条消息作为整体传输, 保持消息边界
  2. 类型选择: 可以根据消息类型选择性接收
  3. 持久性: 消息队列在系统中持久存在
  4. 任意进程通信: 不要求进程间有亲缘关系
  5. 阻塞/非阻塞: 支持阻塞和非阻塞模式

缺点

  1. 系统限制 : 系统对消息队列数量和单条消息大小有限制,例如单个系统允许的消息队列总数(msgmni)、每个队列的最大字节数(msgmnb)、单条消息的最大字节数(msgmax)等。可以通过 ipcs -lcat /proc/sys/kernel/msg* 查看相关限制。要修改这些参数,可以使用 sysctl 命令(如 sysctl -w kernel.msgmax=8192),或临时修改 /proc/sys/kernel/ 下对应参数(如 echo 8192 > /proc/sys/kernel/msgmax)。需要管理员权限。
  2. 性能开销: 需要系统调用, 有一定开销
  3. 复杂性: API 相对复杂, 需要管理键值
  4. 系统资源: 占用系统资源, 需要显式删除

性能特点

  • 延迟: 中等 (需要系统调用)
  • 吞吐量: 中等 (受系统限制影响)
  • CPU 占用: 中等
  • 内存占用: 中等 (内核维护队列)

适用场景

  • ✅ 需要消息边界的通信
  • ✅ 需要根据类型选择消息的场景
  • ✅ 多对多进程通信
  • ✅ 需要持久化消息的场景
  • ✅ 1 对多通信 (通过发送多条不同 mtype 的消息实现)
  • ❌ 对性能要求极高的场景
  • ❌ 简单的单向通信 (管道更合适)
  • ❌ 单条消息广播 (一条消息被多个进程同时接收)

注意事项

  1. 键值管理 : 使用 ftok() 生成键值或使用 IPC_PRIVATE
  2. 消息大小限制: 注意系统对消息大小的限制
  3. 资源清理 : 使用完毕后应该删除消息队列 (msgctl(IPC_RMID))
  4. 权限设置: 注意设置合适的权限, 避免权限问题
  5. 错误处理: 注意处理队列满、队列空等错误情况
  6. 系统限制 : 注意系统的消息队列数量限制 (ipcs -l 查看)
  7. 消息消费模式: 消息一旦被接收就会从队列中删除, 无法实现多进程同时接收同一条消息; 如果需要广播或多播功能, 应考虑其他 IPC 机制 (如共享内存 + 信号量)

扩展阅读

  • man 2 msgget
  • man 2 msgsnd
  • man 2 msgrcv
  • man 2 msgctl
  • man 7 mq_overview
相关推荐
Starry_hello world2 小时前
Linux 信号
linux
jerryinwuhan2 小时前
1210_linux_2
linux·运维·服务器
Two_brushes.2 小时前
字符串<--->网络字节序<--->主机
网络
IDC02_FEIYA2 小时前
Linux怎么看服务器状态?Linux查看服务器状态命令
linux·运维·服务器
爱潜水的小L2 小时前
自学嵌入式day28,文件操作
linux·数据结构·算法
刚入坑的新人编程2 小时前
Linux(小项目)进度条演示
linux·运维·服务器
IT运维爱好者2 小时前
【Linux】Python3 环境的下载与安装
linux·python·centos7
老蒋新思维2 小时前
范式重构:从场景锚点到价值闭环——AI智能体落地知识产业的非技术视角|创客匠人
网络·人工智能·网络协议·tcp/ip·数据挖掘·创始人ip·创客匠人
Apibro2 小时前
【LINUX】时区修改
linux·运维·服务器