磁盘直接访问的特点在文件实现时提供了灵活性。在几乎每种情况下,很多文件都是存储在同一个磁盘上的。主要的问题是,如何为这些文件分配空间,以便有效使用磁盘空间和快速访问文件。
磁盘空间分配的主要常用方法有三个:连续分配、链接分配和索引分配。每个方法各有优缺点。虽然有些系统对这三种方法都支持。但是更为常见的是,一个系统只对同一文件系统类型的所有文件采用一种方法。
连续分配方法要求,每个文件在磁盘上占有一组连续的块。磁盘地址为磁盘定义了一个线性排序。有了这个排序,假设只有一个作业正在访问磁盘,在块 b 之后访问块 b+1 通常不需要移动磁头。当需要磁头移动(从一个柱面的最后扇区到下一个柱面的第一扇区)时,只需要移动一个磁道。因此,用于访问连续分配文件的所需寻道数量最小,在确实需要寻道时所需的寻道时间也最小。
文件的连续分配可以用首块的磁盘地址和连续的块数来定义。如果文件有 n 块长并从位置 b 开始,则该文件将占有块 b,b+1,b+2,…,b+n-1。每个文件的目录条目包括起始块的地址和该文件所分配区域的长度,参见图 1。
连续分配文件的访问非常容易。对于顺序访问,文件系统会记住上次引用的块的磁盘地址,如需要可读入下一块。对于直接访问一个文件的从块 b 开始的第 i 块,可以直接访问块 b+i。因此,连续分配支持顺序访问和直接访问。
不过,连续分配也有一些问题。一个难题是,为新文件找到空间。用于管理空闲空间的系统决定了这个任务如何完成,虽然可以使用任何管理系统,但是有的系统会比其他的要慢。
连续分配问题可以作为通用动态存储分配问题的一个具体应用,即如何从一个空闲块列表中寻找一个满足大小为 n 的空间。从一组空闲块中寻找一个空闲块的最为常用的策略是,首次适合和最优适合。模拟结果显示在时间和空间使用方面,首次适合和最优适合都要比最坏适合更为高效。首次适合和最优适合在空间使用方面不相上下,但是首次适合一般更快。
所有这些算法都有外部碎片的问题。随着文件的分配和删除,可用磁盘空间被分成许多小片。只要空闲空间分成小片,就会存在外部碎片。当最大连续片不能满足需求时就有问题;存储空间分成了许多小片,其中没有一个足够大以存储数据。因磁盘存储总量和文件平均大小的不同,外部碎片可能是个小问题,但也可能是个大问题。
为了防止外部碎片引起的大量磁盘空间的浪费,将整个文件系统复制到另一个磁盘。原来的磁盘完全变成空的,从而创建了一个大的连续空闲空间。然后,通过从这个大的连续空闲空间采用连续分配方法,将这些文件复制回来。这种方案将所有空闲空间有效合并起来,解决了碎片问题。
这种合并的代价是时间,而且大硬盘的代价可能特别高。合并这些磁盘空间可能需要数小时,可能每周都需进行。有些系统要求,这个功能线下执行且文件系统要卸载。在这停机期间不能进行正常操作,因此生产系统应尽可能地避免合并。大多数的需要整理碎片的现代系统能够和正常的系统操作一起在线执行合并,但是性能下降可能很明显。
连续分配的另一个问题是,确定一个文件需要多少空间。当创建一个文件时,需要找到并分配它所需空间的总数。创建者(程序或人员)又如何知道所创建文件的大小?在某些情况下,这种判断可能相当简单(例如,复制一个现有文件),一般来说,输出文件的大小可能难以估计。
如果为文件分配的空间太小,则可能会发现文件无法扩展。特别是对于最优适合的分配策略,文件两侧的空间可能已经使用。因此,不能在原地让文件更大。
这时,有两种可能办法:
即使文件所需的空间总量事先已知,预先分配仍可能很低效。一个文件在很长时间内增长缓慢(数月或数年),仍必须按它的最终大小来分配足够空间,即使这个空间很长时间内不用。因此,该文件有一个很大的内部碎片。
为了最小化这些缺点,有些操作系统使用连续分配的修正方案。这里,最初分配一块连续空间。以后,当这个数量不够时,会添加另一块连续空间(称为扩展)。然后,文件块的位置就记录为:地址、块数、下一扩展的首块的指针。
在有些系统上,文件所有者可以设置扩展大小,但是如果所有者不正确,这种设置会导致低效。如果扩展太大,内部碎片可能仍然是个问题;随着不同大小的扩展的分配和删除,外部碎片可能也是个问题。商用 Veritas 文件系统使用扩展来优化性能。Veritas 是标准 UNIX UFS 的高性能替代。
链接分配解决了连续分配的所有问题。采用链接分配,每个文件是磁盘块的链表;磁盘块可能会散布在磁盘的任何地方。目录包括文件第一块和最后一块的指针。例如,一个有5块的文件可能从块 9 开始,然而是块 16、块 1、块 10,最后是块 25(图 2)。每块都有下一块的一个指针。用户不能使用这些指针。因此,如果每块有 512 字节,并且磁盘地址(指针)需要 4 字节,则用户可以使用 508 字节。
要创建一个新文件,只需在目录中增加一个新的条目。采用链接分配,每个目录条目都有文件首个磁盘块的一个指针。这个指针初始化为 null,表示一个空的文件。大小字段也设置为 0。写文件导致空闲空间管理系统找到一个空闲块,这个新块会被写入,并链接到文件的尾部。读文件,只需按照块到块的指针来读块。
采用链接分配没有外部碎片,空闲空间列表的任何块可以用于满足请求。当创建文件时,并不需要说明文件的大小只,要有可用的空闲块,文件就可以继续增长。因此,无需合并磁盘空间。
然而,链接分配确实有缺点。主要问题是,它只能有效用于顺序访问文件。要找到文件的第 i 个块,必须从文件的开始起,跟着指针,找到第 i 块。每个指针的访问都需要一个磁盘读,有时需要磁盘寻道。因此,链接分配不能有效支持文件的直接访问。
另一个缺点是指针所需的空间。如果指针需要使用 512 字节块的 4 字节,则 0.78% 的磁盘空间会用于指针,而不是其他信息。每个文件需要比原来稍多的空间。
这个问题的通常解决方案是,将多个块组成簇,并按簇而不是按块来分配。例如,文件系统可以定义一个簇为 4 块,在磁盘上仅以簇为单位来操作。这样,指针所占的磁盘空间的百分比就要小得多。这种方法使得逻辑到物理块的映射仍然简单,但提高了磁盘吞吐量(因为需要更少的磁头移动),并且降低了块分配和空闲列表管理所需的空间。这种方法的代价增加了内部碎片;如果一个簇而不是块没有完全使用,则会浪费更多空间。簇可以改善许多算法的磁盘访问时间,因此用于大多数操作系统。
链接分配的另一个问题是可靠性。回想一下,文件是通过散布在磁盘上的指针链接起来的,操作系统软件错误或磁盘硬件故障可能导致获得一个错误指针,这个错误可能会导致链接到空闲空间列表或链接到另一个文件。一个不完全的解决方案是,采用双向链表;另一个是,每块存储文件名称和相对块号。然而,这些方案为每个文件增加了更多额外开销。
链接分配的一个重要变种是文件分配表(FAT)的使用。这个简单而有效的磁盘空间分配方法用于 MS-DOS 操作系统。每个卷的开头部分的磁盘用于存储该表。在该表中,每个磁盘块都有一个条目,并可按块号来索引。FAT 的使用与链表相同。目录条目包含文件首块的块号。通过这个块号索引的表条目包含文件的下一块的块号。这条链会继续下去,直到最后一块;而最后一块的表条目的值为文件结束值。未使用的块用 0 作为表条目的值来表示。为文件分配一个新块只要简单找到第一个值为 0 的 FAT 条目,用新块的地址替换前面文件结束值,用文件结束值替代 0。由块 217、618、339 组成的文件的 FAT 结构如图 3 所示。
如果不对 FAT 采用缓存,FAT 分配方案可能导致大量的磁头寻道时间。磁头必须移到卷的开头,读入 FAT,找到所需块的位置,再移到块本身的位置。在最坏的情况下,每块都需要移动两次。优点是改善了随机访问时间;因为通过读入 FAT 信息,磁头能找到任何块的位置。
链接分配解决了连续分配的外部碎片和大小声明的问题。然而,在没有 FAT 时,链接分配不能支持髙效的直接访问,因为块指针与块一起分散在整个磁盘上,并且必须按序读取。索引分配通过将所有指针放在一起,即索引块,解决了这个问题。
每个文件都有自己的索引块,这是一个磁盘块地址的数组。索引块的第 i 个条目指向文件的第 i 个块。目录包含索引块的地址(图 4)。当查找和读取第 i 个块时,采用第 i 个索引块条目的指针。
当创建文件时,索引块的所有指针都设为 null。当首次写入第块时,先从空闲空间管理器中获得一块,再将其地址写到索引块的第 i 个条目。
索引分配支持直接访问,并且没有外部碎片问题,因为磁盘的任何空闲块可以满足更多空间的请求。然而,索引分配确实浪费空间。索引块指针的开销通常大于链接分配的指针开销。考虑一下常见情况,即一个文件只有一块或两块。采用链接分配,每块只浪费一个指针的空间。采用索引分配,即使只有一个或两个指针是非空的,也必须分配一个完整的索引块。
这一点提出了一个问题:索引块应为多大?每个文件必须有一个索引块,因此需要索引块尽可能小。然而,如果索引块太小,它不能为大的文件存储足够多的指针。因此,必须采取一种机制,以处理这个问题。此目的的机制包括:
采用这种方法,一个文件的块数可以超过许多操作系统所用的 4 字节的文件指针所能访问的空间。32 位指针只能访问 232 字节,或 4GB。许多 UNIX 和 Linux 现在支持 64 位的 文件指针。这样的指针允许文件和文件系统为数艾字节。ZFS 文件系统支持 128 位的文件 指针。
索引分配方案与链接分配一样在性能方面有所欠缺。尤其是,虽然索引块可以缓存在内存中,但是数据块可能分布在整个卷上。
已上讨论的分配方法,在存储效率和数据块访问时间上有所不同。当操作系统选择合适方法来实现时,这两者都是重要依据。
在选择分配方法之前,需要确定系统是如何使用的。以顺序访问为主的系统和以随机访问为主的系统,不应采用相同的方法。
对于任何类型的访问,连续分配只需访问一次就能获得磁盘块。由于可以在内存中容易地保存文件的开始地址,所以可以立即计算第 i 块(或下一块)的磁盘地址,并直接读取。
对于链接分配,也可以在内存中保留下一块的地址,并直接读取。对于顺序访问,这种方法很好;然而,对于直接访问,对第i块的访问可能需要读i次磁盘。这个问题表明了,为什么链接分配不适用于需要直接访问的应用程序。
因此,有的系统通过使用连续分配支持直接访问的文件,通过链接分配支持顺序访问的文件。对于这些系统,在创建文件时必须声明使用的访问类型。用于顺序访问的文件可以链接分配,但不能用于直接访问。
用于直接访问的文件可以连续分配,能支持直接访问和顺序访问,但是在创建时必须声明其最大的文件大小。在这种情况下,操作系统必须具有适当的数据结构和算法,来支持两种分配方法。
文件可以从一种类型转成另一种类型,创建一个所需类型的新文件,将原来文件的内容复制过来,然后可以删除旧文件,再重新命名新文件。
索引分配更为复杂。如果索引块已在内存,则可以进行直接访问。然而,在内存中保存索引块需要相当大的空间。如果没有这个内存空间,则可能必须先读取索引块,再读取所需的数据块。对于两级索引,可能需要读取两次索引块。对于一个极大的文件,访问文件末尾附近的块需要首先读取所有的索引块,最后才能读入所需的数据块。因此,索引分配的性能取决于索引结构、文件大小以及所需块的位置。
有些系统将连续分配和索引分配组合起来:对于小文件(只有 3 或 4 块的)采用连续分配;当文件增大时,自动切换到索引分配。由于大多数文件较小,小文件的连续分配的效率又高,所以平均性能还是相当不错的。
还可以采用许多其他优化方法。鉴于 CPU 速度和磁盘速度的差距,操作系统采用数千条指令以节省一些磁头移动,不是不合理的。此外,随着时间的推移,这种差距会增加,以致于操作系统采用数十万条指令来优化磁头移动也是值得的。