mmap 比 write() 写日志更快,因其避免用户态到内核页缓存的重复拷贝;write() 需复制数据进 page cache 再异步刷盘,而 mmap 直接修改映射内存,跳过一次 memcpy。

为什么 mmap 比 write() 写日志更快
mmap 的核心优势是避免了用户态缓冲区到内核页缓存的重复拷贝。普通 write() 调用会把日志数据从应用内存复制进内核的 page cache,再由内核异步刷盘;而 mmap() 直接把文件页映射进进程虚拟地址空间,写日志等于直接改内存——只要保证映射页不被意外覆盖、且调用 msync() 或依赖内核回写策略,就能跳过一次 memcpy。
但注意:mmap 不是万能加速器。小日志(10k/s)场景下,频繁 msync() 反而比 buffered write 更慢;真正受益的是中大块日志(如 JSON batch、二进制 record)或需要多进程共享写入的场景。
如何安全地用 mmap 实现多进程并发写日志
多个进程同时 mmap 同一个文件并写入,必须自己管理写位置,不能依赖 lseek() + write() 那套原子性假设——mmap 后的指针操作完全不带同步语义。
- 用一个固定偏移的原子变量(如
std::atomic<size_t></size_t>)记录下一个可用写位置,所有进程通过 CAS 更新它 - 每次写前先读取当前 offset,检查剩余空间是否足够(比如预留 8 字节存 record length),不够则触发 rotate 或 wait
- 写完后不立即
msync(),改为定时(如每 100ms)或按页边界批量 sync,减少系统调用开销 - 务必用
MAP_SHARED | MAP_SYNC(Linux 4.12+)或至少MAP_SHARED,MAP_PRIVATE对日志毫无意义
常见崩溃点:SIGBUS 和页面对齐陷阱
最常遇到的错误是 SIGBUS,通常因为:写越界(超过 mmap 长度)、文件被 truncate、或底层文件系统不支持 mmap 写(如某些 NFS 或 tmpfs 配置)。
立即学习“C++免费学习笔记(深入)”;
关键防御措施:
- 映射时用
posix_fallocate()预分配文件空间,避免写时触发 ENOSPC 或自动扩展失败 - 所有写操作前做边界检查:
if (offset + len > map_size) { handle_rotate(); } - 确保
mmap()的length是sysconf(_SC_PAGESIZE)的整数倍,否则msync()可能失败 - 捕获
SIGBUS并退出,不要试图恢复——这表示内存页已失效,继续写必然 UB
何时该放弃 mmap 改用 io_uring 或 O_DIRECT
当你的日志系统要求:严格顺序落盘(如 WAL)、单条延迟 1GB/min)且磁盘 I/O 成瓶颈时,mmap 的内核回写不确定性会成为问题——你无法控制某一页何时刷盘,msync(MS_SYNC) 又太重。
这时候更合适的选择是:
-
io_uring+O_DIRECT:绕过 page cache,直接提交 IO 请求,配合 ring buffer 实现无锁提交 - 预分配多个固定大小的 log segment 文件,每个文件只 append 写,用
pwrite()+fsync()控制刷盘粒度 - 完全避开文件系统,写入裸设备或 SPDK NVMe 设备(仅限超低延迟场景)
别低估 mmap 的维护成本:信号处理、文件截断恢复、mmap 失败后的 fallback 路径——这些在高可靠日志系统里都得实打实写出来,不是加个 mmap() 就完事。



















