10、消息队列实战:从入门到精通

消息队列,说白了就是内核帮我们维护的一个「邮局」。进程A往里头扔信,进程B从里头取信。我在做嵌入式项目时,经常用它来解耦不同模块——比如传感器采集和数据处理,各干各的,互不干扰。

今天咱们就把消息队列的方方面面都捋一遍。嗯,内容不少,但都是干货。

10.1 消息队列的权限设置

创建消息队列用的是 msgget() 函数。它的第二个参数就是权限标志,跟文件权限类似。

#include <sys/msg.h>

// 创建或获取一个消息队列
int msgget(key_t key, int msgflg);

// 示例:创建一个权限为 0666 的消息队列
int msgid = msgget(IPC_PRIVATE, IPC_CREAT | 0666);

这里的 0666 表示所有用户都可读写。但我在实际项目中,一般只给 0640——只有自己和同组人能操作。你想想看,万一别的进程乱发消息,调试起来多头疼。

注意:权限设置不是摆设。我曾经在一个多租户系统里,因为偷懒用了 0777,结果两个业务模块的消息互相串了,查了一整天才找到原因。

10.2 消息队列的大小限制

消息队列不是无限大的。内核有几个硬限制:

参数 默认值 说明
MSGMNB 16384 字节 单个队列的最大字节数
MSGMNI 32000 系统最大队列数
MSGMAX 8192 字节 单条消息的最大长度

这些值可以通过 sysctl 查看和修改:

# 查看当前限制
sysctl kernel.msgmnb
sysctl kernel.msgmni
sysctl kernel.msgmax

# 临时修改(重启后失效)
sysctl -w kernel.msgmnb=65536

我个人习惯在生产环境里把 MSGMAX 调到 65535。为什么?因为有一次我传一张小图片,消息被截断了,数据对不上,排查了好久才发现是默认限制太小。

10.3 阻塞与非阻塞

发送和接收消息时,可以设置阻塞或非阻塞模式。关键在 msgflg 参数:

// 发送消息
int msgsnd(int msgid, const void *msgp, size_t msgsz, int msgflg);

// 接收消息
ssize_t msgrcv(int msgid, void *msgp, size_t msgsz, long msgtyp, int msgflg);

msgflg 包含 IPC_NOWAIT 时,就是非阻塞模式。

核心区别:

  • 阻塞模式:队列满了就等着,直到有空间
  • 非阻塞模式:队列满了直接返回 -1,errno 设为 EAGAIN

我建议在实时性要求高的场景用非阻塞。比如传感器数据采集,丢一帧没关系,但不能卡住整个流程。

10.4 消息类型的使用

消息队列最妙的地方就是支持消息类型。每个消息可以带一个 long 类型的编号:

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

接收时可以根据类型过滤:

// 接收类型为 1 的消息
msgrcv(msgid, &buf, sizeof(buf.mtext), 1, 0);

// 接收类型为 2 的消息
msgrcv(msgid, &buf, sizeof(buf.mtext), 2, 0);

// 接收所有消息(类型设为 0)
msgrcv(msgid, &buf, sizeof(buf.mtext), 0, 0);

我在项目中常用类型来区分优先级:类型 1 是紧急消息,类型 2 是普通消息,类型 3 是日志。这样接收方可以优先处理紧急任务。

10.5 多进程通信实战

来个完整的例子。父进程发消息,子进程收消息:

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

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

int main() {
    int msgid = msgget(IPC_PRIVATE, IPC_CREAT | 0640);
    if (msgid == -1) {
        perror("msgget");
        exit(1);
    }

    pid_t pid = fork();
    if (pid == 0) {
        // 子进程:接收消息
        struct msgbuf buf;
        msgrcv(msgid, &buf, sizeof(buf.mtext), 1, 0);
        printf("子进程收到: %s\n", buf.mtext);
    } else {
        // 父进程:发送消息
        struct msgbuf buf = {1, "Hello from parent!"};
        msgsnd(msgid, &buf, strlen(buf.mtext) + 1, 0);
        wait(NULL);
    }

    msgctl(msgid, IPC_RMID, NULL);
    return 0;
}

