背景

硬件视角:“持久化” 的层层抽象

  • 物理层 1-bit 的存储
  • 设备层 I/O 设备 (寄存器)
  • 驱动层 (可读/写/控制的对象)

用户 (应用程序) 视角:对象 + API

  • C:\Program Files\...
  • /etc/apt/sources.list
  • /bin/bash...

中间的部分:文件系统

本次课内容与目标

理解 “文件系统” 的设计

  • “文件系统” 的需求分析
    • 需要什么对象
    • 提供什么 API

为什么需要文件系统?

为什么需要文件系统?

我都有吃饱穿暖了,还要什么计算机?

  • 计算机:辅助人类更好地完成物理世界中的任务
    • 开机以后,必须得有点 “代表物理世界” 的东西

“开机” 后操作系统中应该有的对象

  • 各类数据
    • 《操作系统》点名册/成绩单/Online Judge 结果/...
    • 处理这些数据的程序
      • wc, grep, vim, LibreOffice, ...
      • libc, X11, gnome-settings-manager, ...
  • 这些 “对象” 应该被持久地保存在介质上

文件系统的诞生

没问题!我们已经讲过 1-bit 的存储了

  • 但让应用程序直接通过驱动访问存储设备 (1950s)?

今天的系统中有不止一个程序

  • 每个程序还需要考虑各种访问权限、并发控制……
  • 程序出 bug 了,不小心弄坏了整块磁盘

文件系统:设计目标

  1. 提供合理的 API 使多个应用程序能共享数据
  2. 提供一定的隔离,使恶意/出错程序的伤害不能任意扩大
    • 这就是文件系统
      • 你会怎么办?

文件系统:存储设备的虚拟化

磁盘 (I/O 设备) = 一个可以读/写的字节序列

虚拟磁盘 (文件) = 一个可以读/写/的动态字节序列

  • std::vector<char>
  • 类比
    • 进程抽象:一个 CPU → 在时间上共享
    • 虚拟存储:一份内存 → 划分给多个虚拟地址空间
    • 文件系统:一个物理磁盘 → 多个虚拟磁盘

文件系统 API: 虚拟磁盘管理

  • 需要解决的问题
    • 虚拟磁盘的命名、查找、权限
    • 虚拟磁盘的操作 (读写)

虚拟磁盘:命名管理

虚拟磁盘:命名管理

文件系统 = “虚拟磁盘名” 到 “虚拟磁盘对象” 的映射

  • 如果我有几百万个文件?

局部性来帮忙!

  • 目录:文件/目录的集合 (形成一棵树)
    • 逻辑相关的数据存放在相近的目录
.
└── 学习资料
    ├── .学习资料(隐藏)
    ├── 问题求解1
    ├── 问题求解2
    ├── 问题求解3
    └── 问题求解4

文件系统的 “根”

树总得有个根结点

  • Windows: 每个驱动器是一棵树
    • C:\ “C 盘根目录”
      • C:\Program Files\, C:\Windows, C:\Users, ...
    • D:\ “D 盘根目录”
      • D:\学习资料\
    • 优盘分配给新的盘符
      • 为什么没有 A:\, B:\?
      • 简单、粗暴、方便,但 game.iso 一度非常麻烦……
  • UNIX/Linux
    • 只有一个根 /。其他没有了
      • 第二个设备呢?优盘呢???

文件系统的挂载:一个有趣的设计

UNIX 允许任何一个目录都可以 “挂载” 一个设备代表的目录树

  • 非常灵活的设计;充分利用了目录的局部性
    • 我的 128G 优盘分成了两个 64G 分区 (Linux / 和 exFAT)
      • 可以直接使用 mount 命令 “挂载” 到一个目录上
        • 目录里原先的内容会暂时消失 (但不会丢失)
    • /, /home, /var 可以是独立的磁盘

如何 mount 一个文件?

  • disk-img.tar.gz 的挂载:创建一个 “loopback” 设备
    • lsblk 可以看到
  • 然后就变成挂载设备了
    • 可以 strace 一下,看看操作系统提供了哪些 API

Filesystem Hierarchy Standard (FHS)

This standard (FHS) enables:

  • Software to predict the location of installed files and directories, and
  • Users to predict the location of installed files and directories
    • 例如 macOS 虽然是 UNIX 的内核 (BSD), 但就不遵循 Linux FHS

目录 API (系统调用)

目录管理:创建/删除/遍历

这个简单

  • mkdir
    • 创建一个目录
    • 可以设置访问权限
  • rmdir
    • 删除一个空目录
    • 没有 “递归删除” 的系统调用
      • (应用层能实现的,就不要在操作系统层实现)
      • rm -rf 会遍历目录,逐个删除 (试试 strace)
  • getdents
    • 返回 count 个目录项 (ls, find, tree 都使用这个)
      • 以点开头的目录会被系统调用返回,只是 ls 没有显示

硬 (hard) 链接

