【梳理】简明操作系统原理 一个简易文件系统的实现(内附文档高清截图)

参考教材:
Operating Systems: Three Easy Pieces
Remzi H. Arpaci-Dusseau and Andrea C. Arpaci-Dusseau
在线阅读:
http://pages.cs.wisc.edu/~remzi/OSTEP/
University of Wisconsin Madison 教授 Remzi Arpaci-Dusseau 认为课本应该是免费的
————————————————————————————————————————
这是专业必修课《操作系统原理》的复习指引。
在本文的最后附有复习指导的高清截图。需要掌握的概念在文档截图中以蓝色标识,并用可读性更好的字体显示 Linux 命令和代码。代码部分语法高亮。
操作系统原理不是语言课,本复习指导对用到的编程语言的语法的讲解也不会很细致。如果不知道代码中的一些关键字或函数的具体用法,你应该自行查找相关资料。

一个简易文件系统的实现

在本章,我们实现一个名为VSFS(Very simple file system)的文件系统。

1、构思一个文件系统,主要通过两方面:
一是数据结构。在磁盘上需要采用什么结构来存储数据和元数据呢?
二是访问方法。当进程发出open()、read()、write()等系统调用时,应当怎样处理?需要读写哪些数据结构?处理效率如何?

2、首先我们设置一个块大小:4 KB。这个块大小比较常见。设置块大小的原因主要有二:
(1)防止单次传输的数据量过小进而导致性能低下。
(2)整体进行管理。我们不可能为每个字节记录相应的用于管理的数据。

3、文件系统的结构我们设计成这样:

首先是数据区(data region)。顾名思义,这部分自然用来存放数据。
接下来是索引节点(index node)区。索引节点包含了文件的元数据,包括文件大小、所在的块、访问权限、修改日期、访问日期、文件类型等信息。所有的文件系统也有类似索引节点的数据结构。ib和db分别代表索引节点位映像(bitmap)、数据区位映像。位映像的每一位都对应一个块,标记其是否已经使用。S区域叫做超级块(super block),主要包括文件系统本身的基本信息,比如索引节点数、数据块数、各区域的起始位置等。S区也很可能包含一个魔数,方便应用程序快速识别文件系统的类型。

4、Ext2的索引节点至少包含以下信息:

回到我们实现的VSFS上来。
一个索引节点需要包含一些指针,指向文件在磁盘上存储的块,称为直接指针(direct pointer)。如果文件比较大,就会把这个区域用完。所以我们需要想其它办法。
一个常用的办法是:把存储直接指针的区域的一个指针改为间接指针(indirect pointer)。当文件存储的块数比较多的时候,就分配一个新的块,里面包含了额外的指针。而间接指针指向这个新分配的区域。如果还是不够,就把这个分配再做一次,直到直接指针的数量足够为止。
还可以使用二重间接指针(double indirect pointer):在索引节点中加入一个指针,指向一个块,块里全部是指向数据区的直接指针。当然,还可以再进一步,使用三重间接指针(triple indirect pointer)。原理很相似,这里不多说。
这种思想称为多级索引(multi-level index)。
Linux ext2和ext3,NetApp的WAFL都采用多级索引方式。而SGI XFS和Linux ext4则采用“指针+长度”的方式来描述数据在磁盘上的位置。一个“指针+长度”结构可以代替大量的直接指针来描述一段连续的数据的存储位置。
研究表明,计算机中存储的绝大多数文件都很小。所以,索引节点一般都会留有一些位置存储直接指针;只有在存储更大的文件的时候,才启用多级索引或基于范围(extent)的表示——(头)指针与长度。多数时候,一个索引节点包含12个直接指针。当块大小为4 KB时,这种直接的表示方法最多可以表示不超过48 KB的小文件在磁盘上的分布。
当然,计算机在发展,文件系统也是。一些新型的文件系统可能采用完全不同的方式来索引文件。

5、下面我们为VSFS构造一个目录的结构。目录的本质是一张表,里面存储了该目录下的各个文件的基本信息。我们采用这样的结构:

目录的每一项都有两个成员:与该目录关联的文件的索引节点号,和该项记录的长度。由上图可以看出,这个目录记录了5项,分别是当前目录、父目录、foo、bar、foobar_is_a_pretty_longname的信息。strlen一项代表文件名的长度(文件名末尾的’\0’也算)。foobar_is_a_pretty_longname正好比foo、bar多出24个字符,导致记录它的项的长度从12增加到了36。
如果删除一个目录下的文件,那么目录中对应的记录就会被擦掉并留空。要有专门的方法来表示这种情况(例如将索引号记0)。当添加新的项时,可以直接写入中间的空白部分。
文件系统常常把目录也当作一种特殊的文件对待,所以一个目录也有它的索引节点号。如果采用多级索引,那么新申请的存储间接指针的块自然也会有索引节点号。
线性表并不是存储目录的唯一方法。例如XFS就采用B树作为目录结构。

