Files
baim0-learn/linux/02 断电文件0填充-iomap读路径填零机制.md
T
2026-09-16 18:13:56 +08:00

7.2 KiB
Raw Blame History

title, date, tags, categories
title date tags categories
断电后文件内容变为 0 填充的分析(iomap 读路径填零机制) 2026-09-16
linux
linux

0. 总结(TL;DR)

断电后"文件变 0 填充" = 内存脏页丢失 + 日志恢复把未提交范围回滚为 hole/unwritten + iomap 读路径按 POSIX 语义对该范围填 0(不发磁盘 IO)。属预期内的数据丢失行为,不是篡改、也不是磁盘被写 0。

  • 填 0 区间 = 故障前已 write 未 fsync 的窗口,按页/块(4K)对齐;
  • 早已 fsync 的旧数据完好,其他文件完好;
  • "读"只填内存 page cache,盘上块不会被覆写;unwritten 情形下盘上甚至可能残留真实数据,但读路径"依法"返回 0;
  • 根治手段:关键数据写后 fsync()/fdatasync()(或 O_SYNC/O_DSYNC),硬件层配 BBU/UPS。

(以下为完整版分析)


1. 问题现象

  • 物理机故障(断电/异常掉电重启)后,部分保存的文件内容整体或某区间变为全 0,疑似"被篡改"。
  • 典型特征:
    • 填 0 区间与故障前最近一次写入窗口吻合(新写、未 fsync 的数据);
    • 同一文件中早已 fsync 落盘的旧数据完好,其他文件也完好;
    • 文件大小、mtime 看起来正常,给人"文件还在、内容被替换"的错觉;
    • 0 区间边界通常按页/块(4K)对齐,干净整齐,无 IO 错误、坏道伴随。

2. 结论(本质)

不是篡改,也不是磁盘被写 0,而是两个机制叠加的预期行为:

  1. 页缓存易失丢失:数据写入后只存在于 page cache(dirty page),writeback 未完成即断电,内存数据丢失;
  2. iomap 读路径主动填零:重启后日志(journal)恢复,未提交落盘的范围在磁盘元数据中回滚为 HOLE 或保持 UNWRITTEN;读取时内核按 POSIX 语义(读 hole/unwritten 必须返回 0)在内存中直接填 0 返回,根本不发起磁盘 IO。

即:page cache 内有数据、落盘过程中断电 → 读回时看到 0 填充。

3. 代码依据(kernel iomap,fs/iomap/buffered-io.c + include/linux/iomap.h)

版本说明:文中 5.x 内核的函数名基于 struct page(iomap_readpage_iter / zero_user);6.x 内核已完成 folio 化,对应 iomap_read_folio_iter / folio_zero_range,判定与填零逻辑完全一致,下文以 5.x 名称叙述。

3.1 映射类型(include/linux/iomap.h)

/*
 * Types of block ranges for iomap mappings:
 */
#define IOMAP_HOLE	0x01	/* no blocks allocated, need allocation */
#define IOMAP_DELALLOC	0x02	/* delayed allocation blocks */
#define IOMAP_MAPPED	0x03	/* blocks allocated at @addr */
#define IOMAP_UNWRITTEN	0x04	/* blocks allocated at @addr in unwritten state */
#define IOMAP_INLINE	0x05	/* data inline in the inode */

注:取值是 0x01–0x05,0 被刻意保留不用,避免零初始化的内存被误判为合法映射类型。

3.2 读路径关键分支(iomap_readpage_iter)

if (iomap_block_needs_zeroing(iter, pos)) {
	zero_user(page, poff, plen);              /* 页内填 0 */
	iomap_set_range_uptodate(page, poff, plen); /* 标记 uptodate */
	goto done;                                 /* 不发 bio,不读盘 */
}

3.3 填零判定条件(iomap_block_needs_zeroing)

return srcmap->type != IOMAP_MAPPED ||   /* HOLE / DELALLOC / UNWRITTEN 均命中 */
	(srcmap->flags & IOMAP_F_NEW) ||   /* 新分配块,无有效数据 */
	pos >= i_size_read(iter->inode);   /* 超出 EOF 的部分 */

