结论:不推荐用纯文件模拟消息队列,除非满足跨进程、无依赖、可持久化且性能要求低于1000 msg/s的极简场景;应优先使用System V IPC(msgget/msgsnd/msgrcv)或mmap+ring buffer+flock方案。

直接说结论:C++ 用文件实现消息队列,不是“能不能”,而是“该不该”——除非你明确需要跨进程、无依赖、可持久化、且性能要求低于 1000 msg/s 的极简场景,否则不推荐用纯文件模拟。它本质是用 fseek + fread + fwrite 模拟环形缓冲区,但会踩到原子性、并发竞争、文件锁粒度粗、磁盘 I/O 成为瓶颈等一堆坑。
为什么不能直接用 std::fstream 做多线程消息队列
文件 I/O 本身不是线程安全的:std::fstream 对象实例不能被多个线程同时读写;即使加 std::mutex,也只保护了 C++ 流对象状态,不保证底层 write(2) 系统调用的原子性。更关键的是:Linux 下普通文件写入不保证“追加写”的原子性(除非用 O_APPEND 标志打开),两个线程同时 write 可能导致数据错乱或覆盖。
常见错误现象:
- 消费者读出半条消息(比如只读到 struct 头部,没读完 payload)
- 两条消息粘包(没有分隔符,
read返回连续字节流) - 生产者写入中途崩溃,文件末尾残留脏数据,下次读取直接
segfault
必须用 mmap + ring buffer + fcntl 文件锁才勉强可用
真正能落地的基于文件的消息队列,核心是把文件当内存映射载体,而非 I/O 流。典型结构是「固定头 + 环形消息区」,所有读写都通过 mmap 指针操作,避免 read/write 调用开销和边界问题。
实操要点:
- 用
open(path, O_RDWR | O_CREAT, 0644)创建文件,再ftruncate预分配大小(比如 4MB) - 用
mmap(nullptr, size, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0)映射整块区域 - 头部放两个原子变量:
std::atomic和read_off std::atomic(需对齐到缓存行)write_off - 每次
Push()前,用flock(fd, LOCK_EX)锁整个文件(或用fcntl锁特定字节范围),写完立即解锁;Pop()同理 - 消息体必须带长度字段(如
uint32_t len),不能依赖换行或 \0 分隔
性能影响:mmap 方式比纯 write/read 快 3–5 倍,但依然比内存队列慢一个数量级;flock 是阻塞锁,高并发下容易成为瓶颈。
msgget/msgsnd/msgrcv 系统调用才是正解
Linux 原生消息队列(System V IPC)就是基于内核文件系统(/dev/mqueue 下可见)实现的,但它由内核保障原子性、类型过滤、阻塞/非阻塞语义,且支持 mtype 字段做消息路由——这才是“基于文件”的正确打开方式,而不是自己造轮子。
关键差异:
-
msgget(key, IPC_CREAT|0666)创建队列,返回msgid(本质是内核索引) -
msgsnd(msgid, &msgbuf, msgsz, IPC_NOWAIT)发送,msgbuf.mtype必须 > 0 -
msgrcv(msgid, &msgbuf, msgsz, type, flag)中type=0表示取队首,type>0表示只取该类型,type 表示取 ≤ |type| 的最小类型 - 队列满时
msgsnd默认阻塞,加IPC_NOWAIT则返回 -1 并设errno=EAGAIN
兼容性注意:System V IPC 在容器中默认禁用(需 --ipc=host 或挂载 /dev/mqueue),macOS 不支持;但 glibc 兼容层稳定,从 2.2 到 2.39 都可用。
最容易被忽略的三个细节
一是 ftok(".", 'a') 生成 key 时,路径参数必须存在且权限可读,否则 ftok 返回 -1 —— 很多人用不存在的路径调试半天才发现;
二是 struct msgbuf 中 mtype 必须是 正整数,传 0 或负数会导致 msgsnd 直接失败(errno=EINVAL),但错误信息不提示具体原因;
三是消息体大小不能超过系统限制(cat /proc/sys/kernel/msgmax,默认 65536),超长消息会被截断,且 msgrcv 返回实际读取字节数,必须校验是否等于预期长度,否则后续解析全错。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











