文件的存储实现
# 引言
文件存储的实现的关键问题是记录各个文件分别用到哪些磁盘块。不同操作系统采用不同的方法。
# 连续分配
最简单的分配方案是把每个文件作为一连串连续数据块存储在磁盘上。所以,在块大小为1KB的磁盘上,50KB的文件要分配50个连续的块。对于块大小为2KB的磁盘,将分配25个连续的块。
连续磁盘空间分配方案有两大优势。首先,实现简单,记录每个文件用到的磁盘块简化为只需记住两个数字即可:第一块的磁盘地址和文件的块数。给定了第一块的编号,一个简单的加法就可以找到任何其他块的编号。
其次,读操作性能较好,因为在单个操作中就可以从磁盘上读出整个文件。只需要一次寻找(对第一个块)。之后不再需要寻道和旋转延迟,所以,数据以磁盘全带宽的速率输入。可见连续分配实现简单且具有高的性能。
但是,连续分配方案也同样有相当明显的不足之处:随着时间的推移,磁盘会变得零碎。为了了解这是如何发生的,请考察图4-10b。这里有两个文件(D和F)被删除了。当删除一个文件时,它占用的块自然就释放了,在磁盘上留下一堆空闲块。磁盘不会在这个位置挤压掉这个空洞,因为这样会涉及复制空闲块之后的所有文件,可能会有上百万的块。结果是,磁盘上最终既包括文件也有外部碎片。
# 链表分配
存储文件的第二种方法是为每个文件构造磁盘块链表,如图4-11所示。每个块的第一个字作为指向下一块的指针,块的其他部分存放数据。
与连续分配方案不同,这一方法可以充分利用每个磁盘块。不会因为磁盘碎片(除了最后一块中的内部碎片)而浪费存储空间。同样,在目录项中,只需要存放第一块的磁盘地址,文件的其他块就可以从这个首块地址查找到。
另一方面,在链表分配方案中,尽管顺序读文件非常方便,但是==随机存取却相当缓慢==。要获得块n,操作系统每一次都必须从头开始,并且要先读前面的n-1块。显然,进行如此多的读操作太慢了。
而且,由于指针占去了一些字节,每个磁盘块存储数据的字节数不再是2的整数次幂。虽然这个问题并不是非常严重,但是怪异的大小确实降低了系统的运行效率,因为许多程序都是以长度为2的整数次幂来读写磁盘块的。由于每个块的前几个字节被指向下一个块的指针所占据,所以要读出完整的一个块,就需要从两个磁盘块中获得和拼接信息,这就因复制引发了额外的开销。
# 索引方式
如果取出每个磁盘块的指针字,把它放在内存的一个表中,就可以解决上述链表的两个不足。图4-12表示了图4-11所示例子的内存中表的内容。这两个图中有两个文件,文件A依次使用了磁盘块4、7、2、10和12,文件B依次使用了磁盘块6、3、11和14。利用图4-12中的表,可以从第4块开始,顺着链走到最后,找到文件A的全部磁盘块。同样,从第6块开始,顺着链走到最后,也能够找出文件B的全部磁盘块。这两个链都以一个不属于有效磁盘编号的特殊标记(如-1)结束。内存中的这样一个表格称为文件分配表(File Allocation Table,FAT)。
按这类方式组织,整个块都可以存放数据。进而,随机存取也容易得多。虽然仍要顺着链在文件中查找给定的偏移量,但是整个链表都存放在内存中,所以不需要任何磁盘引用。与前面的方法相同,不管文件有多大,在目录项中只需记录一个整数(起始块号),按照它就可以找到文件的全部块。
这种方法的主要缺点是必须把整个表都存放在内存中。对于200 GB的磁盘和1KB大小的块,这张表需要有2亿项,每一项对应于这2亿个磁盘块中的一个块。每项至少3个字节,为了提高查找速度,有时需要4个字节。根据系统对空间或时间的优化方案,这张表要占用600MB或800MB内存,不太实用。很显然FAT方案对于大磁盘而言不太合适。
# iNode
最后一个记录各个文件分别包含哪些磁盘块的方法是给每个文件赋予一个称为i节点(index-node)的数据结构,其中列出了文件属性和文件块的磁盘地址。图4-13中是一个简单例子的描述。给定i节点,就有可能找到文件的所有块。相对于在内存中采用表的方式而言,这种机制具有很大的优势,即只有在对应文件打开时,其i节点才在内存中。如果每个i节点占有n个字节,最多k个文件同时打开,那么为了打开文件而保留i节点的数组所占据的全部内存仅仅是kn个字节。只需要提前保留少量的空间。
这个数组通常比上一节中叙述的文件分配表(FAT)所占据的空间要小。其原因很简单,保留所有磁盘块的链接表的表大小正比于磁盘自身的大小。如果磁盘有n块,该表需要n个表项。由于磁盘变得更大,该表格也线性随之增加。相反,i节点机制需要在内存中有一个数组,其大小正比于可能要同时打开的最大文件个数。它与磁盘是10GB、100GB还是1000GB无关。