解读:只要某范围的映射不是"已映射且含已提交有效数据",读路径就不碰磁盘,直接在 page cache 填 0 并标记 uptodate 返回给应用。填零只发生在内存层,磁盘上的块不会被这次读覆盖(raw 块里甚至可能残留旧数据,但 VFS 不再读它)。

补充两点严谨性说明:

  • IOMAP_INLINE(数据内联在 inode 中)在进入本判定之前已由 iomap_read_inline_data 单独处理,不会走到填零分支;
  • 适用范围:iomap 缓冲读路径主要用于 XFS / gfs2 / zonefs / erofs 等文件系统;ext4 的缓冲读走 mpage 而非 iomap(仅 DIO 用 iomap),但 ext4 对 unwritten extent 的读同样直接返回 0,现象与机理等价。下文时间线以 XFS 为例。

4. 故障完整时间线

  1. 应用 write():数据进入 page cache 并标脏;文件系统采用延迟分配(DELALLOC)或预分配的 unwritten extent(UNWRITTEN),盘上此时无有效数据或数据状态为"未写"。
  2. writeback 尚未把这些脏页写盘,或 bio 已下发但未完成/未提交(含 unwritten→written 的元数据转换未落日志)。
  3. 断电:page cache 脏页随掉电丢失;无掉电保护的磁盘/RAID 卡易失缓存同样丢失。
  4. 重启后文件系统日志恢复:只恢复已提交的元数据;未提交的分配回滚为 HOLE,或保持 UNWRITTEN;而 i_size 可能已随更早的提交持久化 → 出现"文件有长度、无内容"。
  5. 应用读取:iomap 查到该范围为 HOLE/UNWRITTEN(或 F_NEW)→ iomap_block_needs_zeroing 为真 → zero_user 填 0 → 应用读到全 0。

两种落盘子情形(都表现为 0)

  • 情形 A(delalloc):块从未分配 → 恢复后为 HOLE → 读填 0;
  • 情形 B(unwritten):数据 bio 甚至已写到盘,但 unwritten→written 的状态转换未提交日志 → 恢复后仍为 UNWRITTEN → 读填 0。此时盘上块里可能躺着真实数据,但读路径依法返回 0——这是"落盘过程中断电也填 0"的最直接解释。

5. 为什么看起来像"被篡改"

  • 0 是整段瞬间出现、4K 对齐,与人为覆写或介质故障的形态不同;
  • 文件长度/mtime 正常,只有内容变 0;
  • 实质是"数据从未(或未完成)提交到盘" + "内核对无数据范围的合法返回值是 0"。

6. 排查与认定要点

  • 对照故障时间点与应用写入日志:填 0 区间 = 故障前已 write 未 fsync 的窗口;
  • dmesg / 文件系统日志中有 unclean shutdown / log recovery 记录(如 XFS log recovery);
  • 已 fsync 的旧数据完好 → 排除盘介质故障与人为篡改;
  • 需要实证时可离线比对底层 raw 块:UNWRITTEN 情形下 raw 块可能残留旧数据,而文件读回为 0,恰好印证"读路径填零、盘未被覆写";
  • 盘固件/介质故障通常伴随 IO error、坏道、随机花数据,而非整齐全 0。

7. 规避建议

  • 应用层(根本手段):关键数据写完后 fsync()/fdatasync()(或 O_SYNC/O_DSYNC),确认落盘再返回成功;事务性数据用 WAL + 校验和,重启后自检修复;
  • 认知对齐:write() 返回 ≠ 数据落盘;延迟分配窗口内数据只在内存;
  • 硬件层:RAID 卡/磁盘易失缓存配 BBU/掉电保护,或关闭回写缓存;UPS 避免直接掉电;
  • 极端场景可评估 sync 挂载或应用层双写/校验,代价是性能。

8. 一句话总结

断电后"文件变 0 填充" = 内存脏页丢失 + 日志恢复把未提交范围回滚为 hole/unwritten + iomap 读路径按 POSIX 对该范围 zero_user 填 0(不发磁盘 IO);属预期数据丢失行为而非篡改,根治靠 fsync 等落盘确认手段。