在大屏或电脑上体验更佳——动画和操作区域更宽敞。
输入

文件系统

文件系统把 /home/user/notes.txt 这样的路径解析成文件数据。本课的 inode 模型把内容放在数据块、元数据放在 inode,而名字放在父目录的数据中。文件名不存于文件自己的 inode。

像寄存处的登记簿写着 notes.txt→7,物品挂在 7 号位置;物品记录保存大小、引用数量和内容位置,名字留在登记簿。同一物品可以有多张领取凭据,这对应多个名字指向同一 inode。

文件系统如何工作

画布采用 OSTEP 的教学文件系统 vsfs 思路,共十三个 4 KB 块。块 0 是描述布局的超级块;块 1、2 分别是 inode 位图与数据位图;块 3、4 是 inode 表,每块四个,共八个 inode;块 5–12 是数据区 D0–D7。

inode 保存类型(文件或目录)、字节大小、链接数与内容块编号。真实 inode 还可有权限、所有者与时间戳,具体字段因文件系统而异。目录同样使用 inode 和数据块,但类型及操作规则特殊,数据包含 name→inode number 条目。

根 inode=i0,数据在 D0,其中有 home→i3、readme.txt→i2。解析路径从根开始,每个分量先读取当前目录 inode 找数据块,再读目录数据找到下个 inode,最后再读目标 inode。

本课每个目录只用一块、每次请求不缓存,路径深度 d 的 open 计 2d+1 次模型块读。块内匹配仍有 CPU 工作,只是不再增加模型磁盘读。此公式不依赖目标内容大小,打开文件尚未读取内容;它也不是所有真实文件系统的物理 I/O 定律。

逐步演示

默认执行 open("/home/user/notes.txt")、read、unlink。根数据在 D0,home 在 D1,user 在 D3,文件内容在 D4、D5,路径连续并不要求块连续。

  1. 读 1、2:读取 i0 得到 D0,再读 D0,找到 home→i3。尚未读取目标内容。
  2. 读 3、4:读取 i3 得到 D1,再读 D1,找到 user→i5。
  3. 读 5、6:读取 i5 与 D3,找到 notes.txt→i7。
  4. 读 7:i7 是文件,大小 8192 字节,链接数 1,指向 D4、D5。inode 中没有文件名。open 返回 fd 3,模型中它指向 i7,此时 your data: 0 B。
  5. 读 8、9:read(fd 3) 已有 inode,不再解析路径,按指针读 D4、D5,取得 8192 字节。
  6. unlink 要修改的是父目录,模型重走整条路径,计读 10–15,停在 user 的目录表 D3,而不是文件本身。随后按以下事件更新。

删除写入顺序:

  1. 写 D3,移除 notes.txt→i7,目录中已看不到名字,inode 与数据块暂未改变。
  2. 读 i7,这是第 16 次读取。
  3. 写 i7,把链接数 1 改成 0。
  4. 写数据位图清 D4、D5,再写 inode 位图清 i7,共两次位图写。位图自挂载起常驻内存,所以不需要先读。

最终 16 读、4 写,二十次模型 I/O 中两次读取目标内容。此模型未维护打开文件引用,所以链接数归零后立即回收;真实系统中前面的 fd 若仍打开,文件通常还必须保留,这一点不能按动画推断。

D4、D5 的位图已空闲,文本仍在画面。清位图不等于覆盖数据块,后续分配才可能重用它。真实介质、TRIM、加密和覆盖策略会影响恢复可能性,“删除名字”不等于“可靠擦除字节”。

rm 移除什么

Two names 中的 link("/home/user/notes.txt", "backup.txt") 在模型里把新名放到同一 D3。先七次读找到 i7,再两次写:D3 增 backup.txt→i7,inode 链接数 1→2。不读写或复制内容块,只增加目录项和引用数。真实相对路径的解释还取决于调用进程的工作目录。

随后 unlink notes.txt,从 D3 移除该名,链接数 2→1,不写位图、不释放内容。通过 backup.txt 再打开,会找到同一 i7、D4、D5 和相同 8192 字节。颜色属于 inode,所以两个名同色。

