POSIX IPC:消息队列与信号量

大家好,我是你们的老朋友。今天我们来聊聊 POSIX IPC 的两员大将——消息队列和信号量。说实话,我在刚接触这部分内容时,也曾在 System V 和 POSIX 之间犹豫过。但用久了你会发现,POSIX 接口的设计思路更贴近现代编程习惯。

POSIX 消息队列:轻量级的进程对话

消息队列,说白了就是让进程之间能互相递纸条。POSIX 版本比 System V 的接口更简洁,也更安全。

核心函数速览

函数 作用 我常用的场景
mq_open() 创建或打开消息队列 服务端初始化时调用
mq_send() 发送消息 生产者线程写入数据
mq_receive() 接收消息 消费者线程读取数据
mq_close() 关闭描述符 进程退出前清理
mq_unlink() 删除队列 彻底销毁时使用

关键点:消息队列是内核持久化的。进程退出后,队列还在。只有调用 mq_unlink() 才会真正删除。

一个完整的例子

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

int main() {
    struct mq_attr attr;
    attr.mq_flags = 0;
    attr.mq_maxmsg = 10;      // 最多10条消息
    attr.mq_msgsize = 256;    // 每条最大256字节
    attr.mq_curmsgs = 0;

    // 创建消息队列
    mqd_t mq = mq_open("/myqueue", O_CREAT | O_RDWR, 0644, &attr);
    if (mq == (mqd_t)-1) {
        perror("mq_open");
        return 1;
    }

    // 发送消息
    char *msg = "Hello from POSIX!";
    if (mq_send(mq, msg, strlen(msg) + 1, 0) == -1) {
        perror("mq_send");
    }

    // 接收消息
    char buf[256];
    unsigned int prio;
    ssize_t n = mq_receive(mq, buf, sizeof(buf), &prio);
    if (n != -1) {
        printf("收到: %s (优先级: %u)\n", buf, prio);
    }

    mq_close(mq);
    mq_unlink("/myqueue");
    return 0;
}

我的小技巧:消息优先级用 0 表示普通消息。数值越大优先级越高。我曾经在一个日志收集系统里,用高优先级发告警消息,低优先级发普通日志,效果很好。

POSIX 信号量:轻量级的同步工具

信号量就是用来控制多个进程对共享资源的访问。POSIX 信号量分两种:命名信号量和无名信号量。今天重点讲命名信号量,因为它可以在不相关的进程间使用。

核心函数

  • sem_open() — 创建或打开信号量
  • sem_wait() — 减1操作(P操作),如果为0则阻塞
  • sem_post() — 加1操作(V操作),唤醒等待者
  • sem_close() — 关闭信号量
  • sem_unlink() — 删除信号量

注意:sem_wait() 可能会被信号中断返回 EINTR。我早期写代码时没处理这个,结果线上偶发死锁。后来统一加了重试逻辑。

信号量使用示例

#include <semaphore.h>
#include <fcntl.h>
#include <stdio.h>
#include <pthread.h>

sem_t *sem;

void* worker(void* arg) {
    int id = *(int*)arg;
    sem_wait(sem);  // 请求资源
    printf("线程 %d 进入临界区\n", id);
    sleep(1);       // 模拟工作
    printf("线程 %d 离开临界区\n", id);
    sem_post(sem);  // 释放资源
    return NULL;
}

int main() {
    // 创建信号量,初始值为2(允许2个并发)
    sem = sem_open("/mysem", O_CREAT, 0644, 2);
    if (sem == SEM_FAILED) {
        perror("sem_open");
        return 1;
    }

    pthread_t t1, t2, t3;
    int id1 = 1, id2 = 2, id3 = 3;
    pthread_create(&t1, NULL, worker, &id1);
    pthread_create(&t2, NULL, worker, &id2);
    pthread_create(&t3, NULL, worker, &id3);

    pthread_join(t1, NULL);
    pthread_join(t2, NULL);
    pthread_join(t3, NULL);

    sem_close(sem);
    sem_unlink("/mysem");
    return 0;
}

你想想看,如果初始值设为1,那就变成互斥锁了。设为N,就是N个并发。这就是信号量的灵活之处。

POSIX vs System V:我该怎么选?

很多初学者会纠结这个问题。我个人的经验是:新项目优先用 POSIX。原因如下:

对比维度 POSIX System V
接口风格 类文件描述符,简洁 用 key_t 和 id,较复杂
命名方式 以 "/" 开头的字符串 用 ftok() 生成 key
消息优先级 支持,0-31 优先级 支持,但类型是 long
超时机制 mq_timedreceive() 支持 不支持,需自己实现
信号量操作 单个信号量操作 支持批量操作(semop)
可移植性 现代系统都支持 老系统兼容性好

我的建议:如果你在写新项目,且不需要批量信号量操作,选 POSIX。如果你在维护老代码,或者需要复杂的信号量数组操作,System V 可能更合适。

知识体系总览

下面这张图帮你理清今天讲的内容:

POSIX IPC 知识体系 消息队列 (mq_*) 信号量 (sem_*) mq_open() mq_send() mq_receive() mq_close/unlink sem_open() sem_wait() sem_post() sem_close/unlink POSIX vs System V 对比 POSIX:接口简洁、命名直观、支持超时 System V:批量操作、老系统兼容、复杂场景 新项目推荐 POSIX,维护老代码用 System V

避坑指南

最后分享几个我踩过的坑:

  • 消息队列权限问题:创建时记得设置 0644,否则其他进程打不开。我曾经在调试时忘了设权限,排查了半天。
  • 信号量初始值:千万别设成0然后直接 sem_wait(),那会永远阻塞。我见过有人把初始值设成0,然后抱怨程序卡死。
  • 资源清理:进程崩溃后,消息队列和信号量还留在系统里。可以用 ipcs 命令查看,用 ipcrm 手动清理。
  • 命名冲突:POSIX IPC 的名字以 "/" 开头,但不同系统对名字长度有限制。Linux 下建议控制在 255 字节以内。

调试小技巧:cat /proc/sys/fs/mqueue/msg_max 查看系统消息队列上限。如果不够用,可以临时修改:echo 100 > /proc/sys/fs/mqueue/msg_max

好了,今天的内容就到这里。POSIX 消息队列和信号量是进程通信的利器,用好了能写出高效稳定的程序。记住:接口简单不代表功能弱,很多时候简单反而更可靠。


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