一篇围绕文件系统保护展开的学习笔记。


文件系统不仅要把数据组织成文件和目录,还要保证这些数据在共享、修改和设备故障下仍然可控、可恢复、可用。本篇只讨论三个问题:文件共享时写入何时可见,文件能被谁访问,以及多个磁盘如何共同提供更高的可靠性和带宽。

一致性语义

一致性语义讨论的是:一个进程对文件的写入,何时会被其他进程看到。

一致性除了“可见性”之外,还涉及“系统崩溃后元数据和数据是否仍然匹配”。这就是文件系统日志与恢复机制要处理的问题。

日志的核心作用是:在真正修改文件系统结构之前,先把即将发生的关键更新记录下来。这样一来,即使系统在写入过程中崩溃,恢复程序也能依据日志判断哪些更新应该重做,哪些更新应该撤销。 文件系统的一致性语义包含两个方面:

层次关注点
共享一致性多个进程看到的写入何时可见
崩溃一致性系统异常中断后,文件系统结构是否仍然自洽

1. 共享一致性

共享一致性讨论多个进程同时访问同一文件时,可见性如何定义。

语义可见性规则影响
Unix 语义一个进程的写入对其他已打开该文件的进程立即可见共享最直接,但并发写入更需要同步
会话语义写入在当前会话结束前不必对其他会话立即可见更适合缓存或分布式场景

Unix 语义强调“立即可见”。它更接近本地文件系统的传统模型:文件是共享对象,写入落到同一个内核对象上,因此多个进程看到的是同一份最新状态。

会话语义强调“关闭后再可见”。一次打开、写入、关闭可以看成一个完整会话;会话结束之前,修改不需要立刻暴露给其他会话。这种语义更接近某些分布式或缓存较重的环境。

2. 崩溃一致性

崩溃一致性讨论的是:如果系统在更新磁盘结构的中途断电或崩溃,文件系统能否在下次启动后恢复到自洽状态。

一次文件更新通常不只改动一个块。例如,向文件追加一个块时,往往至少要同时更新:

结构为什么要更新
数据块写入新的用户数据
inode更新文件大小和数据块指针
位图标记新块已被分配

麻烦恰恰出在这里:这三个更新通常不是原子完成的。磁盘可以保证单个块写入完成或不完成,但不能保证“数据块、inode、位图”三者要么一起成功,要么一起失败。

为了看清问题,可以把更新后的三个对象记为:

记号含义
Db新写入的数据块
I[v2]更新后的 inode
B[v2]更新后的位图

如果崩溃发生在三次写入之间,不同落盘顺序会得到不同后果:

已落盘的内容崩溃后的结果
只有 Db数据块存在,但没有 inode 指向它,也没有位图标记它已分配,等价于这次写入丢失
只有 I[v2]inode 已经指向新块,但该块内容可能还是旧垃圾数据,且 inode 与位图不一致
只有 B[v2]位图显示块已被占用,但没有文件引用它,形成空间泄漏
I[v2]+B[v2]元数据彼此一致,但 inode 指向的数据块仍可能是垃圾
I[v2]+Db文件可指向正确数据,但位图仍认为该块未分配,后续可能被其他文件覆盖
B[v2]+Db数据和位图都在,但没有 inode 指向该块,仍然形成空间泄漏

因此,只是“换一种写入顺序”并不能彻底解决崩溃一致性问题。先写I[v2] ,可能让指针指向垃圾;先写B[v2],可能造成空间泄漏;先写数据虽然更安全一些,但仍然不能保证I[v2]B[v2]同时更新完成。

早期文件系统的一种处理方式是使用磁盘检查程序 fsck。它允许不一致先发生,再在重启时扫描磁盘并修复元数据冲突。

fsck 的思路是:重新遍历 inode、位图、目录树等结构,重新计算“哪些块应该被占用”“哪些 inode 应该有引用”“哪些目录项是合法的”,再把磁盘状态修补到一个元数据自洽的状态。

fsck 能做什么fsck 解决不了什么
修复 inode、位图、目录项之间的冲突不能保证用户数据一定正确
回收泄漏空间不能阻止 inode 指向已经写坏的垃圾块
在挂载前把元数据修到自洽大磁盘上恢复很慢,需要扫描整个文件系统

因此,fsck 的主要问题是恢复代价太高,而且它更擅长修元数据,不擅长保证用户数据语义。

另一种更常见的方式是日志(journaling,或 write-ahead logging)。它不再等到崩溃后全盘扫描,而是在写入文件之前,先把这次事务记到日志里。

一个典型日志事务至少包含三部分:

日志块作用
开始块 TxB表示一个新事务开始,并给出事务标识等信息
更新内容这次事务涉及的数据块、inode、位图或其他元数据内容
结束块 TxE表示事务完整写入日志,可以视为已提交

