管道是字节流,socket 是连接或数据报,共享内存是无结构字节区——都不自带”消息”语义。Linux 上提供”带边界、可优先级排序”的消息队列其实有两代 API(SysV msg、POSIX mq),以及一个曾经想成为第三代但夭折的 kdbus。本文讲清楚它们的差别以及为什么新代码一般不选它们。
一、先看图
flowchart LR
subgraph SysV[SysV msg · 内核队列]
S1[msgget key]-->S2[msgid]
S2-->S3[msgsnd type=N]
S2-->S4[msgrcv type=0/+N/-N]
end
subgraph POSIX[POSIX mq · /dev/mqueue]
P1[mq_open /name]-->P2[fd-like mqd]
P2-->P3[mq_send prio]
P2-->P4[mq_receive 最高prio]
P2-->P5[mq_notify signal/fd]
end
subgraph Alt[现代替代]
A1[UNIX socket<br/>SOCK_SEQPACKET]
A2[broker: NATS/Redis/Kafka]
A3[dbus-broker]
end
classDef old fill:#63636e22,stroke:#636e7b,color:#adbac7;
classDef posix fill:#388bfd22,stroke:#388bfd,color:#adbac7;
classDef alt fill:#3fb95022,stroke:#3fb950,color:#adbac7;
class S1,S2,S3,S4 old
class P1,P2,P3,P4,P5 posix
class A1,A2,A3 alt
二、SysV msg
int id = msgget(IPC_PRIVATE, 0600 | IPC_CREAT);
struct { long mtype; char data[64]; } buf = { 1, "hi" };
msgsnd(id, &buf, sizeof(buf.data), 0);
msgrcv(id, &buf, sizeof(buf.data), 1, 0);特性:
- “类型 mtype” 是过滤字段:0 = 任意、+N = 精确匹配、-N = ≤|N| 的最小值
- 队列容量由
msgmnb系统限制 - 无优先级,靠 mtype 近似
- 生命周期同 SysV shm,需要
IPC_RMID
现代使用:几乎无。嵌入式或旧 Unix 代码还留。
三、POSIX mq_* 家族
struct mq_attr attr = { .mq_maxmsg = 10, .mq_msgsize = 1024 };
mqd_t mq = mq_open("/foo", O_CREAT | O_RDWR, 0600, &attr);
mq_send(mq, buf, len, 5 /* priority */);
mq_receive(mq, buf, sizeof(buf), &prio);
mq_close(mq);
mq_unlink("/foo");特性:
- 每条消息带优先级 0-32;高优先级先出
mq_notify可以在队列从空变非空时发 signal 或线程回调;5.1+ 还可绑 eventfd 供 epoll- 挂在
/dev/mqueue伪文件系统,cat /dev/mqueue/foo能看状态 - mqd_t 在 Linux 上就是 fd,可
poll(mq, POLLIN)等
限制:
/proc/sys/fs/mqueue/msg_max默认 10/proc/sys/fs/mqueue/msgsize_max默认 8KB- 超限需要 CAP_SYS_RESOURCE
比 SysV 干净,但仍很少见。大部分项目觉得不如 Unix socket + 小序列化来得灵活。
四、kdbus 的夭折
2013-2015 年 systemd 团队力推 kdbus——把 D-Bus 的消息路由搬进内核,解决 userspace dbus-daemon 成为瓶颈的问题。提 mainline 被拒(设计复杂、安全面宽、被认为”应该在 userspace 做更快”)。
取而代之的是 dbus-broker(userspace),用 epoll + SCM_RIGHTS + 精心优化把传统 dbus-daemon 的性能提高几倍。systemd 现在默认用 dbus-broker。
教训:内核 API 一旦加了就永远得维护,内核社区对新 IPC 子系统极其保守。
五、为什么大家都转向 socket + 库
- 协议演化:socket + protobuf/cap’n’proto 可以改 schema;内核 mq 绑死字节结构
- 可路由:broker(Redis Streams、NATS、Kafka、RabbitMQ)提供跨机、fanout、持久化
- 可观测:socket 能 tcpdump、strace;mq
只有
/dev/mqueue - 语言生态:所有语言都有 socket 库,mq 大多数要 C binding
IPC 选型经验:
| 场景 | 推荐 |
|---|---|
| 进程间小消息、低延迟 | AF_UNIX SOCK_SEQPACKET |
| 父子共享、有序字节流 | pipe |
| 跨机、持久化、多订阅 | broker(NATS/Kafka) |
| 进程总线、系统服务 | D-Bus(via dbus-broker) |
| 超高性能、大批量 | shared memory ring + futex |
只有当”多进程、需要优先级、不想自己写 broker”时才可能用 POSIX mq。
六、mq_notify 的三种变体
- Signal(
SIGEV_SIGNAL):空转非空送 signal。信号的老毛病(B-13) - Thread(
SIGEV_THREAD):内部起线程调回调函数。glibc 每次 notify 都起一个新线程,非常重 - fd(
SIGEV_THREAD_ID+ event fd 魔法,或直接 poll/epoll mqd):现代姿势
只用 poll/epoll 的话连 notify 都用不上——和普通 fd 一样。
七、D-Bus 的地位
Linux 桌面、systemd、NetworkManager、BlueZ、PulseAudio 的系统总线。提供:
- 可名字空间路由(
org.freedesktop.systemd1) - introspection / method call / signal
- 策略控制(policy XML)
实现:userspace dbus-broker(Red Hat
开发,性能接近当年 kdbus 目标)。内核仅提供 Unix
socket。
在系统编程里:你要和 systemd / NetworkManager / Upower 打交道就用 D-Bus;纯服务间 RPC 用 gRPC / Thrift / Cap’n Proto。
八、消息队列常见 bug
bug A:生产速度 > 消费,mq 满 原因:默认 10 条 解决:调大 maxmsg、设 nonblocking、反压到生产端
bug B:kdbus/dbus 性能不够 解决:换 dbus-broker;或评估是否真需要总线
bug C:重启后 POSIX mq 还在
原因:/dev/mqueue
持久;进程退出不自动 unlink 解决:启动时
mq_unlink 清理
bug D:cgroup 限制内存,mq 段被计入
原因:POSIX mq 内存走
kernel.msg_max 但计入进程内存;某些老内核有争议
诊断:/proc/<pid>/status、cgroup
memory stats
九、把消息队列当 FIFO 跨进程缓冲?
一个反模式:把 POSIX mq 当”持久 FIFO”。它不是:
- 进程都退出后内核仍保留(好)
- 但系统重启丢失(坏)
- 内存代价(RAM-backed)
- 不跨主机
真要持久性:用 Redis Streams、NATS JetStream、Kafka。
十、小结
- SysV msg 老、POSIX mq 冷、kdbus 夭折
- Linux 上消息队列在 systemd/实时/嵌入式偶尔见,通用服务几乎不用
- 首选:AF_UNIX SOCK_SEQPACKET + 序列化;跨机走 broker
- D-Bus 用 userspace dbus-broker,不要期待内核版
下一篇 B-17 讲 namespace——容器的内核根基,Linux 最重要的进程属性扩展。
参考文献
- Kerrisk, M. The Linux Programming Interface, Ch. 45, 52
- Linux source:
ipc/msg.c、ipc/mqueue.c mq_overview(7)、svipc(7)、sd-bus(3)- Corbet, J. “Post-mortem on kdbus.” LWN.net 2016
- D-Bus specification, freedesktop.org
- Gundersen, T. “dbus-broker: a new implementation.” systemd.conf 2017
工具
ipcs -q/ipcrm -q—— SysV msg 列表与清理ls /dev/mqueue—— POSIX mq 列表busctl list—— D-Bus 总线上所有服务dbus-monitor/busctl monitor—— 消息抓取bpftrace -e 'kprobe:do_mq_timedsend { ... }'
上一篇:共享内存:SysV vs POSIX vs memfd 下一篇:namespace:容器的内核根基
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】共享内存:SysV vs POSIX vs memfd
共享内存是最快的 IPC,但 API 分裂成三代:SysV shmget/shmat、POSIX shm_open、现代 memfd_create。本文讲三者的内核差别、hugetlbfs 的大页共享、memfd sealing 在 GPU/Wayland 里的安全作用、以及 /dev/shm 的 tmpfs 真相。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。