Linux进程间通信之消息队列(POSIX)

POSIX 消息队列 (Message Queue)

概述

POSIX 消息队列是 POSIX.1b 标准定义的进程间通信机制, 允许进程通过消息的形式进行通信. POSIX 消息队列中的每条消息都有一个优先级字段, 接收进程可以根据优先级选择性地接收消息. 消息队列在系统内核中维护, 具有持久性(直到被显式删除), 在 /dev/mqueue 文件系统中可见.

通信原理

基本概念

POSIX 消息队列的特点:

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

实现机制

  1. 创建/打开消息队列:

    • 使用 mq_open() 系统调用创建或打开消息队列
    • 通过名字(如 /my_mq)标识消息队列
    • 返回消息队列描述符(mqd_t)
  2. 消息结构:

    • POSIX 消息队列不要求特定的消息结构
    • 消息正文是任意字节序列
    • 每条消息有一个优先级(unsigned int)
  3. 发送消息:

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

    • 使用 mq_receive() 从队列接收消息
    • 总是接收优先级最高的消息
    • 可以指定阻塞或非阻塞模式
    • 重要: 消息一旦被接收就会从队列中删除, 无法被多个进程同时接收
  5. 数据流向:

    复制代码
    进程A  -->  [消息队列]  -->  进程B
    进程C  -->              -->  进程C

消息优先级

POSIX 消息队列使用优先级而非类型来区分消息:

  • 优先级范围 : 0sysconf(_SC_MQ_PRIO_MAX) - 1
  • 优先级规则: 数值越大, 优先级越高
  • 接收规则: 总是接收优先级最高的消息, 同优先级按 FIFO 顺序

与 System V 消息队列的区别:

  • System V: 使用 mtype(long) 作为消息类型, 可以按类型选择接收
  • POSIX: 使用优先级(unsigned int), 总是接收最高优先级的消息

使用优先级实现 1 对多通信

通过优先级可以实现某种形式的 1 对多通信, 但需要注意以下限制和方案:

可行方案

方案 1: 发送多条消息(推荐)

  • 发送方为每个接收方发送一条消息, 每条消息使用不同的优先级
  • 每个接收方通过优先级范围选择接收
  • 限制: 需要发送多条消息, 占用更多队列空间

示例:

c 复制代码
// 发送方: 向 3 个接收方发送相同内容
mq_send(mqdes, msg1, size, 1);  // 优先级 1, 接收方 A
mq_send(mqdes, msg2, size, 2);  // 优先级 2, 接收方 B
mq_send(mqdes, msg3, size, 3);  // 优先级 3, 接收方 C

// 接收方 A: 接收优先级 1 的消息
// 注意: POSIX 消息队列总是接收最高优先级消息, 无法直接按优先级过滤
// 需要接收后检查优先级, 不匹配则重新发送或使用其他机制

方案 2: 使用共享内存 + 信号量

  • 如果需要真正的广播功能, 考虑使用共享内存 + 信号量
  • 共享内存可以同时被多个进程读取
不可行方案

单条消息广播:

  • 发送方只发送一条消息, 期望多个接收方都能收到
  • 不可行: 消息一旦被第一个接收方接收就会从队列中删除, 其他接收方无法再接收
总结
  • 可行: 通过发送多条消息(不同优先级)实现 1 对多
  • 不可行: 单条消息被多个进程同时接收(广播)
  • 💡 建议: 如果需要真正的广播功能, 考虑使用共享内存 + 信号量

API 说明

mq_open()

c 复制代码
#include <mqueue.h>