6、如果用链表实现索引节点或等效的结构,会导致顺序访问和随机访问(尤其后者)的性能大幅降低。为了提升性能,有的文件系统会在内存中保留这种表,而不是向刚才说的那样将指向下一片连续区域的指针存在专门的数据块中。文件分配表(file allocation table,FAT)采用的就是这种基本结构。FAT将若干个连续扇区视为一个簇(cluster),以簇为单位来存取数据。表中每一项的索引是簇编号,而项的内容是所属文件的下一个数据块或EOF(End of File)标记。FAT与UNIX文件系统还有其它的不同,比如没有索引节点,使用目录中的项来存放元数据并指向文件的第一个数据块,这也同样为实现硬链接提供了基础。

7、位映像只是一种管理空闲空间的方法。一些早期的文件系统使用专门的空闲列表,用一个指针指向空闲空间的第一个块,块内具有指向下一个空闲块的指针。当有块需要被写入时,相应的头指针就要改变。
现代的文件系统则采用较复杂的结构。XFS使用B树来记录空闲块。

8、如果使用位映像来标记空闲区域,那么创建新文件时,申请新的索引节点和数据块后,要把相应的位标为已占用。
文件系统在为新文件分配空间时,常常使用预分配(pre-allocation)的启发式策略:尽量查找一段连续的空闲区域。

9、下面通过一个例子演示VSFS如何实现读取文件:

设要打开一个文件 /foo/bar,一共12 KB,正好占了三个块。
当调用open(bar)时,文件系统需要先找到根目录的索引节点,然后依次查找foo目录和文件bar的索引节点,来获得诸如访问权限和文件大小之类的基本信息。文件系统根据需要打开的文件的绝对路径去查找需要的索引节点。通过索引节点号可以找到索引节点的位置。根没有父目录,要通过专门的方法来访问根的索引节点(想一想:在数据结构的学习中,通过一个专门的变量指向二叉树的根节点)。在UNIX文件系统中,根节点的索引节点号一般都是2。
找到根的索引节点后,就从索引节点中读出根目录的数据块的分布位置并进行访问。此时,文件系统通过索引节点中的指针来读取根目录文件,查找包含foo的项。找到后,文件系统就去查找foo的索引节点。如此递归查找,直到找到bar的索引节点。找到以后,就将其读入内存,检查权限。权限检查通过后,就分配一个文件描述符,并更新已打开文件表,将文件描述符返回给用户。
打开以后,程序就可以进行read()和lseek()操作了。进行读操作时,先从文件的索引节点中找到需要读取的数据块,读取,然后在索引节点中修改最后访问时间。同时,内存中的已打开文件表也要被更新,因为要修改偏移量。
读取完毕关闭文件的时候,工作量就少多了:只需要释放文件描述符。关闭文件不引发磁盘IO。
open引发的IO数量与路径的级数成正比例,因为每访问一级子目录都需要读取对应的索引节点和目录文件。但是IO个数不是决定open操作的耗时的唯一因素。如果目录中的文件非常多,读完目录文件就需要较长的时间。

10、我们继续通过一个例子来演示VSFS的写入文件操作:

如果需要创建新文件并写入或追加内容,那么还需要分配新的块。创建文件时,需要更新索引节点的位映像,还要在新文件所在目录写入新的条目,所在目录对应的索引节点也要更新,然后为新文件创建索引节点。创建一个空文件不写入数据区,但需要在找不到该文件的索引节点时创建新的索引节点。
写入新文件时,因为要在数据区写入,所以索引节点和数据区的位映像都要更新。先后顺序是:先读取新文件的索引节点,在数据区位映像中查找一个数据块,确认对应的块空闲,就将这一位标记为占用,然后在数据区写入数据,并更新索引节点。

11、由上面两个例子我们可以看到,读写操作会产生很多IO。怎样提升磁盘IO效率呢?答案是:缓存。非常多的文件系统在系统启动时申请一部分内存空间作为缓存。如果这段空间是静态分配的,它们常常占据内存的10 %。不过,现代的文件系统采用动态分配的方式。具体来说,是采用统一页缓存(unified page cache)的方式,使得在内存和文件系统之间的分配方式更灵活。这部分缓存占用的空间视需要而定。
存储空间的静态分配和动态分配各有优缺点。静态分配更容易实现,而且性能更容易预测,还容易保证每个用户都能尽量公平地分得空间。但动态分配则令空间利用率更好,但实现更复杂,并且有时还会导致性能降低:一些用户因为某段时间对空间需求较少,分配给它们的资源被分配给了其它用户;而当这些用户又需要更多空间时,可能需要等待其它用户释放资源。

12、出于性能的考虑,写入缓存会延迟写入。但是如果在存盘之前发生了系统故障或断电,未存盘的数据就会丢失。如果确实需要立即将数据写入磁盘,应用程序往往调用fsync(),或者通过直接I / O(Direct I / O)或Raw Disk方式绕过文件系统读写磁盘。不过,多数程序在写入缓存上都遵循文件系统的安排。

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章