真实系统还有内存中的打开引用。大日志被 rm 后,即使链接数为零、原目录项消失,进程仍打开它就可能继续占空间;关闭最后相关引用或进程退出后才可回收。该计数不是本模型的磁盘链接数,其他内核引用也会影响具体生命周期。

边界情况

  • ENOENT:open("/home/guest/diary.txt") 解析 home 后,在 D1 找不到 guest,四次模型读后停止,已花的读不退回。open 只返回 ENOENT,不说明缺的是哪个分量;画布上的红色标记是模型标出的停止位置。真实重复失败也可能被负目录项缓存,不能直接按路径数乘磁盘读。
  • 打开目录:自定义 /home/user 可得到 i5 描述符,其数据是目录条目;接着 read 返回 EISDIR。Linux 可 open 目录、用 fstat 读元数据,目录条目经 readdir 以解析后的形式取得,而不是用 read 读原始块。
  • 路径中间是文件:自定义 /readme.txt/x,读到 i2 发现是文件而非目录,返回 ENOTDIR,已花三次读。
  • Append one block:增加 4 KB,模型有三次写——数据位图标分配、新块写内容、inode 更新指针和大小。元数据与数据顺序需要崩溃一致性协议,画布没有日志保证。
  • 路径操作会重走,本模型 read(fd) 则使用已解析 inode。真实 dentry、inode、页缓存可能让热路径 open 不触磁盘。
  • Shallow path 的 /readme.txt 深度一,open 三次读,默认深度三为七次。差异是目录层数,不是内容大小;真实成本还取决于缓存和目录索引。

常见错误

  • 把名字当 inode 的字段。名字属于目录,同一 inode 可多名,也可暂时无名而仍被引用。同文件系统内 rename 通常只改命名空间元数据,无需复制大文件内容;具体写几块取决于文件系统与目标状态。
  • 认为 rm 擦除全部数据。它移除条目并减少引用,介质清除是另一个问题,SSD 的映射与磨损均衡尤其不能由简单覆盖模型保证。
  • 把硬链接当副本或符号链接。硬链接共用 inode、内容、大小与权限,没有“原名”身份,通常不能跨文件系统;符号链接是保存路径的独立对象,可跨文件系统,也可悬空。
  • 从一次 ls 推断所有占用。它展示当前目录名,其他名字或打开引用仍可能保留文件。
  • 只看空闲字节。本模型及某些文件系统的 inode 配额可独立耗尽;其他文件系统动态分配记录,也可能有其他元数据限制。适用时用 df -i 看 inode,用普通 df 看字节。
  • 认为路径层次没有代价。本课每层两次读,真实热缓存可隐藏成本,冷层次、网络文件系统和大量路径查找仍应测量。

模型范围

十三块、八 inode,每 inode 四个直接指针,因此文件最多四块。真实系统可用间接块或 extent,例如 ext2/ext3 的间接寻址、ext4/XFS/APFS 的 extent 类结构。根编号本课为 0,ext 系列为 2。

目录在这里线性扫描;大目录可用索引,例如 ext4 的 dir_index 哈希树,不能用逐条扫描公式替代所有实现。除两张位图常驻内存外,没有 dentry、inode 或数据缓存,也无权限、时间戳、所有者、日志和内存打开文件表。unlink 的分步写入遇断电可能留下不一致,崩溃恢复需单独协议,本课不声称该顺序已保证耐久一致性。

可在测试文件上用这些命令观察元数据与硬链接:

stat notes.txt          # inode number, size, link count, block count
ls -i                   # the name → inode mapping, straight from the directory
ln notes.txt backup.txt # watch the link count go to 2, with no data copied
df -i                   # inodes used and free, separately from bytes

Linux 的 strace -e trace=openat,read ./your-program 显示系统调用,不展开每次内核目录查找或物理读盘;filefrag -v 可查看支持文件系统的 extent。macOS 的 stat -f '%i %l %z' 显示 inode、链接数、大小,fs_usage -f filesys 可观察文件系统操作。创建硬链接后链接数 1→2,数据块不复制;du 的硬链接计数规则和元数据分配会影响所见统计。