目录
文件系统的结构是怎么样的?
磁盘的相关知识(纯属废话,不看也罢)
要介绍文件系统的结构,首先要了解磁盘的相关知识。磁盘之所以能够成为大多数操作系统的二级存储设备,有以下两点原因:
- 磁盘可以在指定位置重新写入
- 磁盘支持随机访问(任意位置访问数据)
这两点看起来理所当然,但和其他存储设备对比时候就可以看出磁盘的优势了。对比于ROM和磁带存储,显然ROM无法做到指定位置写入数据,磁带可以写入,但是也很难做到指定位置写入,随机访问更是天方夜谭,由此来看这两点原因就合情合理了。
磁盘的结构相信大家都很清楚了,如果不清楚也没关系,因为我也不清楚。。。
但有几点还是需要强调一下,磁盘的最小数据单元是扇区,每个扇区大小从32B到4096B不等,最常见的一种是512B。但为了提高IO的效率,一般IO操作的最小数据单元是按照block为单位的,一个block由一个或者多个扇区组成(想象一下极端情况,如果每个扇区只有1B,如果按照扇区为单位,1MB的数据要操作1M次,但如果把1K扇区合成一个block,1MB操作只需要1K次)
操作系统的分层设计
类似于计算机网络的设计理念,文件系统设计的时候也采用分层设计的方法,虽然不同文件系统的分层方式略有不同,但总的来说可以看作以下几部分。
我把图片放到右边方便读者边看文字边对照图片。
接下来介绍各层的作用。
IO control
IO control层由磁盘驱动和中断控制程序组成。
磁盘驱动可以被看作一个翻译官,他得到的是高层次的指令,类似与:“从XXblock读出XX个数据”,输出的是低层次的硬件控制指令,这个低层次的硬件控制指令会发送给硬件控制器,硬件控制器进一步在磁盘上进行数据操作,大概关系示意图如下:
高层次指令--》磁盘驱动--》低层次指令--》硬件控制器--》磁盘
Basic file system
从图中可以看出,basic file system下面是IO control层,由于IO control层是由磁盘驱动组成的,所以basic file system下面接的就是磁盘驱动,所以basic file system层的作用就是给磁盘驱动发指令。
除此之外,basic file system层还会控制buffer和cache,buffer是写入磁盘时候的缓冲区,cache是从磁盘读取信息时候的缓存。
再把图放一遍方便大家边看文字边对照,不是凑篇幅的哈
file-organization module
讲这一层的作用之前,先看一下图中的关系。
file-organization module上面是logical file system,虽然还没说logical file system是什么,但从名字上可以看出,就是逻辑层面的文件系统。
file-organization module下面是basic file system,之前已经介绍过,basic file system是向磁盘驱动发指令的,比如,“给第34个block写入I love you baby"。这里的“34”当然就是物理block地址,所以basic file system是属于物理层面上的。
既然file-organization module上面是逻辑层面,下面是物理层面,那他自己是什么呢?----答案就是,他就是逻辑层面的文件系统和物理层面的文件系统的中转站!!
这里物理层面和逻辑层面的概念如果大家不了解就看一下下面的补充,如果了解了就直接跳过看下下面部分即可。
补充说明(这里只是说明概念,细节方面笔者也不是很懂,大家明白思想即可哈):
每个文件系统的block都是默认从0开始的,如果有2个文件系统F1和F2。
F1的block范围:0-100
F2的block范围:0--200
在磁盘上,F1被分在了0-99的block范围内,F2被分在了100--299的block范围内。
但是当F2想要从他的第一个block读取数据,他发出的指令时“从block0读取数据”
但是磁盘上的block0是F1的第一个block,这不就有问题了吗?
解决方法很简单,就是只要是F2的指令,就给他要读取的block数量加上100,这样F2想要读取block0时候,磁盘上对应的就是100+0=100.
这里F1和F2的block范围就是逻辑层面的,磁盘上的就是物理层面的。
再再把图放一遍方便大家边看文字边对照,不是凑篇幅的哈
所以file-organization module接受的就是逻辑层面的文件系统指令(如果看了上面的说明的话,就是F2的读取block0指令),输出的就是物理层面的问及按系统指令(如果看了上面说明的话,就是磁盘上的读取block100指令)。
除了逻辑和物理层面的中转之外,这一层还负责空闲空间的调度问题,也就是说,磁盘上有这么多空闲空间,应该挑出来哪一个来用作系统需求呢?
logical file system
最上面的就是logical file system啦,如果不考虑电脑啦,磁盘啦什么什么,要我们自行脑补一个文件系统出来,我们要怎么设计这个东西呢?这个脑补出来的东西,就是逻辑层面的文件系统。logical file system的输入是应用程序的指令,比如“创建一个名为text的文件”,输出的是逻辑层面的文件系统指令,比如“在block0处留出空闲空间供text文件使用”(注意这里的block0是逻辑层面的block地址)。
明白了输入输出是什么之后,我们还要看这一层里面有什么。
上面说过,我们脑补了一个文件系统,那么这一层里面就存放了我们脑补的这个文件系统的信息,例如,文件权限、文件位置等。
接下来引出一个专业名词。
在介绍logical file system时,提到了这个脑补的文件系统有很多信息。文件系统里面存放的当然就是文件,这些文件的尺寸、名字、创建时间等信息,被打包放到了一个叫做FCB(file control block)中。
文件系统的设计方式
文件系统最基本操作的设计方式
虽然不同文件系统的设计方式各有特点,但总的来说都是“换汤不换药”,这里的“药”有以下几部分:
基本组成部分
1. boot control block
操作系统是一个程序,程序自然就要放到文件系统里面。所有文件系统都单独留出了这个boot control block单元来存放操作系统的开机启动程序。
但你可能有疑惑,拿windows来说,我的系统程序确实放在了C盘,但是D盘并没有放置操作系统啊,那D盘也要有这样一个boot control block吗?答案是是的,只不过没有放置操作系统的文件系统的boot control block是空的而已。
2. volume control block
虽然我们的电脑的存储设备是以磁盘为单位的,我们可以说,我的电脑有几块磁盘,有几个固态,有几个机械,但是在操作系统视角来说并不是这样的,在我们看来有几块磁盘,在操作系统看来,其实是有几个volume(几个卷)比如说Windows的C盘D盘,其实应该叫做C卷,D卷(好难听)
磁盘和卷的关系并非一一对应的,一个磁盘可以被切割为多个卷,也可以多个磁盘合并到一起作为一个卷。
这里的volume control block就是记录的卷的信息,比如,一个卷里有多少个block,有多少空闲的block,空闲block指针等等。
3. directory structure
一个文件系统自然要有目录结构啦,这一部分就是存放的目录结构的信息,比如这里面存放了
“我这个文件系统啊,他有C盘和D盘,C盘里有XX目录,D盘里只有一个目录,这个目录名字叫做漂亮小姐姐“
4. FCB
FCB是什么,不记得的可以向上翻,我在灰色引用块有介绍。
文件系统中的每一个文件都有它自己的FCB,FCB存放了他对应的文件的相关信息。
内存中存放的信息
程序需要加载到内存中才能运行,文件系统的各种操作也是通过各种程序执行的,所以文件系统的相关信息也需要放到内存里才能被操作,把文件系统相关信息放到内存里的这个操作就叫做挂载(mount),把文件系统中的信息从内存中取出的操作就叫做取消挂载(dismount)
那么内存中究竟存放了什么信息呢?
挂载表(mount table) | 收集了所有被挂载的文件系统的信息 |
目录结构缓存 | 记录了最近访问的目录的信息 |
system-wide open-file-table | 存储了每一个被打开的文件的FCB,也包含一些其他信息 |
per-process open-file-table | 包含了一些指向system-wide open-file-table入口的指针,也包含一些其他信息 |
缓冲区 | 缓冲磁盘读写时候的数据 |
一个文件的一生--创建、打开、读写、关闭
下表介绍了文件的各个操作流程的实现方式,这里再放一次之前的文件系统的分层设计图(不是为了凑篇幅哈)
这里再次回顾一遍,FCB就是一个把文件的信息收集起来的信息箱,下面介绍的时候会再用到这个概念。
创建文件 | 1. 第一步:更新目录信息。 文件的创建是应用程序层发起的,他会将指令发送到Logical file system层,logical file system层会为接收到的请求分配一个FCB。然后操作系统把要创建文件对应的目录信息加载到内存中,根据新分配的FCB更新目录信息,然后把目录信息写入磁盘。 2. 第二步:为创建的文件在磁盘上分配一块空间。 logical file system层把逻辑层面上创建一个目录这一行为转化成“在逻辑文件系统中分配一个block”这句话,逻辑层面的block被传递到file-organization module,进而完成逻辑block地址到物理block地址映射,再经过后续层,最终实现了为创建的文件在磁盘上分配出一块空间。 |
打开文件 | 打开文件需要open函数,open会给逻辑文件系统层传递一个文件名。进而去去搜索system-wide open-file table 1. 如果system-wide open-file table含有open要找的文件,就在当前进程的per-process open-file table中添加一个指针指向对应的system-wide OFT(open-file table)的入口。 2. 如果没有在system-wide OFT找到对应入口,就去目录结构中搜索,搜索到之后把对应文件的FCB复制到system-wide OFT中,在per-process OFT添加对应指针。 per-process OFT还包含了其他信息,包括当前的文件指针,指向正在读写的位置,访问模式等 open函数返回的,就是per-process OFT的文件指针,在UNIX中他被叫做file descriptor,在Windows中,他被叫做file handle |
关闭文件 | 当进程关闭文件时,首先移除per-process OFT,然后把system-wide OFT中的计数器减一(system-wide OFT中存放一个计数器,用来计数正在使用该entry对应的进程数量)。当system-wide OFT中的计数器为0时,system-wide OFT的entry也被移除。 |
虚拟文件系统
问题的引入:一般的操作系统都支持多个文件系统,但是不同的文件系统有不同的结构,如果想要打开一个NTFS文件系统中的file1,又要打开一个FAT文件系统中的file2,可以使用同一个open函数吗?当然是不能的。所以我们应该使用类似NTFS_open和FAT32_open这两个函数,但是如果像Linux支持40多个文件系统,难道要设计出40个open函数?即使设计是可行的,用户使用起来也会很繁琐。
为了解决这个问题,我们就需要虚拟文件系统的概念了。不同的文件系统虽然实现细节不同,但是他们都被统一出相同的接口,建立在这个统一接口上的文件系统,就是虚拟文件系统(VFS,virtual file system)
Linux虚拟文件系统实例
Linux的VFS有四个主要部分:
inode object:代表了一个单独的文件
file object:代表一个打开的文件
superblock object:代表整个文件系统
dentry object:代表一个单独的目录
VFS对于每一个object都定义了一系列相关操作,每一个object都有一个指向function table的指针,这个function table包含了这些相关操作的函数地址。
VFS不需要知道inode代表的文件是一个NTFS还是FAT文件,只需要调用这个inode的相关函数即可进行相关操作。
文件系统中的目录结构是怎么设计的
设计方法一:Linear List
这种方法最简单,即使用一个list来存储文件名字和它对应的block指针。这种方法的优点就是设计简单,缺点是较差的性能体验,因为寻找和删除文件等很多操作都需要线性时间复杂度。
对于这种方法的改进方式有:通过给list排序然后使用binary search来减少访问时间,但是这种方式为创建和删除文件提供了复杂度(因为添加和修改list中的元素同时需要保持排序状态需要额外的运算资源)
另一种改进方式是使用一个binary balanced tree,但这种方式又丢失了linear list的最大优势:设计简单
设计方法二:Hashed table
这种方式依然使用一个linear list来存储文件名和对应的block number,当要访问一个文件时,文件名字通过哈希函数得到了一个哈希值,这个哈希值被用作linear list的索引指向linear list的特定位置。
但是这种方式受限于linear list的声明大小,如果当前的linear list已经存满,再添加一个新的文件需要重新设计哈希函数,重新声明更大尺寸的linear list。
对这种问题的改进措施为对于使用一个链表来代替原来linear list的每一个entry。当两个文件出现哈希冲突时,这两个文件按照一定顺序存放在链表中。
特别声明
本篇博客到此结束,下一篇会介绍把文件分配到磁盘上的方法,文件系统如何管理空闲空间,文件系统的恢复模式,以及NFS(network file system)的相关内容。文中内容全部参考OPERATING SYSTEM CONCEPTS NINTH EDITION(ABRAHAM SILBERSCHATZ)Chapter 12: Implementing File System,如有不正欢迎指出,如造成误解深表歉意!