LabHub
学习 学习路径 课程

操作系统

文件系统 — 把名字和实体分开的设计

在 LabHub 中继续学习

一句话总结

Unix 文件系统把文件实体(inode)与名称(目录项)分离;这一决定解释了硬链接、删除已打开文件和原子替换等行为。

概念图: 元数据与内容由 inode 表示,名称放在目录中 · 直接 · 一级间接 · 硬链接

为什么需要它

文件包含内容和元数据(大小、权限、所有者、修改时间、数据块位置),此外还有名称。把三者绑定在一起虽简单,却让一个文件拥有两个名称或改名变得昂贵。

Unix 的选择是:元数据与内容由 inode 表示,名称放在目录中。目录也是一种特殊文件,其内容是“名称 → inode 编号”的列表。

工作原理

inode 保存文件大小、权限、时间戳、链接数和指向数据块的指针。前几个指针直接指向数据块,之后依次为指向指针块的一级间接、二级间接和三级间接。小文件只用直接指针,速度快;大文件也能表示,但层级越深,访问所需的数据块读取越多。

由此自然产生以下结果。

硬链接只是再创建一个指向同一 inode 的名称。没有原件与副本之分,二者完全平等,只是 inode 链接数加一。

删除文件实际是删除名称(因此系统调用叫 unlink)。只有链接数变为 0 且没有进程打开文件时,数据块才会回收。因此删除日志后磁盘空间可能仍未增加:进程还开着该文件,必须重启进程或关闭文件描述符。

**重命名(rename)**在同一文件系统内只修改目录项,因此是原子的。安全替换配置文件的标准做法由此而来:把新内容写入临时文件,用 fsync 落盘,再以 rename 替换。读取者只会看到完整的旧版或新版,绝不会看到写到一半的文件。

**日志(journaling)**用于应对崩溃。元数据变更在写入实际位置前先记入日志,断电后可在重启时重放,恢复一致性。ext4 默认的 ordered 模式只记录元数据;连数据也记录更安全,但每次写入都要执行两遍。

实际工作中的表现

df 显示仍有空间但写入失败,通常是 inode 耗尽(用 df -i 检查),或有人仍打开已删除文件,空间无法回收。后者可用 lsof +L1 找出链接数为 0 的打开文件。日志轮转删除旧文件后,应用仍向旧描述符写入,就是典型情况。

写入真的到达磁盘了吗

安全替换配置的步骤中包含 fsync。要理解它的必要性,必须了解写入经过的层次

程序调用 write 后,数据进入内核页缓存,调用立即成功返回,此时磁盘上还没有任何内容。 内核稍后才会写盘,期间断电则数据消失。fsync 的含义是“现在写下去,并等待完成”。

另一个常被忽略的问题是:同步文件内容并不能保证文件名安全。 新建文件、写入并同步后若断电,内容可能存在,但目录项尚未持久化,导致无法访问。因此新建文件或执行 rename 后,还必须同步所在目录。数据库与日志系统正是这样做的。

层次并不止于内核。磁盘也有写缓存,设备回答“已写入”时,数据仍可能在易失性缓冲区。可靠设备支持缓存刷新命令,文件系统会发送该命令;但一些廉价设备接到命令后什么也不做,却返回成功。 在这种设备上,任何软件都无法保证崩溃安全。

实际结论很简单:不能丢失的数据必须使用 fsync,并接受性能代价。 随时可重建的缓存和临时文件则不必使用。全部同步会拖垮性能,全部省略则会在断电时无法判断丢失了什么。

后续练习将做什么

亲手复现这些行为:通过 inode 判断是否为同一文件,继续读取已取消名称的文件,编写脚本查找删除后仍占用空间的进程;还要编写配置替换脚本,评分器会在持有打开文件时触发替换,检查读取者是否会看到半写入文件。