消息队列:进程间通信的“邮局”

消息队列,说白了就是内核帮我们维护的一个消息链表。每个消息都有自己的类型,进程可以往队列里扔消息,也可以按类型取消息。这就像邮局——你寄信时写上地址(消息类型),收信人按地址取信。

我在做嵌入式网关项目时,多个模块需要交换配置数据。共享内存虽然快,但同步问题让人头疼;管道又太死板,只能单向流。最后选了消息队列,嗯,恰到好处。

核心API:四个函数搞定一切

System V消息队列就四个函数:msggetmsgsndmsgrcvmsgctl。我习惯把它们记成“创建-发送-接收-控制”四步曲。

msgget — 创建或获取队列

#include <sys/msg.h>

int msgget(key_t key, int msgflg);
// 返回:成功返回队列ID,失败返回-1

key是队列的“身份证号”,不同进程用同一个key就能找到同一个队列。msgflgIPC_CREAT | 0666表示创建,权限0666表示所有人可读写。

避坑指南:我曾经在项目里忘了检查msgget的返回值,结果队列没创建成功,后面msgsnd一直报错。排查了半天才发现是key冲突了。记住:msgget失败时errno会告诉你原因——EACCES是权限不够,EEXIST是队列已存在且你没设IPC_CREAT

msgsnd — 发送消息

int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
// 返回:成功返回0,失败返回-1

消息结构必须这样定义:

struct msgbuf {
    long mtype;       // 消息类型,必须 > 0
    char mtext[1];    // 消息数据,大小自定
};

注意mtype必须是正数。0是非法类型,负值也不行。我见过有人传0进去,结果msgsnd直接返回EINVAL

msgflgIPC_NOWAIT表示非阻塞——队列满了就立刻返回EAGAIN。不传这个标志,进程会阻塞直到有空间。

msgrcv — 接收消息

ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, 
               long msgtyp, int msgflg);
// 返回:成功返回接收到的字节数,失败返回-1

msgtyp的取值很有意思:

msgtyp行为
0取队列中第一条消息(先进先出)
>0取类型等于msgtyp的第一条消息
<0取类型小于等于|msgtyp|的最小类型消息

我个人最常用的是msgtyp > 0,按类型取消息。比如类型1是控制指令,类型2是数据包,接收方各取所需。

注意msgsz是消息数据部分的长度,不包括mtype。如果实际消息比msgsz长,且msgflg没设MSG_NOERRORmsgrcv会返回E2BIG并截断消息。我曾经因为这个bug导致数据丢失,排查了一下午。

msgctl — 控制队列

int msgctl(int msqid, int cmd, struct msqid_ds *buf);
// 返回:成功返回0,失败返回-1

三个常用命令:

  • IPC_STAT:获取队列状态信息
  • IPC_SET:修改队列属性(比如权限)
  • IPC_RMID:删除队列

删除队列时,所有阻塞在msgsnd/msgrcv上的进程会立刻被唤醒,并返回EIDRM。这个特性我在热升级模块里用过——先删队列,所有等待线程收到错误后优雅退出。

消息队列的优缺点

用了这么多年,我总结一下:

优点缺点
支持多对多通信,灵活消息有大小限制(通常8KB)
按类型接收,天然支持优先级System V接口偏底层,使用稍复杂
内核自动管理同步,无需额外锁队列总数有限(cat /proc/sys/kernel/msgmni可查)
消息持久化在内核,进程退出不丢失POSIX消息队列更现代,但System V更通用

说白了,消息队列适合“异步、松耦合”的场景。比如A模块发配置,B模块慢慢处理,两者不需要同时运行。但如果你追求极致性能,共享内存+信号量可能更合适。

完整示例:一个简单的消息收发

下面是我写的一个demo,发送方发两种类型的消息,接收方按类型取:

// msg_send.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <errno.h>

#define MSG_KEY 1234

struct msgbuf {
    long mtype;
    char mtext[256];
};

int main() {
    int msqid = msgget(MSG_KEY, IPC_CREAT | 0666);
    if (msqid == -1) {
        perror("msgget");
        exit(1);
    }

    struct msgbuf msg;
    
    // 发送类型1的消息
    msg.mtype = 1;
    strcpy(msg.mtext, "Hello, this is type 1");
    if (msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0) == -1) {
        perror("msgsnd type 1");
        exit(1);
    }
    printf("Sent type 1: %s\n", msg.mtext);

    // 发送类型2的消息
    msg.mtype = 2;
    strcpy(msg.mtext, "Hi, this is type 2");
    if (msgsnd(msqid, &msg, strlen(msg.mtext) + 1, 0) == -1) {
        perror("msgsnd type 2");
        exit(1);
    }
    printf("Sent type 2: %s\n", msg.mtext);

    return 0;
}
// msg_recv.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <errno.h>

#define MSG_KEY 1234

struct msgbuf {
    long mtype;
    char mtext[256];
};

int main() {
    int msqid = msgget(MSG_KEY, IPC_CREAT | 0666);
    if (msqid == -1) {
        perror("msgget");
        exit(1);
    }

    struct msgbuf msg;
    
    // 只接收类型1的消息
    if (msgrcv(msqid, &msg, sizeof(msg.mtext), 1, 0) == -1) {
        perror("msgrcv type 1");
        exit(1);
    }
    printf("Received type 1: %s\n", msg.mtext);

    // 再接收类型2的消息
    if (msgrcv(msqid, &msg, sizeof(msg.mtext), 2, 0) == -1) {
        perror("msgrcv type 2");
        exit(1);
    }
    printf("Received type 2: %s\n", msg.mtext);

    // 清理队列
    if (msgctl(msqid, IPC_RMID, NULL) == -1) {
        perror("msgctl");
        exit(1);
    }

    return 0;
}

先跑发送方,再跑接收方。你会看到接收方只取到了类型匹配的消息。这就是消息队列的“按类型过滤”能力。

核心要点:消息队列是内核级的异步通信机制。它不像管道那样只能单向,也不像共享内存那样需要同步。你只需要定义好消息类型,发送方和接收方各司其职。但要注意——消息队列资源有限,用完记得msgctl(IPC_RMID)清理,否则系统会慢慢耗尽队列。

知识体系图

下面这张图帮你理清消息队列的核心脉络:

System V 消息队列核心知识体系 msgget 创建/获取队列 msgsnd 发送消息 msgrcv 接收消息 msgctl 控制/删除队列 消息结构 struct msgbuf long mtype(类型,必须 > 0) char mtext[N](数据,大小自定) 优点 • 多对多通信,灵活 • 按类型接收,天然优先级 • 内核自动同步,无需额外锁 缺点 • 消息大小有限(通常8KB) • 接口偏底层,使用稍复杂 • 队列总数有限,需注意清理

这张图把四个API、消息结构、优缺点串在了一起。你想想看,msgget是入口,msgsndmsgrcv是核心操作,msgctl负责收尾。消息结构里的mtype是关键——它决定了消息的“收件地址”。

我的习惯:在实际项目中,我会把消息类型定义成枚举,比如MSG_CMDMSG_DATAMSG_ACK。这样代码可读性高,也不容易写错类型值。另外,我建议在msgrcv时用IPC_NOWAIT配合轮询,或者用信号驱动I/O,避免进程长时间阻塞。

消息队列虽然古老,但在很多场景下依然好用。尤其是当你需要“异步、按类型、多对多”通信时,它比管道和共享内存都顺手。嗯,今天就聊到这儿,下一节我们看看POSIX消息队列和System V的区别。


公众号:蓝海资料掘金营,微信deep3321