双缓冲本身不提升IO吞吐量,真正起效的是避免业务线程阻塞write()、批量合并系统调用、绕过page cache干扰;盲目加大buffer或硬上io_uring易丢日志、卡死或触发-EINVAL。

双缓冲本身不提升 IO 吞吐量,真正起效的是让业务线程不等 write()、批量合并系统调用、绕过 page cache 干扰;盲目加大 buffer 或硬上 io_uring 反而容易丢日志、卡死或触发 -EINVAL。
为什么只用 std::atomic<char></char> 交换指针会丢日志
常见错误是声明 std::atomic<char> current_buf</char>,业务线程直接往里 memcpy,但没同步“写入完成”。一旦后台线程在拷贝中途执行 exchange(),就会读到截断、未对齐甚至跨字段的日志项。
- 必须配合一个独立的原子长度变量:
std::atomic<size_t> used_len</size_t>,每次完整写入一条struct log_entry后用memory_order_release更新 - 切换前检查
used_len.load()是否已对齐到log_entry边界,不能停在level和msg中间 - 更稳妥的做法是弃用单原子指针,改用三状态机:
READY_TO_WRITE → SWITCHING → READY_TO_FLUSH,靠std::atomic<int></int>+compare_exchange_weak控制跃迁
posix_memalign 和 mlock 为什么不能省
write() 第一次访问某段内存仍可能触发缺页异常,尤其在大 buffer(如 16MB)下,延迟不可控。
- buffer 内存必须用
posix_memalign(4096, size)分配,不能用new char[n]或std::vector - 分配后立即用
mlock()锁住物理页(若权限允许),或至少用memset(buf, 0, size)触发首次映射 - 调用
write()时传入的buf指针必须是posix_memalign返回的原始地址,不能偏移(否则内核校验失败返回-EINVAL)
刷盘线程该用 write() 还是 io_uring
除非你已在 kernel ≥ 5.11 稳定运行且全链路适配 io_uring(包括文件打开标志、内存对齐、sqe 生命周期管理),否则 write() 更稳。
立即学习“C++免费学习笔记(深入)”;
- 用
io_uring必须满足:O_DIRECT | O_SYNC打开文件、buffer 地址用posix_memalign对齐、每个io_uring_sqe提交前重置user_data和flags - 提交返回
-EAGAIN时,绝不能立刻重试——应把该 buffer 挂入 pending 队列,等下一轮io_uring_submit()成功后再推 - 普通场景推荐「专用 flush 线程 +
writev()+fdatasync()」:每满 4KB 或空闲超 1s 触发fdatasync(),避免每次write()都等磁盘确认
文件打开和刷盘时机的硬约束
加错 flag 就等于废掉整个双缓冲设计。
- 文件打开必须用
O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC,禁用O_SYNC、O_DSYNC、O_DIRECT -
fdatasync()只在三个时机调用:buffer 满、空闲超 1s、显式flush()请求;它比fsync()轻量,只刷数据页 - buffer 大小不是越大越好:64KB(65536)适合 SSD + 高频小日志;4MB 适合机械盘或超高密度日志,但需搭配
posix_fadvise(fd, 0, 0, POSIX_FADV_DONTNEED)清 page cache
最容易被忽略的点是:崩溃一致性不靠 buffer 大小,而靠 memory_order_release 写入后、memory_order_acquire 切换前的内存屏障,以及 fdatasync() + 元数据文件的组合保障。少一个环节,最后 500ms 日志就可能永远消失。


















