一篇围绕文件系统保护展开的学习笔记。
文件系统不仅要把数据组织成文件和目录,还要保证这些数据在共享、修改和设备故障下仍然可控、可恢复、可用。本篇只讨论三个问题:文件共享时写入何时可见,文件能被谁访问,以及多个磁盘如何共同提供更高的可靠性和带宽。
一致性语义
一致性语义讨论的是:一个进程对文件的写入,何时会被其他进程看到。
一致性除了“可见性”之外,还涉及“系统崩溃后元数据和数据是否仍然匹配”。这就是文件系统日志与恢复机制要处理的问题。
日志的核心作用是:在真正修改文件系统结构之前,先把即将发生的关键更新记录下来。这样一来,即使系统在写入过程中崩溃,恢复程序也能依据日志判断哪些更新应该重做,哪些更新应该撤销。 文件系统的一致性语义包含两个方面:
| 层次 | 关注点 |
|---|---|
| 共享一致性 | 多个进程看到的写入何时可见 |
| 崩溃一致性 | 系统异常中断后,文件系统结构是否仍然自洽 |
1. 共享一致性
共享一致性讨论多个进程同时访问同一文件时,可见性如何定义。
| 语义 | 可见性规则 | 影响 |
|---|---|---|
| Unix 语义 | 一个进程的写入对其他已打开该文件的进程立即可见 | 共享最直接,但并发写入更需要同步 |
| 会话语义 | 写入在当前会话结束前不必对其他会话立即可见 | 更适合缓存或分布式场景 |
Unix 语义强调“立即可见”。它更接近本地文件系统的传统模型:文件是共享对象,写入落到同一个内核对象上,因此多个进程看到的是同一份最新状态。
会话语义强调“关闭后再可见”。一次打开、写入、关闭可以看成一个完整会话;会话结束之前,修改不需要立刻暴露给其他会话。这种语义更接近某些分布式或缓存较重的环境。
2. 崩溃一致性
崩溃一致性讨论的是:如果系统在更新磁盘结构的中途断电或崩溃,文件系统能否在下次启动后恢复到自洽状态。
一次文件更新通常不只改动一个块。例如,向文件追加一个块时,往往至少要同时更新:
| 结构 | 为什么要更新 |
|---|---|
| 数据块 | 写入新的用户数据 |
| inode | 更新文件大小和数据块指针 |
| 位图 | 标记新块已被分配 |
麻烦恰恰出在这里:这三个更新通常不是原子完成的。磁盘可以保证单个块写入完成或不完成,但不能保证“数据块、inode、位图”三者要么一起成功,要么一起失败。
为了看清问题,可以把更新后的三个对象记为:
| 记号 | 含义 |
|---|---|
| 新写入的数据块 | |
| 更新后的 inode | |
| 更新后的位图 |
如果崩溃发生在三次写入之间,不同落盘顺序会得到不同后果:
| 已落盘的内容 | 崩溃后的结果 |
|---|---|
| 只有 | 数据块存在,但没有 inode 指向它,也没有位图标记它已分配,等价于这次写入丢失 |
| 只有 | inode 已经指向新块,但该块内容可能还是旧垃圾数据,且 inode 与位图不一致 |
| 只有 | 位图显示块已被占用,但没有文件引用它,形成空间泄漏 |
| 元数据彼此一致,但 inode 指向的数据块仍可能是垃圾 | |
| 文件可指向正确数据,但位图仍认为该块未分配,后续可能被其他文件覆盖 | |
| 数据和位图都在,但没有 inode 指向该块,仍然形成空间泄漏 |
因此,只是“换一种写入顺序”并不能彻底解决崩溃一致性问题。先写
早期文件系统的一种处理方式是使用磁盘检查程序 fsck。它允许不一致先发生,再在重启时扫描磁盘并修复元数据冲突。
fsck 的思路是:重新遍历 inode、位图、目录树等结构,重新计算“哪些块应该被占用”“哪些 inode 应该有引用”“哪些目录项是合法的”,再把磁盘状态修补到一个元数据自洽的状态。
fsck 能做什么 | fsck 解决不了什么 |
|---|---|
| 修复 inode、位图、目录项之间的冲突 | 不能保证用户数据一定正确 |
| 回收泄漏空间 | 不能阻止 inode 指向已经写坏的垃圾块 |
| 在挂载前把元数据修到自洽 | 大磁盘上恢复很慢,需要扫描整个文件系统 |
因此,fsck 的主要问题是恢复代价太高,而且它更擅长修元数据,不擅长保证用户数据语义。
另一种更常见的方式是日志(journaling,或 write-ahead logging)。它不再等到崩溃后全盘扫描,而是在写入文件之前,先把这次事务记到日志里。
一个典型日志事务至少包含三部分:
| 日志块 | 作用 |
|---|---|
开始块 TxB | 表示一个新事务开始,并给出事务标识等信息 |
| 更新内容 | 这次事务涉及的数据块、inode、位图或其他元数据内容 |
结束块 TxE | 表示事务完整写入日志,可以视为已提交 |
这里的“提交”是把结束块 TxE 真正写入磁盘。只有开始块、更新内容和结束块都安全到达日志区后,这个事务才算提交。
因此,日志协议的顺序通常是:
- 先写入
TxB与事务内容。 - 内容全部写完后,再写入结束块
TxE。 - 当
TxE落盘,事务才算提交成功。 - 此后再把这些更新写回文件系统的最终位置,这一步通常称为 checkpoint。
这样做的意义是:如果崩溃发生在 TxE 写入之前,恢复程序会把这次事务当成未完成事务而忽略;如果崩溃发生在 TxE 写入之后但 checkpoint 完成之前,恢复程序只需要扫描日志并重放这些已提交事务,而不必重新扫描整个磁盘。
日志又可以分为两类:
| 类型 | 日志里记录什么 |
|---|---|
| 数据日志 | 元数据和用户数据都记入日志 |
| 元数据日志 | 只记录元数据,用户数据不写入日志 |
元数据日志更常见,因为它避免把每个数据块写两次,性能更好。但它会带来一个关键顺序问题:如果 inode 和位图已经通过日志提交,而真正的数据块还没写入磁盘,那么恢复后 inode 就可能稳定地指向一块垃圾数据。
因此,元数据日志必须满足下面这条规则:
换句话说,先保证“被指向的对象”已经存在,再提交“指向它的元数据”。这也是崩溃一致性里最核心的顺序规则之一:
这样,恢复程序即使只重放元数据,也不会让 inode 指向尚未写好的垃圾块。
除了 fsck 和日志,还有另一条重要路线:写时复制(Copy-On-Write,COW )。它不在原位置覆写旧结构,而是把新版本写到以前未使用的位置,等所有新块都准备好后,再原子地切换顶层指针,让文件系统整体看到新版本。
| 方法 | 更新方式 | 恢复思路 |
|---|---|---|
fsck | 允许不一致先发生 | 重启后全盘扫描并修复 |
| 日志 | 先记账,再提交,再写回最终位置 | 重放已提交事务 |
COW | 永不原地覆写,写好新版本后再切换根指针 | 继续使用旧版本或切到新版本 |
COW 的好处是旧版本块始终还在,新版本块也已经完整写好,最后只需切换根结构即可。因此它不仅能避免很多原地更新导致的不一致问题,还天然支持快照(snapshot):因为旧块不会立刻被覆盖,所以某个时间点的文件系统状态可以被保留下来。
访问控制
Unix 文件权限由 9 个 bit 组成,分为三组:
| owner | group | others |
|---|---|---|
| rwx | rwx | rwx |
每一组三位分别是读(read),写(write),执行(execute),使用0或1标识
因此,一组权限可以使用一个八进制数来表示,如 rw- 可写为 06 (前缀0表示8进制,前缀0x表示16进制)
三组权限可以使用3位八进制数(9-bit 位图)表示,如rw- r-- r--可写为0644
而ls -l上实际使用了10位,最高位用于表示类型(例如-表示普通文件,d表示目录)
prompt> ls -l
total 0
drwxr-xr-x 2 userA staff 64 Apr 17 20:09 testdir
-rw-r--r-- 1 userA staff 0 Apr 17 20:08 testfile在open()新建文件时,可以使用第三位mode给出权限,给出的权限还需要umask进行处理后,才能得到最后的权限。
final_mode = requested_mode & ~umask例如,创建文件(umask=0022):
open("a.txt", O_CREAT, 0666);最后的权限是rw- r-- r--
RAID
RAID 是 Redundant Arrays of Independent Disks 的缩写。它要解决的是单个磁盘同时面临的两个问题:
| 问题 | 含义 |
|---|---|
| 性能 | 单个磁盘的带宽和 I/O 次数有限 |
| 可靠性 | 单个磁盘故障会直接导致数据不可用或丢失 |
RAID 的基本思路是把多个磁盘组织成一个逻辑存储单元,同时引入并行访问和冗余信息。
| 机制 | 作用 |
|---|---|
| 分条 | 把数据分散到多个磁盘上,提高并行读写能力 |
| 冗余 | 额外保存镜像或奇偶校验信息,用于故障恢复 |
过去 RAID 常被描述为“用很多便宜小磁盘替代昂贵大磁盘”的方案;现在更重要的理由是可靠性和吞吐量。因此其中的 I 通常理解为 Independent,而不是 Inexpensive。
1. 数据分条
RAID 的性能基础来自数据分条。分条意味着把一份逻辑数据拆开,分布到多个磁盘上,使多个磁盘能够并行工作。
常见分条方式如下:
| 方式 | 含义 |
|---|---|
| 位级分条 | 一个字节的不同位位于不同磁盘上 |
| 块级分条 | 文件的不同块分散到多个磁盘上 |
位级分条的并行度很高,但所有磁盘几乎必须共同参与每次访问。块级分条更常见,因为它既能提高大请求的吞吐量,也能让不同磁盘并行处理多个小请求。
因此,分条主要带来两类收益:
| 收益 | 含义 |
|---|---|
| 提高大块顺序访问带宽 | 多个磁盘同时传输数据 |
| 提高小请求总体吞吐量 | 不同磁盘可以并发处理不同请求 |
但分条本身不等于冗余。只有分条而没有镜像或奇偶校验时,系统只是更快,并不会更可靠。
2. RAID级别
RAID 的不同级别可以看成“分条方式”和“冗余方式”的不同组合。
RAID 0
RAID 0 只有块分条,没有任何冗余。
| 特点 | 结果 |
|---|---|
| 有分条 | 顺序和并行读写性能较好 |
| 无冗余 | 任一磁盘故障都可能导致整个逻辑卷失效 |
因此,RAID 0 只适合数据丢失风险可接受、而吞吐量优先的场景。
RAID 1
RAID 1 是镜像。每份数据都会写入两块磁盘。
| 机制 | 优点 | 缺点 |
|---|---|---|
| 每个数据块保存完整副本 | 单盘故障后仍可继续读数据 | 容量利用率下降到一半 |
| 读请求可分配到任一副本 | 读性能较好 | 需要维护多个副本的一致写入 |
因此,RAID 1 用额外容量换取可靠性和较好的读取性能。
RAID 2 / 3 / 4
这三类 RAID 都基于奇偶校验或纠错信息恢复故障磁盘的数据,只是分条粒度和校验布局不同。
| 级别 | 特点 |
|---|---|
| RAID 2 | 使用位级分条和专门的纠错位磁盘 |
| RAID 3 | 使用位级分条和单独奇偶校验磁盘 |
| RAID 4 | 使用块级分条和单独奇偶校验磁盘 |
RAID2使用较多的纠错位,而RAID 3/4只为每个字节/块使用单个奇偶校验位,因此后者磁盘利用率更高。
RAID 2/3/4相对RAID/1更省冗余空间,但写入时都要维护额外的校验信息,因此控制器和写入路径更复杂。
除此之外,单独的奇偶校验磁盘容易成为瓶颈。尤其是小写入场景,更新一个数据块往往还要同步更新奇偶校验块,这会形成明显的热点。
RAID 5
RAID 5 仍然使用块级分条和奇偶校验,但不再把奇偶校验集中在一块磁盘上,而是把奇偶校验分布到所有磁盘中。
| 优点 | 含义 |
|---|---|
| 没有单独奇偶校验热点 | 缓解 RAID 4 的校验磁盘瓶颈 |
| 冗余开销低于 RAID 1 | 更适合大容量数据存储 |
| 可容忍一块磁盘故障 | 可靠性和容量之间较平衡 |
这也是为什么 RAID 5 长期以来是最常见的奇偶校验 RAID。
RAID 0+1 与 RAID 1+0
这两类都是分条和镜像的组合,但组合顺序不同。
| 级别 | 组织方式 |
|---|---|
| RAID 0+1 | 先分条,再对分条结果做镜像 |
| RAID 1+0 | 先镜像,再对镜像组做分条 |
RAID 1+0 一般比 RAID 0+1 更稳健。原因是前者在每组镜像里允许单盘失效,而后者一旦某条带中的一盘故障,就可能让整个RAID0崩溃,系统退化为单个RAID0系统。
3. RAID对比
假设每个磁盘的顺序读取速率是
| 项目 | RAID0 | RAID1 | RAID2/3/4 | RAID5 | RAID1+0/0+1 |
|---|---|---|---|---|---|
| 容量 | |||||
| 顺序读取 ( | 同 RAID1 | ||||
| 顺序写入 ( | 同 RAID1 | ||||
| 随机读取 ( | 同 RAID1 | ||||
| 随机写入 ( | 同 RAID1 |
注1:RAID 1的顺序读取虽然可以分配到所有磁盘, 但每个镜像副本上只会处理交错的一半块,因此实际可持续带宽通常近似为
注2:RAID 2/3/4 使用独立校验磁盘,小写入通常要经历读-改-写过程:先读取旧数据块和旧校验块,再计算新校验信息,然后写回新数据块和新校验块。因此写入容易受校验磁盘限制。
小结
本文介绍了文件系统保护相关的内容,包括:
| 层次 | 解决什么问题 |
|---|---|
| 一致性语义 | 多个进程共享文件时,写入何时可见 |
| 访问控制 | 文件对象能被谁访问、以何种方式访问 |
| RAID | 多个磁盘如何共同提高可靠性与性能 |
前两层主要保护的是文件对象的语义和权限边界,后一层保护的是底层存储设备的可用性。