文件系统
对于长期存储的信息有三个基本要求:
- 能够存储大量信息
- 使用信息的进程终止时,信息依旧存在
- 必须能使多个进程并发访问相关信息
磁盘由于其长期存储的性质,已有多年的使用历史。今年固态硬盘因其没有易损坏的移动部件、可以提供高速的随即访问,而流行起来。但磁盘和光盘虽然性能较差但也广泛用于备份。磁盘可以看做一种大小固定的块的线性序列,且支持:
- 读块 k
- 写块 k
磁盘一般支持更多操作,但只要存在这两个操作,原则上就可以解决长期存储问题。当然这还远远不够,一些操作不便于实现,在思考时往往还产生一些问题:
- 如何找到信息
- 如何防止一个用户读取另一个用户的数据
- 如何知道哪些块是空闲的
就像 OS 提取处理器的概念来创建进程的抽象,以及提取 RAM 的概念来创建进程虚拟地址空间的抽象一样,使用 文件 (File) 来解决磁盘的问题。File 是 进程创建的信息逻辑单元,文件是对磁盘的建模而非 RAM,因此将文件看做地址空间就能理解了。
进程可以读取已存在的 File,并在需要时建立新 File,存储的文件必须是持久的,因此不会受到进程的创建与终止而受到影响,只有在其所有者明确删除它的情况下才会消失。File 受操作系统管理,有关的构造、命名、访问、使用、保护、实现和管理方法都是 OS 设计的主要内容。从总体上看,OS 处理文件的部分被称为 文件系统 (file system),这才是 OS 的核心问题之一。从用户的角度看,File System 中最重要的就是其表现形式,即文件由什么组成的,如何命名、修改等操作。至于如何实现,和我这个用户有什么关系呢。
文件
首先先从用户的角度观察文件,文件是对磁盘上保存信息的一种抽象,用户不必关心磁盘如何存储、存储到哪里、实际的工作方式等细节。
文件系统是实现这些细节的程序,在 MS-DOS 中使用的是 FAT-16
文件系统,Windows98
对其进行了扩展,从而成为今天耳熟能详的 FAT-32
文件系统,Windows 如今是用一种更为先进的 NTFS
文件系统。微软还根据 FAT 文件系统开发出了 exFAT
,这是针对于闪存和大文件开发的系统,并且也是唯一能满足 OS X 读写操作的微软开发的文件系统。UNIX
的文件系统也有很多,目前主流的是 Linux 的 ext4
(其是 ext2 和 ext3 的升级版本)、
Solaris 的 ZFS
、Unix 传统的 FFS
以及 OS X 的 HFS+
等,还有一些著名公司如
SGI 开发的 XFS
、RedHat 主导的 BtrFS
等。这些文件系统可能底层实现、适用环境不同,但大部分都遵循 POSIX 实现,对用户操作来说是透明的。
文件系统限制着文件的命名,但是我们可以在进程结束后并且文件依然存在时,根据名称访问文件。文件名一般用圆点隔开分文两部分,圆点后的部分被称为 文件扩展名 (file
extension),extension 往往是约定俗成的,表明了当前文件的内容。在 FAT-16 中限制文件名由 1 到 8 个字符以及 1 到 3 个字符的 extension 组成,而 UNIX 中则以约定而非强制使用 extension。比如 FAT-16 是不区分大小写的文件系统,但是 UNIX 会严格区分文件的大小写,所以一个 C++ 文件 a.C
,在 FAT-16 中被认为是一个 C 语言文件,而
UNIX 中则是一个 C++ 文件。虽然 UNIX 并不关心 extension,但是现代图形界面往往支持程序与 extension 注册绑定,方便用户使用。
文件系统支持多种文件类型,比如 UNIX 与 Windows 中都支持的 普通文件 (regular file) 与 目录 (directory),UNIX 还支持 字符文件 (character file) 与 块文件 (block file)。regular 包含有用户信息的文件,有二进制文件或 ASCII 文件,ASCII 文件在每行结束有一个换行符 LF (有的操作系统采用回车 CR,或混合 CRLF),ASCII 文件方便显示、打印并可以编辑。二进制文件人们往往无法理解,其打印也是一堆无意义数据,但二进制文件其格式正确,执行的程序才能精准的将其展示或打印为人类可懂的内容。
文件结构
文件可以有多种构造方式,最常用的是三种方式:
- 字节,即文件是由多个字节组成的,OS 并不关心文件的内容是什么,其内容的含义交由处理其的软件解释。这是 OS 中最主流的实现方式,有极大的灵活性,允许用户写入任何内容,OS 不提供帮助也不构成障碍。
- 记录,即文件是具有固定长度记录的序列,每个记录都有其内部结构。读操作将返回一个记录,写操作重写或追加一个记录。
- 树,即文件由一颗记录树构成,每个记录不必具有相同的长度,记录的固定位置上有一个 key 字段,树按照 key 进行排序并可以快速查找。采用这种方式时,用户可以为文件添加新的记录,并无需关心记录的存储位置,但用户不能决定记录添加在什么位置,这是 OS 的工作范围。这种方法主要被用语一些大型计算机中。
文件属性
文件除了文件名与数据,还有保存其他与文件相关的信息,如创建日期与时间、大小等,这些附加信息被称为 属性 (attribute) 或 元数据 (metadata)。但是 attribute 在不同 OS 中差别很大,这里列出其中一些作为举例。
属性 | 含义 |
---|---|
创建者 | 创建文件者的 ID |
所有者 | 文件当前的所有者 |
只读标志 | 文件处于只读、读写状态 |
隐藏标志 | 文件是否在列表中显示 |
系统标志 | 这是一个系统文件或普通文件 |
存档标志 | 该文件是否备份 |
临时标志 | 该文件在进程退出时是否删除 |
创建时间 | 创建文件的日期与时间 |
访问时间 | 上一次访问文件的日期与时间 |
修改时间 | 上一次修改文件的日期与时间 |
当前大小 | 文件的字节数 |
占用大小 | 文件实际占用的字节数 |
文件操作
早期操作系统仅支持 顺序访问 (sequential access),进程在这些系统中从头按顺序读取文件的全部字节或记录,不能跳过或随机读取内容,可以返回起点并根据需求多次读取文件,这是磁带的访问方式。当我们使用磁盘时,可以不按顺序读取文件,这种访问方式被称为 随机访问 (random access)。
使用文件的目的是储存信息并未了之后的检索,因此这里介绍一些常见的与文件相关的系统调用
create
,创建不包含任何数据的文件,表明文件即将建立并设置一些属性delete
,当文件不再需要时,以删除文件并释放磁盘空间open
,将文件属性与磁盘地址装入内存,以供以后的使用close
,关闭文件并释放内部表空间read
,在文件中读取数据,读取的数据来自文件的当前位置write
,向文件的当前位置写入数据append
,向文件的末尾写入数据seek
,指定文件的位置,方便对文件进行随机访问
目录
文件系统通常提供 目录 或 文件夹 来记录文件的位置,当然很多系统中目录也是文件。目录是层级结构的,最顶层的目录被称为 根 (root),在 UNIX 系统中也被称为 /
,目录将文件自然而然的进行分组。
当要访问一个文件时,最简单的方法就是从 root 开始,将所有目录与该文件组成一个路径,这被称为 绝对路径 (absolute path)。由于可能有多个路径,因此系统会使用一种符号作为路径的分隔符,在有些系统是不同的,对于一个路径 \(usr \rightarrow GinShio \rightarrow mailbox\),它的绝对路径有以下表示:
- Windows (
\
): \usr\GinShio\mailbox - UNIX (
/
): /usr/GinShio/mailbox - MULTICS (
>
): >usr>GinShio>mailbox
另一种方式就是 相对路径 (relative path),它与 工作目录 / 当前目录 (working
/ current directory) 一起使用,用户不再需要从 root 开始指定,在没有任何前置分隔符时系统会将其解释为 current 下的文件。这使得 relative 更加方便,且与 absolute
功能相同。如果需要声明 current 一般使用 .
(dot) 表示,而需要声明当前目录的上一级目录则使用 ..
表示。
以上图为例,假设我们现在工作目录位于 init.d
中,则有以下表示
- 绝对路径表示当前路径:/etc/init.d
- 相对路径表示 X11 的路径:../X11
- 相对路径表示 boot.d 的路径:./boot.d
文件系统的实现
文件系统的布局
文件系统存放于磁盘上,多数磁盘划分为一个或多个分区,每个分区是一个独立的文件系统。在传统的 BIOS 中需要将磁盘格式化为 主引导记录 (Master Boot Record, MBR) 格式,而 UEFI 则会将磁盘格式化为 全局唯一标识磁盘分区表 (GUID Partition Table, GPT) 格式,GPT 的最开始部分有一段兼容 MBR 的引导,不论是 MBR 还是 GPT 都为了引导计算机系统的启动。
引导记录结尾有一段分区表,该表给出了每个分区的起始和结束地址,表中的一个分区被标记为活动分区。在引导时 BIOS 会读入并执行 MBR,MBR 做的第一件事即确定活动分区并读入其第一个块,这个块被称为 引导块 (boot block),并执行。boot block 中的程序将装载该分区中的操作系统。为了统一,每个分区都有从 boot block 开始,无论是否包含操作系统。磁盘的分区的布局是根据文件系统的不同而变化的。一般情况下第一个是 超级块 (superblock),其中包含文件系统的所有关键参数,在计算机启动时或该文件系统首次使用时,superblock 被读入内存。其中的典型信息包括:确定文件系统类型用的魔数、文件系统中的块数量以及其他重要的管理信息。空闲空间管理是针对空闲块的信息,可以使用位图或指针列表的形式给出。index 组是记录文件信息的数组,说明了文件的方方面面。根目录存储着目录树的根部,之后则是其他所有文件和目录。
文件的实现
文件存储实现的关键是记录各个文件分别用到哪些磁盘块。
-
连续分配
最简单的方案是将每个文件作为一串连续数据块存储到磁盘上,根据块大小的不同文件占用的块数量也不同。每个文件都从一个新的块开始存储,因此当最后一个块不被占满的时候,将造成一定的内部浪费,这与内存分页的内部浪费类似。这样的文件分配方式有两大优点:
- 实现简单,仅需记住磁盘地址与文件块数即可
- 读操作性能好
当删除文件时,文件所占用的块被释放,这时会出现外部的碎片化的空闲块。这与 RAM 的外部碎片相似,当文件无法找到足够容纳自己的空闲区时,这个文件将不能被写入磁盘。对于已满的磁盘来说,我们可以进行压缩操作,将磁盘中的空闲区合并。
-
链表分配
可以与链表的实现类似,将每个块的第一个字作为指向下一块的指针,块的其他部分存放数据。这样可以充分利用磁盘的每一个块,不会造成磁盘碎片进而浪费存储空间。并且 index 中只需要存储文件第一个块的地址即可,实现并不复杂。但是这个实现中,顺序读写并不是什么问题,当需要随机读写时该实现将造成很大困扰,链表并不能进行随即访问 (或者是十分缓慢)。
为了增强读效率,可以将链表存放于内存当中,使用表来记录文件使用的块,这个表被称为 文件分配表 (File Allocation Table, FAT)。按 FAT 的组织方式整个块都可以存放数据,但内存消耗极大,并不实用。
-
index 节点
i 节点 (index node) 中可以列出文件属性和文件块的地址,这样只需要 index 即可获取到文件需要哪些块,并且在打开文件的时候将 index node 读入 RAM 即可,不再需要 FAT 对文件进行常驻记录。但是问题也随之接踵而至,index node 只能固定存储一定量的磁盘地址,需要使用文件中一些特殊的块来存储额外的地址空间。
目录的实现
用户利用路径名来寻找对应的文件,因此目录项提供了查找文件磁盘块所需的信息,因系统差异这些信息可能是指向整个文件的磁盘地址 (sequential allocation)、第一个块编号 (list allocation) 或 index node 编号。无论如何目录系统对会将路径名映射为定位文件数据所需的信息。另一个问题是文件的属性如何存放。显而易见的答案是,将属性与磁盘地址都存储于目录项之中。而采用 index node 的文件系统,则可以把属性存放于 index node 中,这样目录项的信息只需要包含文件名与 index 地址即可。
对于可变长的文件名,存储在目录项将会有一个问题,定长的目录项可能无法容纳足够长的文件名,或对目录项的空间造成浪费,而可变长的目录项也可能操作空间的浪费与碎片化。一个比较好的方法是定长的目录项,但是将文件名存储在一个 heap 数据结构中,如此需要额外对其进行管理。
共享文件
共享文件对使用是十分方便的,文件 A 可以共享给目录 B,而不需要占用额外的空间,并且 A 与副本 A 是完全相同的,当 A 发生修改时副本 A 也被修改,这被称为 链接 (link)。文件系统本身是一个 有向无环图 (Directed Acyclic Graph, DAG) 而非树状,因此维护变得复杂。
共享文件带来了一个问题,如果目录中包含磁盘地址,链接文件时必须把目标目录的磁盘地址复制到源目录中,如果目录添加新文件时,只会修改当前添加文件的目录,其他共享的目录无法被修改,这与共享文件的目录相悖。这里提出两个解决方法:
- 磁盘块不列入目录,而是列入一个与文件本身相关的小型数据结构中,目录将指向这个数据结构,即 Index Node 方式 (也是 UNIX 所采用的方式)
- 让系统建立一个类型为 LINK 的新文件,并将其放入目标目录下,新的文件只包含它所链接文件的路径名。当读该共享文件时,操作系统发现其是链接文件,则根据记录的路径名找到原始文件。这个方法被称为 符号链接 (symbolic linking)
在共享文件时 index 记录文件的所有者,建立链接时并不会修改所有者,但将 index 的链接计数加 1,所以系统知道有多少目录项指向该文件。在决定删除文件时,只有当链接计数变为 0 时系统才会真正删除该文件。对于符号链接,当原始文件被删除时,链接文件将会访问错误,但是每个符号链接需要额外的 index 节点,并且额外的开销寻找真正的文件地址。但是符号链接也会带来一个好处:只要提供机器的网络地址与文件在该机器上的路径,就可以链接任意机器上的文件。
日志结构文件系统
在 CPU 性能快速提升、磁盘容量越来越大的背景下,磁盘的性能却没有太大提升。随着缓存的增大,越来越多的操作可以不需要访问操作,就有可能满足来自文件系统高速缓存的很大一部分读请求。在有些系统中采取的提前读机制并不能获取很好的性能,并且大多数写操作是零碎的 (必须写文件、文件的 index、目录块、目录的 index),磁盘利用率很低。因此 Berkeley 设计了一种全新的文件系统 — 日志结构文件系统 (Log-structured File System, LFS)。
LFS 希望在面对一个大部分由零碎的随机写操作组成的任务,同样能够充分利用磁盘的带宽。其基本思路是将整个磁盘结构转化为一个日志,每隔一段时间或有特殊需要时,被缓冲在内存中的所有未决的写操作都被放到一个单独的段中,作为在日志末尾的一个邻接段写入磁盘。这个单独的段可能包含 index、目录块、数据块或全都有,每个段开始都是该段的摘要,说明该段都包含哪些内容。若所有的段平均在 1 MB 左右,那么几乎可以利用磁盘的完整带宽。
LFS 的设计中同样存在 index node,但 index node 分散在整个日志中而不是磁盘的某一固定位置。当 index node 被定位后,定位一个块就与通常的方式相同。但是这种设计中 index node 的寻找将变得困难,因为 index node 不再通过简单的计算得到。为了能够找到 index 节点,必须维护一个由 index node 编号索引组成的节点图,这个图中的表项 i 指向磁盘中第 i 个 index,这个图保存在磁盘上、高速缓存中,因此大多数情况下最常用的部分也在内存中。
由于磁盘并不是无限大的,日志可能会占满整个磁盘,但是幸运的是,有些块虽然被前面的日志使用着,但实际已经不再需要。因此 LFS 有一个清理线程,周期性地扫描日志进行磁盘压缩。该线程首先读入日志的第一个段的摘要,检查有哪些 index 节点与文件,并与当前 index 节点图进行对比,查看文件、index 是否有效,无效时信息将被丢弃,反之 index 与块将进入内存等待写回到下一个段中,原先的段将被标记为空闲以便存放新的日志。
日志结构文件系统的大量零碎写操作性能强于 UNIX 一个数量级,并且其读操作与大块写操作的性能并不比 UNIX 文件系统差,甚至更好。
由于 LFS 并不于现有文件系统匹配,但其面对出错的健壮性,及其思想 (保存系统下一步将要做什么的日志),被其他文件系统所借鉴。这样系统即将完成一些任务时崩溃,重启后通过查看日志即可获取崩溃前的任务并完成。这样的文件系统被称为 日志文件系统, NTFS、ext3、ReiserFS 等都是这种系统的应用。
日志文件系统先写入一个日志项,列出将要完成的任务,当日志项被写入后将开始执行任务,所有的任务都完成后将擦除日志项。如果系统这时候崩溃,可以在系统恢复后通过检查日志项,确定是否有未完成的操作西药继续完成。为了让系统工作,被写入日志的操作必须是 幂等的,意味着只要有必要操作就可以执行多次,并且不会带来破坏。为了可靠性,系统可以添加 原子事务 的概念。
虚拟文件系统
在计算机的不同分区可能使用不同的文件系统,这在现实中很常用,因此操作系统使用 虚拟文件系统 (Virtual File System, VFS) 的概念将不同文件系统统一成一种有序的结构。关键思路是抽象出所有文件系统的共同部分,并将这部分代码放在单独的一层,VFS 调用底层实际的文件系统来管理数据。
VFS 并不关心底层如何存储数据或数据被存储到哪里,正如 Sun 建立 VFS 最初的目的是使用 网络文件系统
(Network File System, NFS) 协议的远程文件系统,因此 VFS 只关心实际的文件系统提供的功能。通常支持一些必要的对象类型,包括超块 (描述文件系统)、v
节点 (描述文件) 和目录,这些中的每一个都有实际文件系统必须支持的相关操作。VFS 也会有一些供自己使用的内部数据结构,包括用于跟踪用户进程中所打开文件的装载表和文件描述符的数组。
在实际运行时,文件系统需要先向 VFS 进行注册,将 VFS 所支持的功能,通过提交对应的功能函数表,告知 VFS 相关的真实操作。当用户通过调用 POSIX 接口进行文件操作时,由 VFS 管理用户打开的文件,并将 POSIX 接口转换为真正的文件系统调用。
文件系统管理和优化
磁盘管理
文件通常存放在磁盘上,所以对磁盘空间的管理是设计者要考虑的一个主要问题。存储一个有 n 个字节的文件可以有两种策略,分配 n 个字节的连续空间,或把文件分成很多连续的块。在存储管理系统中,分段处理和分页处理也需要进行权衡。由于分段需要移动文件,但磁盘相对内存操作慢得多,因此文件系统经常采用分页的方式。
块大小
一旦决定分页,最直观的问题即页大小。大的块尺寸意味着小文件将浪费大量磁盘空间,而小的块尺寸则会有大量文件跨越多个块,因此需要多次寻道与旋转延迟才能找到它们,从而降低性能。
一项关于 VU 计算机系研究数据表明,VU \(59.13\%\) 的文件都小于等于 4 KB,而 \(90.84\%\) 的文件都小于等于 64 KB,其中文件大小的中位数是 2474 字节。假设磁盘每道 1 MB,其旋转时间 8.33 ms,平均寻道时间 5 ms,那么读取一个 k 字节的块需要: \[5 + 4.165 + \frac{k}{1000000} * 8.33\]
由于对一个块的访问主要由寻道时间与旋转延迟决定,因此在访问磁盘块时,取到的数据越多越好,因此数据率随着磁盘块的增大而线性增大。对于空间利用率来说,随着磁盘块的增大而降低。性能与利用率是天生矛盾的。在历史上文件系统的块大小设置在 1 ~ 4
KB 之间,随着磁盘的容量增加,块大小也可以考虑提升到 64 KB 以增加性能而接受空间浪费。
记录空闲块
对于如何记录空闲块的方式,主要有两种:
- 磁盘块链表,每个块中包含尽可能多的空闲磁盘号,并采用空闲块存放空闲列表
- 位图
使用记录连续块的方式优化链表存储,但磁盘碎片严重时,这种方式将比单独记录的方法效率低。并且指针块可以存储在内存中,但在指针块快满时对文件的删除操作,可能使指针块溢出,从而交换新的指针块,导致大量的磁盘 IO。避免过多的磁盘 IO 可以将内存中始终保持半满的指针块,在释放与写入时都可以减少频繁的 IO。
对于位图的实现,内存中只保留一个块是可能的,在块满了或空了的情况下再进行磁盘 IO。另外通过位图在单一块上进行分配操作,磁盘块会较为紧密地聚集在一起,从而减少了磁盘臂的移动。并且如果内核是分页的,可以将固定大小的块读入虚拟内存,在需要时将位图页面调入。
磁盘配额
多用户操作系统往往会为不同用户分配一定大小的空间,防止用户占用太多的磁盘空间。当用户修改文件时,文件属性中的所有者可以记录该文件属于谁,关于文件大小的变动都会记录到所有者的配额上。此外还会有一个指向所有者的配额指针,用以记录所有者配额的详细信息,并在文件变动时检查配额是否超出。配额中存在软配额与硬配额,前者可以超过但会发生警告,后者不可超出。
文件系统备份
文件系统的损坏将造成数据的大量丢失,最直接的方法就是备份。备份有两个好处,从 意外的灾难 或 错误的操作 中恢复数据,但备份可能耗时又浪费空间,因此快又好的备份是十分必要的。
- 对于临时文件与设备文件是不需要备份的,大部分的可执行文件可以再获取也是不需要备份的,因此只备份特定目录与其下的文件即可。
- 对从没有修改过的文件进行多次备份也是一种浪费,因此我们需要用到增量备份的思想。在一次全量备份上只记录最新修改的部分,这样加快了备份的时间、减少了空间的浪费,但对恢复来说是一种挑战,恢复时必须从全量备份开始逐次进行恢复。
- 备份是否需要压缩算法,也是需要考量的。因为备份设备上的一个坏点即可破坏整个文件,导致数据无法读取。
- 对活动的文件系统进行备份是很难的,极有可能发生文件的不一致性。因此人们修改了转储算法,记录下文件系统的瞬时快照 (复制关键的数据结构),然后需要把将来对文件和目录所做的修改复制到块中,而非处处更新。
转储分为 物理转储 与 逻辑转储。前者从磁盘的第 0 块开始,将全部的磁盘块按序输出到磁带,直到最后一块。这种方案很简单,可以确保万无一失。但是物理转储无法跳过选定目录,也无法增量转储。后者从一个或多个指定的目录开始,递归地转储其给定的基准日期后所更改的全部文件和目录。
文件系统的一致性
如果在修改过的磁盘块全部写回之前系统崩溃,则文件系统将处于不一致状态,如果未被写回的块是 index、目录块或含有空闲表的块时,这个问题尤为严重。因此在解决不一致性的问题上,很多系统都带有检验系统一致性的程序。在系统启动 (或因崩溃重启) 时可以运行该程序。一致性检测一般分为两种:块的一致性检测与文件的一致性检测。
在检测块的一致性时,程序构造两张表,每张表中为每个块设立一个计数器,都初始化为 0。接着程序使用原始设备读取全部的 index 节点,忽略文件结构,只返回从零开始的所有磁盘块。以下展示了块一致性检测中可能出现的问题。b 情况 块丢失,即块没有出现在任意一张表中,虽然没有危害,但是造成了资源的浪费,解决方案也很简单,将其加入到空闲表中即可。c 情况出现了重复的空闲块,此时只要重新建立空闲表即可。d 情况最为复杂,在两个或多个数据中都出现一同一个块,如果删除其中一个文件则会出现块处于一种使用与空闲的状态,而删除所有相关文件则会让其在空闲表中重复多次。一种解决方案是复制该块,并让引用这个块的文件得到相同的一份副本,消除文件系统的不一致性,并将错误报告给用户。
除了文件外目录也需要检查,同样使用计数器记录文件 (并非块) 的使用次数。在检测完成后得到一张由 index 节点号索引的表,说明每个文件被多少个目录包含,并与 index node 中的链接数目比较。index 中链接数目大于计数器的数目,则会导致释放完所有文件后 index 依然不能释放相关磁盘块,造成磁盘空间浪费。而小于计数器数目时,index 为 0 后会立即释放磁盘块,index 可能被很快用作其他文件,导致原先的文件内容错误。这两种方法都可以通过设置正确的 index node 链接数目解决。
文件系统性能
高速缓存
最常用的减少磁盘直接访问的方案即 块高速缓存 (block cache),block cache 逻辑上属于磁盘,但实际被存储于内存中。在读请求中首先检测是否在 cache 中,命中 cache 时将不需要进行磁盘访问,从而提升性能。
cache 的使用,需要与磁盘对块进行交换,因此在内存所涉及的页面置换算法都可以使用,与分页相比 cache 的引用很不频繁,因此 LRU 顺序链表是可行的。但关键问题是,一个关键块被修改并保存在 cache 中没有写回,当系统崩溃时将造成数据不一致性。为解决这一问题,可以将块分类为 index、目录项、数据等不同的类型,将最近不再使用的块放在 LRU 链表的头部,对于很快将要再次使用的块 (如正在写入的「部分满数据块」) 放入链表的尾部,让其在高速缓存中保存更长的时间。除了数据块外的其他类型块,都应在修改后立即写回磁盘,保证文件系统的一致性。数据的一致性,可以通过定时的强制将修改的块写回磁盘,保证一定时间间隔内的数据不会丢失。而立即将所有修改的块写回磁盘被称为 通写高速缓存 (write-through cache),这种技术相比非通写需要更多的磁盘 IO。
块提前读
将要使用到的块提前读入 cache,从而提高 cache 命中率。特别地,很多文件是顺序读取的,因此系统提前读取到数据,以便提高 cache 命中率,从而减少磁盘 IO。但是对于随机访问的文件,提前读并不能优化性能,甚至可能降低性能。
减少磁盘臂运动
另一项提升文件系统性能的重要技术是将有可能顺序读取的块放在一起,最好是同一柱面上,减少磁盘臂移动次数。
磁盘碎片整理
最初安装操作系统时,从磁盘开始位置,一个接一个的安装程序与文件,所有的空闲磁盘空间都被存放在一个大的空闲区域内。随着系统的使用,文件的删除与创建,磁盘上会产生很多碎片,此时创建一个新文件可能会使其块散布在整个磁盘上,造成性能下降。不过可以通过一种方式恢复:移动文件使他们相邻,并把大部分空闲块放到一个或多个大的连续区域中。
不过碎片整理的过程中,有些文件并不能被移动,包含页文件、休眠文件和日志,移动这些文件所需的管理成本大于移动他们所获得的收益。这些文件使用固定大小的连续区域,因此不需要进行碎片整理。如果这些文件在分区的末尾,此时用户想缩小分区的时候,将会把它们删除,改变分区大小后再进行重建。
由于 ext2 与 ext3 的选择磁盘块的方式,在磁盘碎片整理上一般不会遭受像 Windows 那样的困难,因此很少需要手动进行碎片整理。此外,SSD 并不受磁盘碎片的影响,并且频繁的碎片整理可能影响其寿命。