双缓冲本身不提升吞吐量,真正起效的是业务线程零等待、批量write()和绕过page cache干扰;盲目堆buffer大小或硬上io_uring易丢日志、卡死、触发-EINVAL。

双缓冲本身不提升吞吐量,真正起效的是让业务线程零等待 + 批量 write() + 绕过 page cache 干扰;盲目堆 buffer 大小或硬上 io_uring 反而容易丢日志、卡死、触发 -EINVAL。
std::atomic 直接交换指针为什么会导致日志撕裂
只用 std::atomic<char></char> 存当前 buffer 地址,业务线程往里 memcpy 时没同步“写入完成”信号。一旦 flush 线程在你拷贝到一半时执行 exchange(),它就读到跨字段、未对齐甚至截断的 log_entry —— 轻则日志错乱,重则进程因读越界崩溃。
- 必须搭配
std::atomic<size_t></size_t>记录已用长度,每次填满一条完整日志后用memory_order_release更新 - 切换前检查
used_len.load()是否对齐到struct log_entry边界,不能停在level和msg中间 - 更稳妥的做法是弃用单原子指针,改用三状态机:
READY_TO_WRITE→SWITCHING→READY_TO_FLUSH,靠compare_exchange_weak跃迁
posix_memalign 分配 buffer 时踩的坑
write() 第一次访问某段内存可能触发缺页异常,尤其在 16MB 大 buffer 场景下,延迟从微秒级跳到毫秒级,完全不可控。
- 必须用
posix_memalign(&buf_a_, 4096, size_)分配,禁止用new char[size_]或std::vector<char></char> - 分配后立即调用
mlock(buf_a_, size_)锁定物理页(需CAP_IPC_LOCK权限),失败则退化为memset(buf_a_, 0, size_)强制首次映射 -
write()调用时传入的buf指针必须是posix_memalign返回的原始地址,严禁偏移或指针算术运算,否则内核返回-EINVAL
O_SYNC/O_DSYNC 必须禁用,但 fdatasync() 不能少
加了 O_SYNC 或 O_DSYNC 就等于废掉整个双缓冲设计:每次 write() 都得等磁盘确认,flush 线程变成串行瓶颈,吞吐归零。
立即学习“C++免费学习笔记(深入)”;
- 打开日志文件时用
O_WRONLY | O_CREAT | O_APPEND | O_CLOEXEC,绝对不加O_SYNC、O_DSYNC、O_DIRECT -
fdatasync()只在三个时机调用:buffer 满、空闲超 1s、显式flush()请求 - 优先用
writev()合并多个log_entry一次性提交,比逐条write()快 3–5 倍;避免拼接字符串,直接二进制直写
后台 flush 线程该用 write() 还是 io_uring
除非你已在 kernel ≥ 6.0 环境下全链路适配 io_uring(包括 O_DIRECT、地址对齐、sqe 生命周期管理),否则别为日志单独引入它——错误码分散、completion 乱序难定位、fallback 逻辑难覆盖,实测吞吐并不比优化后的 write() 高。
- 推荐方案:专用 flush 线程 +
writev()+fdatasync()分离 - 每满 4KB 或空闲超 1s 触发
fdatasync(),避免每次write()都等磁盘确认 - 务必检查
std::thread析构前是否已join()或detach(),否则析构时调用std::terminate静默崩溃
最易被忽略的点是:buffer 切换时没做结构体对齐校验,导致日志项跨 cache line 或字段撕裂;还有就是 write() 传入的指针不是 posix_memalign 的原始地址,内核直接拒绝。


