这里的“提交”是把结束块 TxE 真正写入磁盘。只有开始块、更新内容和结束块都安全到达日志区后,这个事务才算提交。

因此,日志协议的顺序通常是:

  1. 先写入TxB与事务内容。
  2. 内容全部写完后,再写入结束块 TxE
  3. TxE 落盘,事务才算提交成功。
  4. 此后再把这些更新写回文件系统的最终位置,这一步通常称为 checkpoint。

这样做的意义是:如果崩溃发生在 TxE 写入之前,恢复程序会把这次事务当成未完成事务而忽略;如果崩溃发生在 TxE 写入之后但 checkpoint 完成之前,恢复程序只需要扫描日志并重放这些已提交事务,而不必重新扫描整个磁盘。

日志又可以分为两类:

类型日志里记录什么
数据日志元数据和用户数据都记入日志
元数据日志只记录元数据,用户数据不写入日志

元数据日志更常见,因为它避免把每个数据块写两次,性能更好。但它会带来一个关键顺序问题:如果 inode 和位图已经通过日志提交,而真正的数据块还没写入磁盘,那么恢复后 inode 就可能稳定地指向一块垃圾数据。

因此,元数据日志必须满足下面这条规则:

先写数据块,再提交描述这些数据块的元数据日志

换句话说,先保证“被指向的对象”已经存在,再提交“指向它的元数据”。这也是崩溃一致性里最核心的顺序规则之一:

先写被指向的对象,再写指向它的对象

这样,恢复程序即使只重放元数据,也不会让 inode 指向尚未写好的垃圾块。

除了 fsck 和日志,还有另一条重要路线:写时复制(Copy-On-Write,COW )。它不在原位置覆写旧结构,而是把新版本写到以前未使用的位置,等所有新块都准备好后,再原子地切换顶层指针,让文件系统整体看到新版本。

方法更新方式恢复思路
fsck允许不一致先发生重启后全盘扫描并修复
日志先记账,再提交,再写回最终位置重放已提交事务
COW永不原地覆写,写好新版本后再切换根指针继续使用旧版本或切到新版本

COW 的好处是旧版本块始终还在,新版本块也已经完整写好,最后只需切换根结构即可。因此它不仅能避免很多原地更新导致的不一致问题,还天然支持快照(snapshot):因为旧块不会立刻被覆盖,所以某个时间点的文件系统状态可以被保留下来。

访问控制

Unix 文件权限由 9 个 bit 组成,分为三组:

ownergroupothers
rwxrwxrwx

每一组三位分别是读(read),写(write),执行(execute),使用0或1标识

因此,一组权限可以使用一个八进制数来表示,如 rw- 可写为 06 (前缀0表示8进制,前缀0x表示16进制)

三组权限可以使用3位八进制数(9-bit 位图)表示,如rw- r-- r--可写为0644

ls -l上实际使用了10位,最高位用于表示类型(例如-表示普通文件,d表示目录)

bash
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进行处理后,才能得到最后的权限。

c
final_mode = requested_mode & ~umask

例如,创建文件(umask=0022):

c
open("a.txt", O_CREAT, 0666);

最后的权限是rw- r-- r--

RAID

RAIDRedundant 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对比

假设每个磁盘的顺序读取速率是 SR MB/s,顺序写入速率是 SW MB/s,随机读取速率是 RR MB/s,随机写入速率是 RW MB/s。每个 RAID 使用 N 个独立磁盘,存储纠错或奇偶校验信息将占据等效于 NECC 个磁盘区域的空间。

项目RAID0RAID1RAID2/3/4RAID5RAID1+0/0+1
容量NN/2NNECCNNECCN/2
顺序读取 (SR)NSRN2SR(注1)(NNECC)SRNSR同 RAID1
顺序写入 (SW)NSWN2SW12SW(注2)NSW同 RAID1
随机读取 (RR)NRRNRR(NNECC)RRNRR同 RAID1
随机写入 (RW)NRWN2RW12RW(注2)NRW同 RAID1

注1:RAID 1的顺序读取虽然可以分配到所有磁盘, 但每个镜像副本上只会处理交错的一半块,因此实际可持续带宽通常近似为 N2SR,而不是 NSR

注2:RAID 2/3/4 使用独立校验磁盘,小写入通常要经历读-改-写过程:先读取旧数据块和旧校验块,再计算新校验信息,然后写回新数据块和新校验块。因此写入容易受校验磁盘限制。

小结

本文介绍了文件系统保护相关的内容,包括:

层次解决什么问题
一致性语义多个进程共享文件时,写入何时可见
访问控制文件对象能被谁访问、以何种方式访问
RAID多个磁盘如何共同提高可靠性与性能

前两层主要保护的是文件对象的语义和权限边界,后一层保护的是底层存储设备的可用性。