# 第 40 章实验:在真实的 ext4 上观察文件系统实现 对应页面:`chapters/40-fs-impl.html`。三个小程序把本章概念(inode、数据块分配、目录也是文件、extent、页缓存与写缓冲)落到你正在用的 Linux 文件系统上。 ## 编译与运行 ```bash cd labs/40-fs-impl make ./sparse # inode 的 size 与 blocks;目录随条目增多而变大 ./fiemap # 自造一个带"洞"的文件,打印它的 extent 映射 ./fiemap 某个文件 # 也可以看任意已有文件 ./cache # fsync 的代价、读缓存、O_DIRECT make clean # 删除可执行文件和残留的临时文件 ``` 所有程序都在**当前目录**下创建临时文件并在结束时删除。请在 Linux 本地文件系统(WSL2 的 `~` 下是 ext4)里运行;在 `/mnt/c/...`(Windows 盘)上 `fiemap` 会失败、其他数字也没有意义。 ## 1. sparse.c —— size 与 blocks 是两回事 - `stat()` 返回的 `st_size` 是文件"逻辑上"有多长,`st_blocks` 是**真正分配**了多少个 512 字节单位。 - 新建的空文件 `blocks=0`:`creat` 只分配 inode,不分配数据块(和页面上 vsfs 模拟器里的 `[f a:-1 r:1]` 一样)。 - 写 1 字节就占一整块(4KB = 8 × 512)。 - `lseek` 跳到 48KB、1GB 处各写 1 字节:`size` 变成 1GB 多,`blocks` 只有 24(12KB)。中间的"洞"没有分配任何块,读洞得到的是 0。 - 第二部分往一个目录里创建 400 个文件:目录的 `size` 从 4KB 长到 36KB——**目录也是文件**,目录项多了就要分配更多数据块。删光文件后目录并不缩小:被删的目录项只是标记为空闲(对应页面上"目录数据块"模拟器里的空洞)。ext4 的目录超过一块后会启用 htree 索引,所以增长不是严格线性的。 ## 2. fiemap.c —— 看见 extent - 用 `ioctl(fd, FS_IOC_FIEMAP, ...)` 取得文件的"逻辑块 → 物理块"映射(`filefrag -v` 用的也是它)。 - 测试文件:先连续写 1MB(逻辑块 0–255),跳过一个洞,再在逻辑块 1024 处写 64KB。 - 你会看到只有 **2 个 extent**:`(逻辑 0, 物理 X, 长度 256)` 和 `(逻辑 1024, 物理 Y, 长度 16)`。ext4 不像 ext2 那样为每个块存一个指针,而是用"起始块 + 长度"描述一段连续空间——这就是页面上"提示:考虑基于 extent 的方法"讲的东西。洞不出现在映射里。 - 物理块号每次运行都不同,取决于当时的空闲空间。 ## 3. cache.c —— 写缓冲与读缓存 - **200 次 4KB 追加写**:不 fsync 时每次只要几微秒(只是拷贝进页缓存);每次都 fsync 时每次要毫秒级(要等日志提交、等设备确认)。差距通常是几百倍——这就是数据库每次提交都 fsync 的代价,也是文件系统默认把写缓冲 5~30 秒的原因。 - **写 64MB 再 fsync**:`write()` 循环很快返回,真正的设备写发生在 `fsync()`(或稍后的后台刷盘)里。 - **读缓存**:先用 `posix_fadvise(POSIX_FADV_DONTNEED)` 请内核把该文件的干净页踢出页缓存,读一次;再读一次(命中页缓存);最后用 `O_DIRECT` 绕过页缓存读。 - 注意 WSL2:Linux 看到的"磁盘"是 Windows 上的一个虚拟磁盘文件(VHDX),背后还有 Windows 主机的缓存和 SSD,所以"冷读"也可能很快、差距没有物理机械硬盘那么夸张。多跑几次看看波动;如果你有真实 Linux 机器和 HDD,差距会大得多。 ## 思考题 1. 把 `sparse.c` 里的 `lseek` 目标改成 4GB、16GB,`blocks` 会变吗?为什么 `ls -l` 和 `du` 显示的大小不同? 2. 对一个用 `cp` 复制的大文件跑 `./fiemap 文件名`,extent 数是多少?再对一个一边写一边被别的程序写的日志文件跑一次,比较碎片程度。 3. 在 `cache.c` 的小写测试里,把 `fsync` 换成 `fdatasync`,时间会变吗?(提示:fdatasync 在只追加数据时仍需更新文件大小这个元数据。)