MIT 6.1810 · 第 17 课 · rtm

文件系统

文件系统是操作系统中最重要的抽象之一。本节课探讨文件系统的设计、实现与性能优化,以 xv6 的文件系统为案例进行深入分析。

授课教师:rtm (Robert T. Morris)

年份:2025 秋季

难度:中等

视频:Lecture 17 Notes

为什么需要文件系统?

文件系统提供了以下核心功能:

核心功能

为什么文件系统很有趣?

Unix/POSIX/Linux/xv6 文件系统 API

fd = open("x/y", -);
write(fd, "abc", 3);
link("x/y", "x/z");  -- 现在文件有两个名字!
unlink("x/y");
write(fd, "def", 3);
close(fd);
// 文件 y/z 包含 abcdef

API 中的高层设计选择

选择Unix 决策其他可能
对象文件原始磁盘空间、数据库记录
内容字节数组行/列、BTree
大小可随时追加预分配
命名人类可读对象 ID
组织目录层次结构扁平命名空间
同步无(无锁、无版本)事务、锁

API 对实现的影响

fd 指向的是什么?

硬链接的含义:

xv6 文件系统架构

软件层次

系统调用层
    ↓
名称 | 文件描述符 (FDs)
    ↓
inode 层
    ↓
inode 缓存
    ↓
日志 (log)
    ↓
块缓存 (buffer cache)
    ↓
virtio_disk 驱动

磁盘类型

类型特点随机访问顺序访问
机械硬盘 (HDD) 大容量、低成本、慢 5-10ms(寻道+旋转) ~100 MB/s
固态硬盘 (SSD) 更快、更贵、复杂 ~100 μs ~500 MB/s

对文件系统设计的影响

xv6 磁盘布局

块号    0       1       2       33      46      47+
内容   未用   超级块  日志    inode   位图    数据块
               (31块)  (数组)  (使用表)  (文件/目录内容)

磁盘上的 inode

// kernel/fs.h
struct dinode {
  short type;           // 文件类型(空闲、文件、目录、设备)
  short nlink;          // 链接数
  uint size;            // 文件大小
  uint addrs[12+1];     // 直接块 + 1 个间接块指针
};

直接块与间接块

示例:如何找到文件第 8000 字节的磁盘块号?

逻辑块号 = 8000 / BSIZE (= 1024) = 7
inode.addrs[7] 存储了磁盘块号

目录实现

// 目录项结构 (dirent)
struct dirent {
  ushort inum;          // inode 号
  char name[14];        // 文件名(最多 14 字节)
};

// 空闲目录项的 inum == 0

xv6 实际操作示例

创建文件

$ echo > x
bwrite 34 ialloc   // 分配 inode
bwrite 34 iupdate  // 更新 inode(设置 nlink)
bwrite 47 writei   // 写入目录项
bwrite 33 iupdate  // 更新目录 inode
bwrite 34 iupdate  // itrunc 新 inode

写入数据

$ echo a > x
bwrite 46 balloc   // 在位图中分配一个块
bwrite 914 bzero   // 清零分配的块
bwrite 914 writei  // 写入数据
bwrite 34 iupdate  // 更新 inode(长度和新块)

删除文件

$ rm x
bwrite 47 writei   // 删除目录项
bwrite 33 iupdate  // 更新目录 inode
bwrite 34 iupdate  // 减少链接数
bwrite 46 bfree    // 释放数据块
bwrite 34 iupdate  // itrunc:清零长度
bwrite 34 iupdate  // 标记 inode 为空闲

块缓存 (bio.c)

bget() 工作原理

  1. 检查块是否已在缓存中
  2. 如果是,获取锁并返回
  3. 如果否,重用现有缓冲区
  4. 保证内存中每个磁盘块只有一份副本

两层锁机制

缓存替换策略

prev ... head ... next
bget 重用 bcache.head.prev("尾部")
brelse 将块移到 bcache.head.next

这是一种近似的 LRU 策略。

路径名查找

路径名查找可能涉及多个磁盘块:

并发挑战:unlink 可能与 lookup 同时发生

解决方案 (namex):

要点总结

文件系统即数据结构

文件系统本质上是磁盘上的数据结构,包含树形目录结构和两个分配池(inode 和块)。

API 决定实现

Unix API 的每个选择(字节数组、目录层次、无同步)都深刻影响了文件系统的实现。

缓存至关重要

磁盘很慢,块缓存和 inode 缓存是性能的关键。

多磁盘写是原子性挑战

文件系统操作通常涉及多个磁盘写入,崩溃可能导致不一致——这是下节课的主题。

测验

Q1:xv6 文件系统的最大文件大小是多少?

Q2:为什么文件系统需要 inode?

Q3:块缓存的替换策略是什么?

延伸阅读