mqd_t mq_open(const char *name, int oflag, mode_t mode, struct mq_attr *attr);
  • 功能: 创建或打开消息队列
  • 参数 :
    • name: 消息队列的名字, 必须以 / 开头(如 /my_mq)
    • oflag: 标志位, 可以是以下值的组合:
      • O_CREAT: 如果队列不存在则创建, 存在则打开
      • O_EXCL: 与 O_CREAT 一起使用, 如果队列已存在则返回错误
      • O_RDONLY: 只读模式
      • O_WRONLY: 只写模式
      • O_RDWR: 读写模式
      • O_NONBLOCK: 非阻塞模式
    • mode: 权限位(类似文件权限, 如 0666 表示所有用户可读写)
    • attr: 指向 mq_attr 结构的指针, 用于设置队列属性(创建时), 可以为 NULL 使用默认值
  • 返回值 : 成功返回消息队列描述符, 失败返回 (mqd_t)-1
  • 错误码 :
    • EACCES: 权限不足, 无法访问消息队列
    • EEXIST: 使用 O_CREAT | O_EXCL 时, 队列已存在
    • EINVAL: 参数无效(名字格式错误、属性无效等)
    • EMFILE: 进程打开的消息队列数量已达上限
    • ENAMETOOLONG: 名字太长
    • ENOENT: 队列不存在且未指定 O_CREAT
    • ENOSPC: 系统消息队列数量已达上限

mq_attr 结构:

c 复制代码
struct mq_attr {
    long mq_flags;    // 标志位(如 O_NONBLOCK)
    long mq_maxmsg;   // 队列中最大消息数
    long mq_msgsize;  // 单条消息最大大小
    long mq_curmsgs;  // 当前队列中的消息数(只读)
};

mq_send()

c 复制代码
#include <mqueue.h>

int mq_send(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio);
  • 功能: 向消息队列发送消息
  • 参数 :
    • mqdes: 消息队列描述符
    • msg_ptr: 指向消息正文的指针
    • msg_len: 消息正文的长度
    • msg_prio: 消息优先级(0 到 sysconf(_SC_MQ_PRIO_MAX) - 1)
  • 返回值: 成功返回 0, 失败返回 -1
  • 错误码 :
    • EAGAIN: 队列满且指定了 O_NONBLOCK
    • EBADF: 消息队列描述符无效
    • EINTR: 被信号中断
    • EINVAL: 参数无效(msg_len 超过 mq_msgsizemsg_prio 无效等)
    • EMSGSIZE: 消息大小超过队列限制

mq_receive()

c 复制代码
#include <mqueue.h>

ssize_t mq_receive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio);
  • 功能: 从消息队列接收消息
  • 参数 :
    • mqdes: 消息队列描述符
    • msg_ptr: 指向消息缓冲区的指针
    • msg_len: 缓冲区大小(必须 >= mq_msgsize)
    • msg_prio: 指向 unsigned int 的指针, 用于返回消息优先级(可以为 NULL)
  • 返回值: 成功返回接收的字节数, 失败返回 -1
  • 重要说明 :
    • 消息删除 : mq_receive() 成功接收消息后, 该消息会从队列中永久删除, 其他进程无法再接收同一条消息
    • 多端接收: 多个进程无法同时接收同一条消息, 消息一旦被某个进程接收就会从队列中移除
    • 优先级规则: 总是接收优先级最高的消息, 同优先级按 FIFO 顺序
  • 错误码 :
    • EAGAIN: 队列中无消息且指定了 O_NONBLOCK
    • EBADF: 消息队列描述符无效
    • EINTR: 被信号中断
    • EINVAL: 参数无效(msg_len < mq_msgsize 等)
    • EMSGSIZE: 缓冲区大小小于消息大小

mq_close()

c 复制代码
#include <mqueue.h>

int mq_close(mqd_t mqdes);
  • 功能: 关闭消息队列描述符
  • 参数 : mqdes - 消息队列描述符
  • 返回值: 成功返回 0, 失败返回 -1
  • 注意 : 关闭描述符不会删除队列, 需要单独调用 mq_unlink() 删除

mq_unlink()

c 复制代码
#include <mqueue.h>

int mq_unlink(const char *name);
  • 功能: 删除消息队列
  • 参数 : name - 消息队列的名字
  • 返回值: 成功返回 0, 失败返回 -1
  • 注意: 删除后, 已打开的进程仍可使用, 但新进程无法再打开该队列