需求:系统中可能有同一个运行库的多个版本

  • libc-2.27.so, libc-2.26.so, ...
  • 还需要一个 “当前版本的 libc”
    • 程序需要链接 “libc.so.6
    • 能否避免文件的一份拷贝?

硬连接:允许一个文件被多个目录引用

  • 目录中仅存储指向文件数据的指针
  • 链接目录 ❌
  • 跨文件系统 ❌

小知识:其实所有的文件都是硬连接 (ls -i 查看)

  • 删除的系统调用称为 “unlink” (引用计数)

软 (symbolic) 链接

软链接:在文件里存储一个 “跳转提示”

  • 软链接也是一个文件
    • 当引用这个文件时,去找另一个文件
    • 另一个文件的绝对/相对路径以文本形式存储在文件里
    • 可以跨文件系统、可以链接目录、……

ln -s 创建软链接

  • symlink 系统调用

软链接带来的麻烦

软链接可以随意创建 (当前可能不合法;但未来可能合法)

  • 操作系统在处理软链接时会执行路径解析
  • 意想不到的复杂性 😂

允许多次间接链接

  • a → b → c (递归解析)

可以创建软连接的硬链接 (因为软链接也是文件)

  • ls -i 可以看到

符号链接成环?

  • ln -s . a
  • 所有处理符号链接的程序 (tree, find, ...) 都要考虑递归的情况

进程的 “当前目录”

working/current directory

  • pwd 命令或 $PWD 环境变量可以查看
  • chdir 系统调用修改
    • 对应 shell 中的 cd
    • 注意 cd 是 shell 的内部命令
      • 不存在 /bin/cd

问题:线程是共享 working directory, 还是各自独立持有一个?

文件 API (系统调用)

复习:文件和文件描述符

文件:虚拟的磁盘

  • 磁盘是一个 “字节序列”
  • 支持读/写操作

文件描述符:进程访问文件 (操作系统对象) 的 “指针”

  • 通过 open/pipe 获得
  • 通过 close 释放
  • 通过 dup/dup2 复制
  • fork 时继承

复习:mmap

使用 open 打开一个文件后

  • MAP_SHARED 将文件映射到地址空间中
  • MAP_PRIVATE 创建一个 copy-on-write 的副本
void *mmap(void *addr, size_t length, int prot, int flags,
  int fd, off_t offset); // 映射 fd 的 offset 开始的 length 字节
int munmap(void *addr, size_t length);
int msync(void *addr, size_t length, int flags);

小问题:

  • 映射的长度超过文件大小会发生什么?
    • (RTFM, “Errors” section): SIGBUS...
      • bus error 的常见来源 (M5)
      • ftruncate 可以改变文件大小

文件访问的游标 (偏移量)

文件的读写自带 “游标”,这样就不用每次都指定文件读/写到哪里了

  • 方便了程序员顺序访问文件

例子

  • read(fd, buf, 512); - 第一个 512 字节
  • read(fd, buf, 512); - 第二个 512 字节
  • lseek(fd, -1, SEEK_END); - 最后一个字节
    • so far, so good

偏移量管理:没那么简单 (1)

mmap, lseek, ftruncate 互相交互的情况

  • 初始时文件大小为 0
    1. mmap (length = 2 MiB)
    2. lseek to 3 MiB (SEEK_SET)
    3. ftruncate to 1 MiB

在任何时刻,写入数据的行为是什么?

  • blog posts 不会告诉你全部
  • RTFM & 做实验!

偏移量管理:没那么简单 (2)

文件描述符在 fork 时会被子进程继承。

父子进程应该共用偏移量,还是应该各自持有偏移量?

  • 这决定了 offset 存储在哪里

考虑应用场景

  • 父子进程同时写入文件
    • 各自持有偏移量 → 父子进程需要协调偏移量的竞争
      • (race condition)
    • 共享偏移量 → 操作系统管理偏移量
      • 虽然仍然共享,但操作系统保证 write 的原子性 ✅

偏移量管理:行为

操作系统的每一个 API 都可能和其他 API 有交互 😂

  1. open 时,获得一个独立的 offset
  2. dup 时,两个文件描述符共享 offset
  3. fork 时,父子进程共享 offset
  4. execve 时文件描述符不变
  5. O_APPEND 方式打开的文件,偏移量永远在最后 (无论是否 fork)
    • modification of the file offset and the write operation are performed as a single atomic step

这也是 fork 被批评的一个原因

  • (在当时) 好的设计可能成为系统演化过程中的包袱

总结

总结

本次课内容与目标

  • 理解 “文件系统” 的设计
    • 设备、文件和目录
    • mount, chdir, mkdir, rmdir, link, unlink, symlink, open, mmap, read, write, lseek, ftruncate, ...

Takeaway messages

  • 一个经典的设计:简洁、通用、富有远见
  • 但无论多么有远见,在时间面前都会千疮百孔