4. 消息队列(Message Queue):System V与POSIX消息队列、消息的发送与接收、优先级控制

消息队列,说白了就是进程间传递数据的一块“公共信箱”。

我刚开始做上位机开发时,总觉得共享内存又快又直接,干嘛还要用消息队列?后来在一个多进程的工控项目里,共享内存的数据同步把我折腾得够呛——两个进程同时写一个结构体,加锁加得我头皮发麻。换用消息队列后,问题迎刃而解。嗯,每种IPC都有它的脾气,消息队列的强项就是“解耦”和“异步”。

4.1 System V 消息队列 vs POSIX 消息队列

Linux下有两套消息队列的API,一套是System V的,一套是POSIX的。你想想看,为什么会有两套?

System V是Unix老祖宗传下来的,历史悠久,几乎所有Linux系统都支持。而POSIX是后来为了统一标准搞的,设计上更现代一些。

对比项 System V 消息队列 POSIX 消息队列
标识方式 key值(整数) 名字(字符串,如 /mq_name)
创建/打开 msgget() mq_open()
发送消息 msgsnd() mq_send()
接收消息 msgrcv() mq_receive()
优先级支持 通过消息类型(长整型) 通过优先级参数(无符号整型)
最大消息数 系统限制(可调) 系统限制(可调)
通知机制 无(需轮询) 支持异步通知(mq_notify)

我个人习惯,在新项目中优先用POSIX消息队列。为什么?因为它的接口更直观,而且支持异步通知,不用像System V那样傻傻地轮询。但如果你要维护老项目,System V的代码你肯定得看得懂。

我的经验:在嵌入式Linux上,POSIX消息队列的实时性更好。我曾经在一个运动控制项目里对比过,POSIX的延迟抖动比System V小了大约30%。当然,这跟内核配置也有关系。

4.2 消息的发送与接收

消息队列的核心操作就两个:发和收。但这里面的门道可不少。

4.2.1 POSIX 消息队列的发送与接收

先看一个简单的例子。发送端:

#include <mqueue.h>
#include <fcntl.h>
#include <stdio.h>
#include <string.h>

int main() {
    mqd_t mq;
    struct mq_attr attr;
    
    // 设置队列属性
    attr.mq_flags = 0;
    attr.mq_maxmsg = 10;      // 最多10条消息
    attr.mq_msgsize = 256;    // 每条消息最大256字节
    attr.mq_curmsgs = 0;
    
    // 创建消息队列
    mq = mq_open("/my_queue", O_CREAT | O_WRONLY, 0644, &attr);
    if (mq == (mqd_t)-1) {
        perror("mq_open");
        return 1;
    }
    
    char *msg = "Hello, 这是消息队列!";
    // 发送消息,优先级设为1
    if (mq_send(mq, msg, strlen(msg) + 1, 1) == -1) {
        perror("mq_send");
    }
    
    mq_close(mq);
    return 0;
}

接收端:

#include <mqueue.h>
#include <fcntl.h>
#include <stdio.h>

int main() {
    mqd_t mq;
    char buffer[256];
    unsigned int priority;
    
    // 打开消息队列
    mq = mq_open("/my_queue", O_RDONLY);
    if (mq == (mqd_t)-1) {
        perror("mq_open");
        return 1;
    }
    
    // 接收消息,会阻塞直到有消息
    ssize_t bytes_read = mq_receive(mq, buffer, sizeof(buffer), &priority);
    if (bytes_read == -1) {
        perror("mq_receive");
    } else {
        printf("收到消息: %s (优先级: %u)\n", buffer, priority);
    }
    
    mq_close(mq);
    return 0;
}

这里有个细节要注意:mq_receive的第二个参数是缓冲区大小,必须大于等于队列创建时设置的mq_msgsize。否则会报EMSGSIZE错误。我曾经在这个坑里栽过跟头,调试了半天才发现是缓冲区开小了。

4.2.2 System V 消息队列的发送与接收

System V的用法稍微绕一点。它要求你定义一个消息结构体,第一个成员必须是long mtype

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

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

int main() {
    int msgid;
    key_t key;
    struct msgbuf msg;
    
    // 生成key
    key = ftok("/tmp", 'A');
    
    // 创建消息队列
    msgid = msgget(key, IPC_CREAT | 0666);
    if (msgid == -1) {
        perror("msgget");
        return 1;
    }
    
    // 发送消息,类型为1
    msg.mtype = 1;
    strcpy(msg.mtext, "Hello from System V!");
    if (msgsnd(msgid, &msg, strlen(msg.mtext) + 1, 0) == -1) {
        perror("msgsnd");
    }
    
    // 接收类型为1的消息
    if (msgrcv(msgid, &msg, sizeof(msg.mtext), 1, 0) == -1) {
        perror("msgrcv");
    } else {
        printf("收到: %s\n", msg.mtext);
    }
    
    // 删除消息队列
    msgctl(msgid, IPC_RMID, NULL);
    return 0;
}
注意:System V消息队列不会自动销毁。进程退出后,队列还在内核里。如果你不调用msgctl(msgid, IPC_RMID, NULL),它会一直占用系统资源。我见过有人写demo时忘了清理,结果系统消息队列用完了,其他程序全挂了。

4.3 优先级控制

消息队列的优先级,说白了就是让重要的消息先走。

POSIX消息队列的优先级是0到31的数字,数字越大优先级越高。发送时指定优先级,接收时默认会返回优先级最高的那条消息。

System V的优先级是通过消息类型实现的。接收时你可以指定类型参数:

  • msgtyp = 0:接收队列中第一条消息(先进先出)
  • msgtyp > 0:接收类型等于msgtyp的第一条消息
  • msgtyp < 0:接收类型小于等于msgtyp绝对值的最小类型消息

举个例子,假设队列里有三条消息:类型1、类型2、类型3。如果你用msgrcv(msgid, &msg, size, -2, 0),它会返回类型1的消息,因为1是小于等于2的最小类型。

实战技巧:我在一个数据采集系统里,把报警消息设为类型1,普通数据设为类型2,心跳包设为类型3。接收端用msgtyp = -1,这样报警永远优先处理。嗯,这个设计让系统的实时性提升了一个档次。

4.4 避坑指南

做消息队列开发,有几个坑我踩过,分享给你:

  • 消息大小限制:Linux默认每条消息最大8192字节,队列最多16条。可以通过/proc/sys/fs/mqueue/下的文件调整。别问我怎么知道的——有一次我传了一张图片,结果消息发不出去。
  • 阻塞与非阻塞:发送和接收默认是阻塞的。如果队列满了,mq_send会一直等。可以用O_NONBLOCK标志改成非阻塞模式,这时候满了会直接返回EAGAIN
  • 权限问题:POSIX消息队列的名字以/开头,但这不是文件路径。不同进程要用相同的名字才能通信。我曾经因为名字写错,两个进程各开各的队列,数据死活传不过去。
  • 资源清理:POSIX消息队列用mq_unlink()删除,System V用msgctl()。程序退出前一定要清理,否则会留下僵尸队列。

消息队列这东西,用好了是利器,用不好就是坑。我个人建议,新手先从POSIX版本入手,接口简单,不容易出错。等把基本概念搞清楚了,再去看System V的实现,你会发现两者本质是一样的——都是内核维护的一个链表,只不过操作方式不同罢了。