(2021) 24 [持久化] 文件系统API
南京大学操作系统课蒋炎岩老师网络课程笔记。
视频:https://www.bilibili.com/video/BV1HN41197Ko?p=24
讲义:http://jyywiki.cn/OS/2021/slides/14.slides#/
背景
回顾
硬件视角:持久化的“层层抽象”
- 物理层1-bit存储
- 设备层I/O设备(寄存器)
- 驱动层(可读可写可控制的对象)
用户(应用程序)视角:对象 + API
- C:\Program Files\…
- /etc/apt/souces.list
- /bin/bash
中间的部分:文件系统
本次课的内容和目标
理解 “文件系统” 的设计
- “文件系统” 的需求分析
- 需要什么对象
- 提供什么 API
文件系统概述
为什么需要文件系统?
存点什么
计算机:辅助人类更好地完成物理世界中的任务开机以后,必须得有点 “代表物理世界” 的东西。
“开机” 后操作系统中应该有的对象
- 各类数据
- 《操作系统》点名册/成绩单/Online Judge 结果/…
- 处理这些数据的程序
- wc, grep, vim, LibreOffice, …
- libc, X11, gnome-settings-manager, …
- 这些 “对象” 应该被持久地保存在介质上
别乱套
没问题!我们已经讲过 1-bit 的存储了,但让应用程序直接通过驱动访问存储设备 (1950s)?不合适!这样就乱套了。
今天的系统中有不止一个程序
- 每个程序还需要考虑各种访问权限、并发控制……
- 程序出 bug 了,不小心弄坏了整块磁盘
文件系统:设计目标
- 提供合理的 API 使多个应用程序能共享数据
- 提供一定的隔离,使恶意/出错程序的伤害不能任意扩大
- 这就是文件系统
- 你会怎么办?
- 这就是文件系统
文件系统:存储设备的虚拟化
磁盘 (I/O 设备) = 一个可以读/写的字节序列 —> 虚拟磁盘 (文件) = 一个可以读/写/的动态字节序列
结合我们操作系统课前面所学,可以类比:
- 进程抽象:一个 CPU → 切分时间片,在时间上共享
- 虚拟存储:一份内存 → 划分给多个虚拟地址空间
- 文件系统:一个物理磁盘 → 多个虚拟磁盘
文件系统 API: 虚拟磁盘管理
- 需要解决的问题
- 虚拟磁盘的命名、查找、权限
- 虚拟磁盘的操作 (读写)
虚拟磁盘:命名管理
接下来我们就来看看,两个最主流的文件系统是如何实现磁盘到虚拟磁盘的虚拟化的。
文件系统 = “虚拟磁盘名” 到 “虚拟磁盘对象” 的映射
我们的系统中可能会有上百万个文件,这时候,局部性就发挥很大作用了。
目录:文件/目录的集合 (形成一棵树),逻辑相关的数据存放在相近的目录
.
└── 学习资料├── .学习资料(隐藏)├── 问题求解1├── 问题求解2├── 问题求解3└── 问题求解4
Windows文件系统
树总得有个根结点
Windows: 每个驱动器是一棵树
C:\
:“C 盘根目录”C:\Program Files\
,C:\Windows
,C:\Users
, …
D:\
: “D 盘根目录”D:\学习资料\
- 优盘分配给新的盘符
- 为什么没有
A:\
,B:\
? A、B盘历史上是为软驱预留的
Linux、UNIX文件系统
UNIX/Linux只有一个根/
,其他没有了,那第二个设备呢?优盘呢???该怎么办
文件系统的挂载
挂载设备
UNIX 允许任何一个目录都可以 “挂载” 一个设备代表的目录树
非常灵活的设计;充分利用了目录的局部性。
比如 128G 优盘分成了两个 64G 分区 (Linux/
和 exFAT)
可以直接使用mount
命令 “挂载” 到一个目录上
-
目录里原先的内容会暂时消失 (但不会丢失),改换为你挂载的设备的内容。
-
/
,/home
,/var
可以是独立的磁盘
如何 mount 一个文件?
- disk-img.tar.gz的挂载:创建一个 “loopback” 设备(
lsblk
可以看到),实际上loopback相当于是将一个文件(虚拟磁盘设备)反虚拟化为一个设备。 - 然后就变成挂载设备了,可以 strace 一下,看看操作系统提供了哪些 API。
Filesystem Hierarchy Standard (FHS)
Linux的文件树标准。
挂载机制的好处
挂载机制非常灵活,比如说,你有两块磁盘,那你可以将你的/
根目录和/home
主目录分别挂载在两块磁盘上,在/
根目录下创建一个空的主目录即可。这样的好处是对根目录和主目录的访问带宽就分开了,你可以同时读写这两个目录下的内容。
目录API(系统调用)
目录管理:创建、删除、遍历
这个简单
-
mkdir
- 创建一个目录
- 可以设置访问权限
-
rmdir
- 删除一个空目录
- 没有 “递归删除” 的系统调用
- (应用层能实现的,就不要在操作系统层实现)
rm -rf
会遍历目录,逐个删除 (试试 strace)
-
getdents
-
返回
count
个目录项 (ls, find, tree 都使用这个),以点开头的目录会被系统调用返回,只是 ls 没有显示。ls
不显示以.
所有开头的文件的文件,这时为了隐藏当前目录.
和上一级目录..
,但是在实现上所有以.
开头的文件或目录都不会被显示。这使得我们可以通过在文件或目录前面加.
来对其做简单的隐藏。使用ls -a
可以显示全部。
-
小启示:我们遇到Linux系统中的问题时通常会上Stack Overflow来查找别人解决问题的方法,这当然是很好的,Stack Overflow有许多有用的技巧。但实际上,Linux系统是self-contain的,即就算没有互联网,我们也可以通过man page配合strace等工具来找到我们想要的一切说明,并且,man page是最权威的。当然,Stack Overflow上的一些别人的巧妙的方法也是互联网搜索的优势。
硬(hard)链接
UNIX文件指针
在UNIX中,文件和目录完全不是同一个概念,虽然我们平时看着它们仿佛并列地躺在某个文件夹下。但实际上,目录是树状结构组织的,而文件,却是每个目录指向某个文件的指针。并且,每个文件都有一个编号,可能会有多个目录下的多个指针都指向同一个编号的文件。它们虽然存在于不同的目录下,甚至名称也不同,但是同一个编号的文件是完全相同的,修改也是同步的。如下图所示:
我们可以做这样的测试:
创建测试目录并在其中的a.txt
写入Hello World!
mkdir test && cd test && touch a.txt
vim a.txt # 写入 Hello World!
创建a.txt
的硬链接b.txt
:
ln a.txt b.txt
我们查看两个文件的内容,输出显示都是同样的Hello World:
cat *.txt
# 输出:
# Hello World!
# Hello World!
这时,我们修改b.txt
的内容为Hello World! Changed~
,再查看两个文件的内容:
vim b.txt # 更改为 Hello World! Changed~
cat *.txt
# 输出:
# Hello World! Changed~
# Hello World! Changed~
结果两个文件都被修改了,这就是硬链接,我们可以通过-i
参数查看文件的编号:
ls -i
# 输出:
# 8593746 a.txt 8593746 b.txt
可以看到,两个文件其实是同一个编号的文件的不同链接。即硬链接的图示如下:
硬链接
注意:
- 目录中仅存储指向文件数据的指针
- 允许一个文件被多个目录引用
- 不能链接目录 ❌
- 不能跨文件系统 ❌
小知识:其实所有的文件都是硬连接 (ls -i
查看)
- 删除的系统调用称为 “unlink” (引用计数)
应用场景
可以给文件起别名,同步,省空间。
需求:系统中可能有同一个运行库的多个版本
libc-2.27.so
,libc-2.26.so
, …- 还需要一个 “当前版本的 libc”
- 程序需要链接 “
libc.so.6
” - 能否避免文件的一份拷贝?
- 程序需要链接 “
软(symbolic)链接
软链接:在文件里存储一个 “跳转提示”,相当于”快捷方式“。
- 软链接也是一个文件
- 当引用这个文件时,去找另一个文件
- 另一个文件的绝对/相对路径以文本形式存储在文件里
- 可以跨文件系统、可以链接目录、……好处多多
- 甚至,符号链接可以指向一个暂时不存在的文件或目录,只要这个不存在文件或目录将来某天存在了,这个符号链接就会生效
ln -s
创建软链接,用的是symlink
系统调用。现在系统中/lib
下的共享库,通常都是软链接。
我们接着上面硬链接的例子来看一下二者的区别:
再在测试目录下创建a.txt
的软链接c.txt
:
ln -s a.txt c.txt
我们用-li参数查看测试目录中的三个文件:
ls -li
# 输出
# 8593746 -rw-rw-r-- 2 ps ps 22 10月 1 22:14 a.txt
# 8593746 -rw-rw-r-- 2 ps ps 22 10月 1 22:14 b.txt
# 8593742 lrwxrwxrwx 1 ps ps 5 10月 1 22:35 c.txt -> a.txt
在这里,b,c分别是a的硬、软链接。可以看到,a和c的文件编号是不一样的,因为它们是软链接。但是,它们的修改仍然是同步的,因为我们在试图修改c的时候,系统会顺着上面输出的软链接箭头去寻找,直到找到一个真实的文件或者目录。我们还是来试一下:
vim c.txt # 修改为Hello World! Changed~ Soft~
cat *.txt
# 输出:
# Hello World! Changed~ Soft~
# Hello World! Changed~ Soft~
# Hello World! Changed~ Soft~
与预期一致。此时测试目录下的链接关系应该如下图所示:
软链接可能带来的麻烦
软链接可以随意创建 (当前可能不合法;但未来可能合法),操作系统在处理软链接时会执行路径解析,,允许多次间接链接,会有意想不到的复杂性 ,a → b → c (递归解析)。可以创建软连接的硬链接 (因为软链接也是文件),通过ls -i
可以看到。
符号链接成环?ln -s . a
。所有处理符号链接的程序 (tree, find, …) 都要考虑递归的情况。它们默认遇到软链接就跳过,如果加上-L参数强制使它们考虑软链接的话,它们也会很小心的检测成环和递归的情况并适时退出。
进程的 “当前目录”
working/current directory
pwd
命令或$PWD
环境变量可以查看- 用
chdir
系统调用修改- 对应 shell 中的 cd
- 注意 cd 是 shell 的内部命令,不存在
/bin/cd
。不能被strace
问题:线程是共享 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 可以改变文件大小
- (RTFM, “Errors” section):
文件系统的游标(偏移量)
文件系统的游标(偏移量)介绍
文件的读写时,文件描述符自带 “游标”,这样就不用每次都指定文件读/写到哪里了,方便了程序员顺序访问文件
例子
read(fd, buf, 512);
- 第一个 512 字节read(fd, buf, 512);
- 第二个 512 字节lseek(fd, -1, SEEK_END);
- 最后一个字节
偏移量管理的问题 1
mmap, lseek, ftruncate 互相交互的情况
- 初始时文件大小为 0
- mmap (
length
= 2 MiB) - lseek to 3 MiB (
SEEK_SET
) - ftruncate to 1 MiB
- mmap (
在任何时刻,写入数据的行为是什么?
- blog posts 不会告诉你全部
- RTFM & 做实验!
偏移量管理的问题 2
我们知道,文件描述符在 fork 时会被子进程继承。那父子进程应该共用偏移量,还是应该各自持有偏移量?
- 这决定了
offset
存储在哪里
考虑应用场景
- 父子进程同时写入文件
- 各自持有偏移量 → 父子进程需要协调偏移量的竞争
- (race condition)
- 共享偏移量 → 操作系统管理偏移量
- 虽然仍然共享,但操作系统保证
write
的原子性 ✅
- 虽然仍然共享,但操作系统保证
- 各自持有偏移量 → 父子进程需要协调偏移量的竞争
偏移量管理:行为
操作系统的每一个 API 都可能和其他 API 有交互
- open 时,获得一个独立的 offset
- dup 时,两个文件描述符共享 offset
- fork 时,父子进程共享 offset
- execve 时文件描述符不变
O_APPEND
方式打开的文件,偏移量永远在最后 (无论是否 fork)- modification of the file offset and the write operation are performed as a single atomic step
这也是 fork 被批评的一个原因,(在当时) 好的设计可能成为系统演化过程中的包袱,今天的 fork 可谓是 “补丁满满”。
总结
本次课内容与目标
- 理解 “文件系统” 的设计
- 设备、文件和目录
- mount, chdir, mkdir, rmdir, link, unlink, symlink, open, mmap, read, write, lseek, ftruncate, …
Takeaway messages
- 一个经典的设计:简洁、通用、富有远见
- 但无论多么有远见,在时间面前都会千疮百孔