mq_getattr() / mq_setattr()

c 复制代码
#include <mqueue.h>

int mq_getattr(mqd_t mqdes, struct mq_attr *attr);
int mq_setattr(mqd_t mqdes, const struct mq_attr *newattr, struct mq_attr *oldattr);
  • 功能: 获取或设置消息队列属性
  • 参数 :
    • mqdes: 消息队列描述符
    • attr / newattr / oldattr: 指向 mq_attr 结构的指针
  • 返回值: 成功返回 0, 失败返回 -1
  • 注意 : mq_setattr() 只能修改 mq_flags(如设置/清除 O_NONBLOCK), 不能修改 mq_maxmsgmq_msgsize

示例代码

下面给出一个基于 POSIX 消息队列的简单收发消息示例:

1. 发送方示例(sender.c):

c 复制代码
#include <fcntl.h>
#include <mqueue.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define QUEUE_NAME "/my_mq"
#define MAX_MSG_SIZE 128
#define MSG_PRIORITY 0

int main(int argc, char *argv[]) {
    const char *text = (argc > 1) ? argv[1] : "Hello, POSIX message queue!";
    mqd_t mq;
    struct mq_attr attr;

    // 设置消息队列属性
    attr.mq_flags = 0;           // 阻塞模式
    attr.mq_maxmsg = 10;         // 最大消息数
    attr.mq_msgsize = MAX_MSG_SIZE;  // 最大消息大小
    attr.mq_curmsgs = 0;         // 当前消息数

    // 创建或打开消息队列
    mq = mq_open(QUEUE_NAME, O_CREAT | O_WRONLY, 0666, &attr);
    if (mq == (mqd_t)-1) {
        perror("mq_open");
        return 1;
    }

    // 发送消息
    if (mq_send(mq, text, strlen(text) + 1, MSG_PRIORITY) == -1) {
        perror("mq_send");
        mq_close(mq);
        return 1;
    }

    printf("消息已发送: %s\n", text);

    // 关闭消息队列
    mq_close(mq);

    return 0;
}

2. 接收方示例(receiver.c):

c 复制代码
#include <fcntl.h>
#include <mqueue.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define QUEUE_NAME "/my_mq"
#define MAX_MSG_SIZE 128

int main() {
    mqd_t mq;
    char buffer[MAX_MSG_SIZE];
    unsigned int priority;
    ssize_t bytes_read;

    // 打开消息队列(只读)
    mq = mq_open(QUEUE_NAME, O_RDONLY);
    if (mq == (mqd_t)-1) {
        perror("mq_open");
        printf("消息队列不存在, 请先运行 sender 程序\n");
        return 1;
    }

    // 接收消息
    bytes_read = mq_receive(mq, buffer, MAX_MSG_SIZE, &priority);
    if (bytes_read == -1) {
        perror("mq_receive");
        mq_close(mq);
        return 1;
    }

    printf("收到消息: %s (优先级: %u, 大小: %zd 字节)\n", 
           buffer, priority, bytes_read);

    // 关闭消息队列
    mq_close(mq);

    // 删除消息队列
    if (mq_unlink(QUEUE_NAME) == -1) {
        perror("mq_unlink");
        return 1;
    }

    printf("消息队列已删除\n");

    return 0;
}

3. 编译与运行

sh 复制代码
gcc sender.c -o sender -lrt
gcc receiver.c -o receiver -lrt
# 首开终端 A, 先运行接收方(阻塞等待消息)
./receiver
# 新开终端 B, 运行发送方
./sender

性能评价

优点

  1. 消息边界: 每条消息作为整体传输, 保持消息边界
  2. 优先级支持: 支持消息优先级, 高优先级消息先被接收
  3. 持久性: 消息队列在系统中持久存在
  4. 任意进程通信: 不要求进程间有亲缘关系
  5. 阻塞/非阻塞: 支持阻塞和非阻塞模式
  6. 文件系统可见 : 在 /dev/mqueue 中可见, 便于管理
  7. 跨平台: POSIX 标准, 跨平台兼容性好
  8. 名字直观: 使用名字标识, 比键值更直观

