一、消息队列:进程间通信的“邮局”
消息队列,说白了就是内核帮我们维护的一个消息链表。每个消息都是一个数据块,有类型、有长度、有内容。进程可以往队列里扔消息,也可以从队列里取消息——就像去邮局寄信、收信一样。
我刚开始学进程间通信时,总觉得管道就够用了。直到有一次做嵌入式项目,需要在多个进程之间传递结构化的数据包,管道那串字节流简直让人抓狂。后来换成消息队列,整个世界都清净了。
消息队列的核心概念
消息队列有几个关键点,我建议你记牢:
- 消息有类型:每个消息可以带一个长整型值作为类型标识。接收方可以按类型取消息,不用按先进先出的顺序。
- 消息有边界:每条消息都是独立的数据单元,不会像管道那样粘在一起。
- 消息队列有生命周期:除非你显式删除,否则消息队列会一直存在内核里。哪怕创建它的进程退出了,队列依然在。
消息队列 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;
}
四、消息队列的知识体系
下面这张图帮你理清消息队列的核心脉络:
消息队列是System V IPC三大件之一(另外两个是共享内存和信号量)。它最大的价值在于:让不同进程之间可以像发快递一样传递结构化数据。你不需要关心数据什么时候到、谁发的,只要往队列里扔,或者从队列里取就行。
我个人觉得,消息队列特别适合“生产者-消费者”模型。比如一个传感器采集进程不断发数据,多个处理进程按类型取走各自关心的数据。这种场景下,管道根本玩不转,共享内存又太复杂,消息队列刚刚好。
最后提醒一句:消息队列虽然好用,但消息大小有限制(通常8KB以内),队列总容量也有限制(通常16KB)。如果你要传大块数据,还是考虑共享内存吧。消息队列更适合传控制指令、小数据包这类东西。