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

消息队列,说白了就是内核帮我们维护的一个消息链表。每个消息都是一个数据块,有类型、有长度、有内容。进程可以往队列里扔消息,也可以从队列里取消息——就像去邮局寄信、收信一样。

我刚开始学进程间通信时,总觉得管道就够用了。直到有一次做嵌入式项目,需要在多个进程之间传递结构化的数据包,管道那串字节流简直让人抓狂。后来换成消息队列,整个世界都清净了。

消息队列的核心概念

消息队列有几个关键点,我建议你记牢:

  • 消息有类型:每个消息可以带一个长整型值作为类型标识。接收方可以按类型取消息,不用按先进先出的顺序。
  • 消息有边界:每条消息都是独立的数据单元,不会像管道那样粘在一起。
  • 消息队列有生命周期:除非你显式删除,否则消息队列会一直存在内核里。哪怕创建它的进程退出了,队列依然在。

消息队列 vs 管道:一张表说清楚

对比项 消息队列 管道
数据格式 有类型、有边界的消息 无格式的字节流
通信方向 双向(任意进程可发可收) 单向(半双工)
生命周期 随内核,直到被删除 随进程,关闭即销毁
消息优先级 支持按类型选择接收 不支持,严格FIFO
适用场景 结构化数据、多对多通信 简单字节流、父子进程

你想想看,管道就像一根水管,水(数据)流过去就混在一起了。消息队列呢,就像一个个贴着标签的快递盒,想拿哪个就拿哪个。这就是本质区别。

消息队列的键值与标识符

这里有个容易搞混的概念——键值(key)和标识符(id)。

键值(key_t):是一个整数,用来在多个进程间约定同一个消息队列。就像一把钥匙,大家用同一把钥匙才能打开同一个邮箱。

标识符(msgid):是msgget()返回的整数,在内核内部唯一标识一个消息队列。你可以把它理解成文件描述符。

我见过不少新手直接写死一个数字当key,结果跟别的程序冲突了。我个人习惯用ftok()函数生成key,这样能保证唯一性。

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

key_t key = ftok("/tmp/msgqueue", 'A');
// 第一个参数是一个存在的文件路径
// 第二个参数是一个字符,用来区分不同的队列

二、消息队列的创建与控制

msgget():创建或获取消息队列

这个函数很简单,但参数里的权限标志位容易写错。我直接上代码:

#include <sys/msg.h>

int msgget(key_t key, int msgflg);

// 示例:创建一个消息队列,权限为0666
int msgid = msgget(key, IPC_CREAT | 0666);
if (msgid == -1) {
    perror("msgget failed");
    exit(1);
}

IPC_CREAT表示如果队列不存在就创建。如果加上IPC_EXCL,则队列已存在时会返回错误——这个组合我经常用来做“只创建一次”的保障。

我的小技巧:权限位一定要写对。0666表示所有用户可读写。如果你只写了644,其他进程可能连发消息的权限都没有。我曾经因为这个bug排查了一下午,最后发现是权限位少了个6。

msgctl():控制消息队列

这个函数能干三件事:获取状态、设置参数、删除队列。最常用的就是删除操作。

#include <sys/msg.h>

int msgctl(int msgid, int cmd, struct msqid_ds *buf);

// 删除消息队列
msgctl(msgid, IPC_RMID, NULL);

// 获取队列状态
struct msqid_ds buf;
msgctl(msgid, IPC_STAT, &buf);
printf("队列中消息数: %ld\n", buf.msg_qnum);

嗯,这里要注意:IPC_RMID是立即删除,所有阻塞在发送或接收操作上的进程会立刻收到错误。我建议在程序退出时主动清理消息队列,否则它会一直留在内核里。用ipcs -q命令可以查看系统中所有的消息队列。

避坑指南:我曾经在生产环境上忘记删除测试用的消息队列,结果系统重启后队列还在,里面的旧数据被新进程读到了,导致业务逻辑错乱。从那以后,我每次创建消息队列都会在程序退出时调用msgctl(IPC_RMID)。

三、消息的发送与接收

msgsnd():发送消息

发送消息需要定义一个消息结构体。这个结构体有严格的要求:第一个成员必须是long mtype,后面才是数据。

#include <sys/msg.h>

struct msgbuf {
    long mtype;       // 消息类型,必须大于0
    char mtext[100];  // 消息内容
};

int msgsnd(int msgid, const void *msgp, size_t msgsz, int msgflg);

// 示例:发送一条类型为1的消息
struct msgbuf msg;
msg.mtype = 1;
strcpy(msg.mtext, "Hello from process A");

if (msgsnd(msgid, &msg, sizeof(msg.mtext), 0) == -1) {
    perror("msgsnd failed");
}

第四个参数msgflg通常传0,表示阻塞发送。如果队列满了,进程会一直等。传IPC_NOWAIT的话,队列满了就直接返回错误——这个在实时性要求高的场景下很有用。

msgrcv():接收消息

接收消息时,你可以指定想要的消息类型。这是消息队列最灵活的地方。

ssize_t msgrcv(int msgid, void *msgp, size_t msgsz, 
               long msgtyp, int msgflg);

// 接收类型为1的消息
struct msgbuf msg;
if (msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0) == -1) {
    perror("msgrcv failed");
}
printf("收到消息: %s\n", msg.mtext);

msgtyp参数很有意思:

  • msgtyp > 0:只接收该类型的消息
  • msgtyp = 0:接收队列中的第一条消息(不管类型)
  • msgtyp < 0:接收类型小于等于|msgtyp|的最小类型消息

我个人最喜欢用正数类型来做“消息分类”。比如类型1是控制指令,类型2是数据包,类型3是心跳。接收方可以各取所需,互不干扰。

完整示例:一个简单的消息队列通信

// sender.c
#include <stdio.h>
#include <sys/msg.h>
#include <string.h>

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

int main() {
    key_t key = ftok("/tmp", 'M');
    int msgid = msgget(key, IPC_CREAT | 0666);
    
    struct msgbuf msg;
    msg.mtype = 1;
    strcpy(msg.mtext, "Hello!");
    
    msgsnd(msgid, &msg, sizeof(msg.mtext), 0);
    printf("消息已发送\n");
    return 0;
}

// receiver.c
#include <stdio.h>
#include <sys/msg.h>

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

int main() {
    key_t key = ftok("/tmp", 'M');
    int msgid = msgget(key, 0666);
    
    struct msgbuf msg;
    msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0);
    printf("收到: %s\n", msg.mtext);
    
    msgctl(msgid, IPC_RMID, NULL);  // 清理队列
    return 0;
}

四、消息队列的知识体系

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

消息队列知识体系 消息队列 msgget() 创建/获取 msgctl() 控制/删除 msgsnd() 发送消息 msgrcv() 接收消息 键值 key_t 标识符 msgid 消息类型 mtype

消息队列是System V IPC三大件之一(另外两个是共享内存和信号量)。它最大的价值在于:让不同进程之间可以像发快递一样传递结构化数据。你不需要关心数据什么时候到、谁发的,只要往队列里扔,或者从队列里取就行。

我个人觉得,消息队列特别适合“生产者-消费者”模型。比如一个传感器采集进程不断发数据,多个处理进程按类型取走各自关心的数据。这种场景下,管道根本玩不转,共享内存又太复杂,消息队列刚刚好。

最后提醒一句:消息队列虽然好用,但消息大小有限制(通常8KB以内),队列总容量也有限制(通常16KB)。如果你要传大块数据,还是考虑共享内存吧。消息队列更适合传控制指令、小数据包这类东西。

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