消息队列:进程间通信的“快递驿站”
消息队列,说白了就是内核帮我们维护的一个消息链表。每个消息都有类型,进程可以往队列里扔消息,也可以按类型取消息。这玩意儿在Linux进程间通信里非常实用,我这些年做嵌入式项目,没少跟它打交道。
今天咱们就聊聊消息队列的四个核心函数:msgget()、msgsnd()、msgrcv()、msgctl()。掌握了它们,你就能让两个毫不相干的进程像发微信一样互相传数据。
消息队列长什么样?
先看一张图,帮你快速建立整体认知:
看到没?进程A往内核的队列里塞消息,进程B从队列里取消息。它们不需要直接通信,全靠内核这个“快递驿站”中转。这样做的好处是:发送方和接收方可以完全解耦,你发你的,我收我的,互不阻塞。
1. msgget():创建或获取消息队列
这个函数负责创建新的消息队列,或者获取一个已有的队列。原型长这样:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
参数说明:
key:一个键值,类似门牌号。不同进程用同一个key就能找到同一个队列。msgflg:权限标志,比如IPC_CREAT | 0666表示如果队列不存在就创建,权限设为666。
返回值:成功返回队列ID(一个非负整数),失败返回-1。
关键点:key的生成通常用 ftok() 函数,它根据文件路径和项目ID生成一个唯一的key。我个人习惯用 ftok("/tmp", 'A') 这种形式,简单可靠。
看个例子:
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <errno.h>
int main() {
key_t key = ftok("/tmp", 'M');
if (key == -1) {
perror("ftok");
return 1;
}
int msgid = msgget(key, IPC_CREAT | 0666);
if (msgid == -1) {
perror("msgget");
return 1;
}
printf("消息队列创建成功,ID: %d\n", msgid);
return 0;
}
小技巧:如果你只是想获取已有的队列,不创建新的,可以用 msgget(key, 0)。这样如果队列不存在,直接返回-1,不会创建。
2. msgsnd():发送消息
有了队列ID,就可以往里扔消息了。发送函数长这样:
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
参数说明:
msqid:msgget返回的队列ID。msgp:指向消息缓冲区的指针。注意,这个缓冲区有固定格式!msgsz:消息正文的长度(不包括消息类型)。msgflg:通常填0,表示阻塞发送。如果填IPC_NOWAIT,队列满时直接返回-1。
消息结构体必须这样定义:
struct msgbuf {
long mtype; // 消息类型,必须 > 0
char mtext[256]; // 消息正文,大小自定
};
注意:mtype 必须是正数!0是无效的。我见过有人踩这个坑,死活发不出去。
看个发送示例:
struct msgbuf {
long mtype;
char mtext[128];
};
int main() {
key_t key = ftok("/tmp", 'M');
int msgid = msgget(key, IPC_CREAT | 0666);
struct msgbuf msg;
msg.mtype = 1; // 消息类型设为1
snprintf(msg.mtext, sizeof(msg.mtext), "Hello, 这是第1条消息");
if (msgsnd(msgid, &msg, strlen(msg.mtext) + 1, 0) == -1) {
perror("msgsnd");
return 1;
}
printf("消息发送成功\n");
return 0;
}
注意:msgsnd的第三个参数是消息正文的长度,不包括mtype。我刚开始学的时候老是把整个结构体大小传进去,结果内核解析出错。记住:只传mtext的长度!
3. msgrcv():接收消息
有发就有收。接收函数是消息队列的精髓,因为它支持按类型接收:
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
参数说明:
msqid:队列ID。msgp:接收缓冲区。msgsz:缓冲区大小。msgtyp:消息类型,这个参数很灵活:- 如果
msgtyp == 0:接收队列中的第一条消息(先进先出)。 - 如果
msgtyp > 0:接收类型等于msgtyp的第一条消息。 - 如果
msgtyp < 0:接收类型小于等于|msgtyp|的最小类型消息。
- 如果
msgflg:通常填0,阻塞接收。填IPC_NOWAIT则非阻塞。
接收示例:
struct msgbuf {
long mtype;
char mtext[128];
};
int main() {
key_t key = ftok("/tmp", 'M');
int msgid = msgget(key, IPC_CREAT | 0666);
struct msgbuf msg;
// 接收类型为1的消息
if (msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0) == -1) {
perror("msgrcv");
return 1;
}
printf("收到消息: %s\n", msg.mtext);
return 0;
}
实用场景:我在做多线程任务调度时,用消息类型来区分任务优先级。类型1是紧急任务,类型2是普通任务,类型3是后台任务。接收方按优先级依次处理,非常优雅。
4. msgctl():控制消息队列
最后一个函数,用来控制队列本身。比如删除队列、获取状态、修改权限等:
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
常用命令:
| cmd | 作用 | 说明 |
|---|---|---|
| IPC_STAT | 获取队列状态 | 把信息填入buf结构体 |
| IPC_SET | 设置队列属性 | 修改权限、用户ID等 |
| IPC_RMID | 删除消息队列 | 从内核中移除,buf填NULL |
删除队列的代码最简单:
if (msgctl(msgid, IPC_RMID, NULL) == -1) {
perror("msgctl");
return 1;
}
printf("消息队列已删除\n");
重要提醒:消息队列不会随进程退出而自动销毁!我曾经在一个服务器程序里忘了删除队列,结果重启后旧的队列还在,导致新消息和旧消息混在一起,排查了半天。所以,程序退出前一定要记得调用 msgctl(msgid, IPC_RMID, NULL)。
完整示例:发送端 + 接收端
最后,给你一个完整的双进程通信示例。发送端和接收端用同一个key连接:
发送端 (sender.c):
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <string.h>
struct msgbuf {
long mtype;
char mtext[256];
};
int main() {
key_t key = ftok("/tmp", 'X');
int msgid = msgget(key, IPC_CREAT | 0666);
struct msgbuf msg;
msg.mtype = 1;
strcpy(msg.mtext, "这是发送端发来的数据");
msgsnd(msgid, &msg, strlen(msg.mtext) + 1, 0);
printf("发送完成\n");
return 0;
}
接收端 (receiver.c):
#include <stdio.h>
#include <sys/ipc.h>
#include <sys/msg.h>
struct msgbuf {
long mtype;
char mtext[256];
};
int main() {
key_t key = ftok("/tmp", 'X');
int msgid = msgget(key, IPC_CREAT | 0666);
struct msgbuf msg;
msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0);
printf("收到: %s\n", msg.mtext);
// 用完记得删除
msgctl(msgid, IPC_RMID, NULL);
return 0;
}
调试小技巧:在终端用 ipcs -q 可以查看当前系统的所有消息队列。如果发现队列没删干净,用 ipcrm -q 队列ID 手动删除。这个命令我几乎每天都会用到。
避坑指南
做消息队列开发,有几个坑我帮你提前踩了:
- 消息类型必须为正数:0是无效的,负数是特殊含义(按类型范围接收)。
- msgsnd的第三个参数:只传消息正文长度,别传整个结构体。
- 队列不会自动销毁:进程退出后队列还在,记得用msgctl删除。
- ftok的路径必须存在:如果路径不存在,ftok会失败。我习惯用
/tmp下的文件。 - 权限问题:创建时指定0666,否则其他进程可能无法访问。
好了,消息队列的四个核心函数就这些。说白了就是:msgget 开门,msgsnd 扔东西,msgrcv 取东西,msgctl 关门打扫。你想想看,是不是跟咱们收发快递一个道理?