消息队列:进程间通信的“快递驿站”

消息队列,说白了就是内核帮我们维护的一个消息链表。每个消息都有类型,进程可以往队列里扔消息,也可以按类型取消息。这玩意儿在Linux进程间通信里非常实用,我这些年做嵌入式项目,没少跟它打交道。

今天咱们就聊聊消息队列的四个核心函数:msgget()msgsnd()msgrcv()msgctl()。掌握了它们,你就能让两个毫不相干的进程像发微信一样互相传数据。

消息队列长什么样?

先看一张图,帮你快速建立整体认知:

消息队列工作原理 进程A msgsnd() 发送 进程B msgrcv() 接收 内核空间(消息队列) 消息1 (type=1) 消息2 (type=2) 消息3 (type=1) 消息4 (type=3) 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 关门打扫。你想想看,是不是跟咱们收发快递一个道理?


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