文件系统:实现设备在应用程序之间的共享

磁盘中存储的数据

  • 程序数据
    • 可执行文件和动态链接库
    • 应用数据 (高清图片、过场动画、3D 模型……)
  • 用户数据
    • 文档、下载、截图
  • 系统数据
    • Manpages
    • 配置文件 (/etc)

字节序列并不是磁盘的好抽象

  • 让所有应用共享磁盘?一个程序 bug 操作系统就没了

文件系统:磁盘的虚拟化

文件系统:设计目标

  1. 提供合理的 API 使多个应用程序能共享数据
  2. 提供一定的隔离,使恶意/出错程序的伤害不能任意扩大

“存储设备 (字节序列) 的虚拟化”

  • 磁盘 (I/O 设备) = 一个可以读/写的字节序列
  • 虚拟磁盘 (文件) = 一个可以读/写的动态字节序列
    • 命名管理
      • 虚拟磁盘的名称、检索和遍历
    • 数据管理
      • std::vector<char> (随机读写/resize)

文件 = 虚拟磁盘

文件:虚拟的磁盘

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

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

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

mmap 和文件

void *mmap(
  void *addr, size_t length, int prot, int flags,
  int fd, off_t offset // 映射 fd 文件
);                     // offset 开始的 length 字节

文件是 “虚拟磁盘”

  • 把磁盘的一部分映射到地址空间,再自然不过了

小问题

  • 映射的长度超过文件大小会发生什么?
    • (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 被批评的一个原因

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