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

122 lines
7.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 断电后文件内容变为 0 填充的分析(iomap 读路径填零机制)
date: 2026-09-16
tags:
- linux
categories:
- 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)
```c
/*
* 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)
```c
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)
```c
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 等落盘确认手段。