这个例子虽然简单,但包含了完整的流程:创建队列、发送、接收、清理。嗯,别忘了最后要删除队列,否则内核里会残留垃圾。

10.6 消息队列的优缺点

用了这么多年,我总结了几点:

优点:

  • 解耦彻底:发送方和接收方不需要同时运行
  • 支持多对多:多个进程可以往同一个队列发消息
  • 有类型过滤:接收方可以只关心自己需要的消息
  • 内核持久化:进程挂了消息不会丢

缺点:

  • 大小有限:不能传大文件或大数据块
  • 无优先级反转保护:高类型消息不一定先被处理
  • 调试困难:消息队列是内核资源,用 ipcs 才能看到
  • 跨机器不行:只能用于同一台机器的进程

10.7 应用场景

根据我的经验,消息队列最适合这些场景:

  • 日志收集:多个模块往队列里写日志,一个专门的日志进程去处理
  • 任务分发:主进程把任务按类型发给不同的工作进程
  • 事件通知:某个模块状态变了,发个消息通知其他模块
  • 数据采集:传感器数据先入队列,处理程序慢慢取

说白了,只要数据量不大、不需要跨机器、需要解耦的场景,消息队列都是好选择。

10.8 综合案例:简易日志系统

最后来个综合案例。三个进程:一个写日志,一个读日志,一个控制日志级别。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/msg.h>
#include <unistd.h>
#include <time.h>

#define MSG_TYPE_LOG    1
#define MSG_TYPE_CTRL   2

struct msgbuf {
    long mtype;
    int  level;      // 0=INFO, 1=WARN, 2=ERROR
    char content[256];
};

int main() {
    int msgid = msgget(1234, IPC_CREAT | 0640);
    if (msgid == -1) {
        perror("msgget");
        exit(1);
    }

    pid_t pid1 = fork();
    if (pid1 == 0) {
        // 进程1:写日志
        srand(time(NULL) ^ getpid());
        for (int i = 0; i < 5; i++) {
            struct msgbuf buf;
            buf.mtype = MSG_TYPE_LOG;
            buf.level = rand() % 3;
            snprintf(buf.content, sizeof(buf.content), 
                     "日志条目 #%d", i);
            msgsnd(msgid, &buf, sizeof(buf) - sizeof(long), 0);
            sleep(1);
        }
        exit(0);
    }

    pid_t pid2 = fork();
    if (pid2 == 0) {
        // 进程2:读日志并输出
        struct msgbuf buf;
        while (1) {
            ssize_t ret = msgrcv(msgid, &buf, 
                                 sizeof(buf) - sizeof(long), 
                                 MSG_TYPE_LOG, IPC_NOWAIT);
            if (ret > 0) {
                const char *level_str[] = {"INFO", "WARN", "ERROR"};
                printf("[%s] %s\n", level_str[buf.level], buf.content);
            } else {
                usleep(100000);
            }
        }
        exit(0);
    }

    // 父进程:发送控制消息
    sleep(3);
    struct msgbuf ctrl = {MSG_TYPE_CTRL, 0, "shutdown"};
    msgsnd(msgid, &ctrl, sizeof(ctrl) - sizeof(long), 0);

    wait(NULL);
    wait(NULL);
    msgctl(msgid, IPC_RMID, NULL);
    return 0;
}

这个案例里,写日志的进程只管发,读日志的进程只管收,控制消息走另一个类型。互不干扰,各司其职。

小技巧:调试消息队列时,多用 ipcs -q 查看队列状态,用 ipcrm -q msgid 清理残留队列。我曾经因为忘记清理,导致系统消息队列用满,新进程创建失败,折腾了半天。

消息队列知识体系 消息队列 权限设置 msgget() + 权限位 大小限制 MSGMNB / MSGMAX 阻塞与非阻塞 IPC_NOWAIT 标志 消息类型 mtype 过滤接收 多进程通信 fork + msg队列 优缺点 解耦 vs 大小限制 应用场景 日志/任务分发 综合案例 简易日志系统

好了,消息队列的核心内容就这些。从权限到大小,从阻塞到类型,再到多进程实战,每一步都有坑,但也都有解法。记住一点:消息队列是工具,用对了地方事半功倍,用错了反而添乱。

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