外观
文件系统:从字节偏移到磁盘块,再到崩溃恢复
一次 write 穿过哪些层
fd 是进程局部整数;struct file 记录打开方式与偏移;inode 描述文件身份、长度和磁盘块地址;目录把名称映射到 inode 编号。write(fd, buf, n) 先找到 file,再根据类型进入管道、设备或 inode 写路径。普通文件最终需要把字节偏移转换成文件逻辑块号,再转换成实际磁盘块号。
flowchart TD A[fd] --> B[struct file 与 offset] B --> C[inode 与字节长度] C --> D[bmap 逻辑块到磁盘块] D --> E[buffer cache] E --> F[log 事务与提交] F --> G[设备驱动与磁盘]
查看流程图文本
flowchart TD A[fd] --> B[struct file 与 offset] B --> C[inode 与字节长度] C --> D[bmap 逻辑块到磁盘块] D --> E[buffer cache] E --> F[log 事务与提交] F --> G[设备驱动与磁盘]
以 1KiB 块为例,文件偏移 2500 对应逻辑块号 2,块内偏移 452。如果本次写 1000 字节,会跨过两个磁盘块。页大小 4096、文件系统块大小 1024,二者不是同一单位;mmap 写回一页时就可能触及多个文件系统块。
inode 和间接索引
基础 xv6 的 inode 有 12 个直接地址和 1 个单级间接地址。一个 1024 字节间接块存放 256 个 4 字节块号,所以文件最多有
2025 fs 实验保持磁盘 inode 大小不变,把 1 个直接地址改为双级间接入口,容量成为 q/256,内层 q%256。
bmap 负责找到或建立映射,itrunc 必须按反方向释放资源:先数据块,再下级索引块,最后上级索引块。只把 inode.size 清零会造成泄漏;只释放数据块而忘记索引块,在反复创建删除大文件时同样耗尽磁盘。
buffer cache 的两种保护
缓存要保证同一设备、同一磁盘块只对应一个缓存身份,否则两个线程可能各自修改不同副本,最后互相覆盖。缓存层锁保护查找、引用与身份分配;每个 buffer 的 sleeplock 保护其数据内容,允许等待磁盘时让出 CPU。这解释了为何不能用“一把锁全部包住”粗暴替代各层协议。
每次 bread 都要找到相应 brelse,但两者之间可能有多个失败分支。读代码时画资源表比记函数名称更有效:取得哪个 buffer,是否持有 inode 锁,是否处于事务,哪条分支负责释放。实验中常见的死锁,就是错误路径漏掉一次释放。
日志解决的是崩溃时的部分更新
创建文件可能同时修改 inode、目录项和空闲位图。如果断电发生在其中一步,重启后可能出现目录指向未初始化 inode,或已经分配的块仍标为空闲。日志把一组修改放进可恢复的事务。
用简化的提交过程理解:先把新块内容写入日志区;再写提交记录;然后把日志内容安装到原位置;最后清除日志记录。如果在提交记录之前崩溃,恢复忽略未提交内容;之后崩溃则重新安装已提交内容。重新执行安装必须是幂等的,因此重复恢复不会越修越坏。
这不意味着每个应用层 write 都是完整事务,也不意味着缓存中的修改已立刻持久化。xv6 还允许分组提交,受日志容量限制,较大写入需要拆分。分析持久化语义时必须明确“哪个边界算提交”,不要把内存复制完成当作磁盘提交完成。
硬链接与符号链接
硬链接是另一个名称指向同一 inode,文件身份不变;符号链接有自己的 inode,内容是目标路径。目标删除后,硬链接仍然引用原文件;符号链接仍保存字符串,但随后 open 可能失败。
符号链接解析可能形成 a→b→a 环,必须限制跟随或检测循环。2025 实验只要求 open 跟随,link/unlink 操作链接本身,并用 O_NOFOLLOW 控制不跟随。它不是完整 POSIX 路径解析器,因此学习时应把实验承诺和生产实现区分开。
mmap 把文件身份与地址空间连起来
mmap 先登记 VMA,缺页时按文件偏移读入数据。即使用户 close 原 fd,VMA 仍应持有 file 引用,否则下一次缺页会使用已释放对象。MAP_PRIVATE 的修改不写回文件;MAP_SHARED 的修改按实验要求在 munmap 或 exit 时写回。实际生产系统的共享可见性更复杂,实验允许不同进程各有物理副本。
自测:为什么目录项消失后,文件内容可能仍存在?
文件可能还有硬链接,也可能仍被打开的 file 对象引用。名称删除、引用计数归零和最终磁盘回收分别发生,不能把 unlink 简化为立即销毁所有内容。
自测:写双级索引块后为什么要经过日志路径?
索引块也是持久化元数据。若数据块已分配而索引指针没有可靠提交,重启可能丢失可达性;反过来指针提交但分配信息不一致,也会破坏文件系统。元数据更新必须加入与既有实现一致的事务协议。