系统编程(六)进程间通信-消息队列

1.消息队列理论


1.1 什么是“消息”

  • 消息(message):进程间传递的数据单位。

    • 可以很简单:一个字符串(例如 "hello")。

    • 也可以很复杂:包含多个字段(id、时间戳、状态码、二进制对象等)。

  • 语义:消息是“自包含”的数据包——发送者封装好数据,接收者按照约定解析即可。

  • 异步传递:发送方把消息放入队列后立即返回,不必等接收方处理(解耦、提高并发)。


1.2 什么是消息队列(抽象层)

  • 消息队列 = 内核/中间件维护的容器(链表/队列),用于暂存消息、做路由和调度。

  • 类比:快递柜

    • 生产者 = 快递员,把快递放进柜子(写入队列)。

    • 消费者 = 收件人,从柜子里取快递(读出消息)。

    • 如果收件人不在,快递(消息)会被保存在柜子里直到被取走(持久性到队列删除或系统重启)。

  • 核心价值:解耦 + 缓冲 + 可控路由(定向/优先)


1.3 消息队列的主要特性(要点)

  1. 消息有类型(mtype)

    • 每条消息带一个 mtype(System V 要求 long mtype 放在结构体首位)。

    • mtype 不是“队列成员/进程 ID”,而是“消息的标签/类别”,接收方可以按类型选择接收。

  2. 消息有格式

    • 通常用 struct 表示:long mtype; /*正文*/。正文可以包含若干字段或 union。

  3. 读取可以按类型选择(不是强制 FIFO)

    • 可按 mtype 精确取单一类型,也可取队首(msgtyp=0)。

    • 若选择某类型,队列内部该类型消息仍遵循该类型内的 FIFO。

  4. 支持多写多读(多对多)

    • 多个进程可以同时写入,也可以同时读取(并通过类型、权限、阻塞/非阻塞等控制)。

  5. 读出即删除

    • 成功 msgrcv() 后该条消息从队列中移除(不像日志那样保留)。

  6. 队列是内核资源(持久)

    • 存在于内核(不是进程内存),进程间共享。只有重启内核或显式 msgctl(IPC_RMID) 才会删除(否则一直存在)。

  7. 实现与限制相关(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

    • 失败:返回 -1errno 指出原因。

例子

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| 的消息。

    • msgflg0 阻塞,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 常见错误

  1. 结构体定义错误

    • long mtype 必须是第一个字段,否则 msgrcv 会出错。

  2. msgsz 传错

    • msgsnd/msgrcv 的第三个参数 只写正文大小,不包括 mtype

  3. 权限不够

    • 如果 msgget 权限设置太严格(如 0600),其他用户进程可能无法访问。

  4. 队列泄露

    • 如果程序退出前不删除队列,队列会一直存在。用 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 -qipcrm -q 是日常调试的利器。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值