缺点

  1. 系统限制 : 系统对消息队列数量和单条消息大小有限制, 可以通过 cat /proc/sys/fs/mqueue/* 查看相关限制
  2. 性能开销: 需要系统调用, 有一定开销
  3. 复杂性: API 相对复杂, 需要管理名字
  4. 系统资源: 占用系统资源, 需要显式删除
  5. 链接库 : 某些系统需要链接 librt
  6. 优先级限制: 无法像 System V 那样按类型选择接收, 只能按优先级

性能特点

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

适用场景

  • ✅ 需要消息边界的通信
  • ✅ 需要消息优先级的场景
  • ✅ 多对多进程通信
  • ✅ 需要持久化消息的场景
  • ✅ 需要跨平台移植的场景
  • ❌ 对性能要求极高的场景
  • ❌ 简单的单向通信(管道更合适)
  • ❌ 单条消息广播(一条消息被多个进程同时接收)
  • ❌ 需要按类型选择接收的场景(System V 更适合)

注意事项

  1. 名字管理: 使用有意义的名字, 避免名字冲突
  2. 消息大小限制: 注意系统对消息大小的限制
  3. 资源清理 : 使用完毕后应该删除消息队列(mq_unlink())
  4. 权限设置: 注意设置合适的权限, 避免权限问题
  5. 错误处理: 注意处理队列满、队列空等错误情况
  6. 系统限制: 注意系统的消息队列数量限制
  7. 消息消费模式: 消息一旦被接收就会从队列中删除, 无法实现多进程同时接收同一条消息; 如果需要广播或多播功能, 应考虑其他 IPC 机制(如共享内存 + 信号量)
  8. 文件系统挂载 : 确保 /dev/mqueue 已挂载
  9. 链接库 : 某些系统需要链接 librt 库(-lrt)
  10. 名字格式 : 名字必须以 / 开头
  11. 优先级范围 : 注意优先级范围限制(sysconf(_SC_MQ_PRIO_MAX))

与 System V 消息队列的区别

特性 System V POSIX
标识方式 键值(key) 名字(name)
消息类型 mtype (long) 优先级(unsigned int)
接收规则 可按类型选择接收 总是接收最高优先级
API 设计 较老 较现代
文件系统可见 是(/dev/mqueue)
删除方式 msgctl(IPC_RMID) mq_unlink()
管理工具 ipcs/ipcrm ls/rm (文件系统)
跨平台 较差 较好(POSIX 标准)

扩展阅读

  • man 2 mq_open
  • man 2 mq_send
  • man 2 mq_receive
  • man 2 mq_close
  • man 2 mq_unlink
  • man 7 mq_overview
相关推荐
laoliu19962 小时前
Odoo 18企业版源码 包含 部署教程
运维·服务器
唐墨1232 小时前
linux kernel源码解析之:smp系统cpu热插拔
linux
开开心心就好2 小时前
免费卸载工具,可清理残留批量管理启动项
linux·运维·服务器·windows·随机森林·pdf·1024程序员节
智算菩萨3 小时前
摩擦电纳米发电机近期进展的理论脉络梳理:从接触起电到统一建模与能量转换
linux·人工智能·算法
Lbwnb丶3 小时前
检测服务器是否是虚拟化,如KVM,VM等
linux·运维·服务器
老猿讲编程3 小时前
【车载信息安全系列4】基于Linux中UIO的HSE应用实现
linux·运维·服务器
wanhengidc3 小时前
巨椰 云手机 云游戏稳定运行
运维·服务器·arm开发·游戏·云计算
不染尘.4 小时前
UDP客户服务器模型和UDP协议
服务器·网络·网络协议·计算机网络·udp
太行山有西瓜汁4 小时前
达梦DTS工具:批量导出与导入DDL脚本完整指南
运维·服务器·数据库