std::ofstream + flush() 无法防止断电丢日志,因其仅将数据刷至操作系统页缓存(page cache),未调用fsync()/fdatasync()强制落盘,而页缓存中数据在断电时必然丢失。

强行断电或进程崩溃时,内存中的日志必然丢失——这是硬件和操作系统层面的硬限制,任何纯用户态异步日志系统都无法绕过。能做的,是把“丢失窗口”压缩到最小,并在可恢复前提下尽可能落盘。
为什么 std::ofstream + std::flush() 不能解决断电丢日志问题
常见误区是以为调用 flush() 就等于数据写到了磁盘。实际上它只保证数据从 C++ stream buffer 刷到 OS 的 page cache,不触发 fsync() 或 fdatasync(),断电时 page cache 中的数据会彻底消失。
-
std::ofstream::flush()不等价于fsync(),也不保证数据到达磁盘介质 - 即使设置了
std::ios::unitbuf,也只是对每个插入操作自动flush(),仍卡在 page cache 层 - Linux 默认挂载选项(如
data=ordered)只保证文件系统一致性,不保证你的日志内容已落盘
真正起作用的是 fsync() / fdatasync() + 合理的刷盘策略
只有显式调用 fsync()(或更轻量的 fdatasync())才能强制 OS 把 page cache 中的脏页写入磁盘并等待完成。但频繁调用会严重拖慢性能,必须权衡。
- 每条日志后都
fsync():安全但吞吐可能跌两个数量级,不现实 - 固定时间间隔刷盘(如 100ms):需配合定时器+条件变量,且 crash 窗口就是该间隔
- 按缓冲区大小刷盘(如满 4KB):更可控,但要注意
write()返回值是否写满,避免部分写 - 关键日志(如 ERROR、FATAL)可单独标记并立即
fdatasync(),普通 INFO 等批量刷
崩溃恢复:用预写日志(WAL)或双缓冲 + 校验头绕过“写一半”问题
即使做了 fsync(),断电仍可能发生在 write 中间,导致日志文件末尾损坏(比如只写入半个结构体)。直接追加写不可靠,需要元数据保护。
立即学习“C++免费学习笔记(深入)”;
- 在每条日志前加固定长度头(如 8 字节),含长度 + CRC32,读取时跳过校验失败的记录
- 用环形缓冲区 + 单独的“提交指针文件”,先
write()日志块,再write()+fsync()指针文件,重启时以指针为准回放 - 更稳妥的做法是 WAL 模式:所有日志先写入一个
log.wal(带序列号和校验),再异步 commit 到主日志文件;crash 后重放未 commit 的 WAL 条目 - 避免 mmap 写日志文件——断电时 mmap 脏页同样丢失,且无可靠方式判断哪些页已刷盘
最常被忽略的一点:fsync() 成功只代表数据到了磁盘缓存(disk cache),而消费级 SSD/硬盘的掉电保护(PLP)并不普及。真要对抗断电,得依赖带电容的服务器级 SSD,或者接受“最后几毫秒日志注定丢失”的事实——工程上只能逼近,无法消灭。


















