文件系统
- 前言
- 一、磁盘硬件
- 1. 磁盘的物理存储结构
- 2. 磁盘存储的逻辑抽象结构
- 二、理解 Ext2 文件系统
- 1. 初步理解文件系统
- 2. 深入理解文件系统
- (1)inode Table
- (2)Data blocks
- (3)inode Bitmap
- (4)Block Bitmap
- (5)Group Descriptor Table
- (6)Super Block
- 3. 扩展
- (1)文件过大
- (2)Ext2 文件系统
- (3)目录
- 三、软硬链接
- 1. 软链接
- 2. 硬链接
- 四、页框和页帧
前言
首先我们需要知道,文件的管理工作分为:
- 打开的文件进行管理;
- 没有被打开的文件也要在磁盘中进行管理。
而以上两个部分我们称为文件系统!我们在上一章已经学习过了在系统中一个被打开的文件,而本章我们开始学习在磁盘中没有被打开的文件。
一、磁盘硬件
在理解文件系统之前,我们先了解一下磁盘这个硬件。
1. 磁盘的物理存储结构
首先我们先认识一下磁盘的外观结构,我们观察以下图片:
如上图,图中两个部分分别称为磁头和盘片。其中磁头不止一个,盘片也不止一片,盘片是有正反两面的,而且一个磁盘有好几个盘片,所以一个磁盘也就有好几面盘面,而一个盘面就对应一个磁头,所以磁头和盘面关系是一比一的,如下图:
其中,文件的所有的数据,包括内容和属性,都存在盘面上,磁头通过左右摆动读取和写入数据,而盘片通过顺时针或逆时针转动,配合磁头的左右摆动,就可以读取整个盘面的数据。
其中我们我们看到的盘面是光滑的,其实它上面是凹凸不平的,因为它上面有许多分区,例如,我们拿一个盘面的俯视图来讲,如下图:
盘面上可以分为许多的同心圆,像上图中红色的这一圈我们称为磁道,这个盘面上的每一个同心圆的最外围都被称为磁道。而我们可以观察到,每个同心圆都有许多分界线,将它分为许多的扇形,其中上图中绿色的部分我们称为扇区。
所以我们知道了,一个盘面可以有很多的同心磁道;一圈磁道可以有很多扇形的扇区!而扇区是磁盘的最小存储单元,其大小为 512字节。其中,哪怕我们在系统当中需要改变某个扇区中的一个比特位,无论是读或者修改,必须把整个扇区加载到内存里!在进行刷新时也必须要以 512字节 为单位进行刷新,这就是磁盘在进行读写时的基本单位必须是 512字节。所以我们把磁盘这样的设备称为块设备!
那么我们可以观察到,在同心圆内圈的扇区明显要比外圈的扇区要小,那么它们的大小还是 512字节 吗?是的!那么它是怎么做到的呢?其实是通过让二进制序列在扇区写入时疏密程度不一样就可以了!
如果我们想向一个扇区写入,我们该如何寻址或者定位呢?
- 首先我们得先确定我们需要写入磁盘的哪一个盘面,本质也就是选择哪一个磁头;
- 选择该盘面上的哪一个磁道;
- 选择在该磁道上的哪一个扇区。
所以我们需要向一个扇区写入,必须要知道以上的三个参数!所以以上的寻址方式我们称为 CHS寻址法,即 Cylinder(磁道、柱面) Head(磁头) Sector(扇区)。
既然我们可以向一个扇区写入,我们就可以向任意一个或者多个扇区写入,也可以连续多个扇区的写入,当然也可以随机写入。所以我们没有被打开的文件,它们都是存放在了以扇区为单位的磁道的盘面上,有的扇区存放的是内容,有的扇区存放的是属性,如果一个文件过大,就会被分为多个扇区存放!
接下来我们要知道,磁头的选择是很快的,一给磁头编号,我们就可以选择哪一个磁头。那么磁头在左右摆动的时候,本质是在干什么呢?其实它本质是在寻找磁道(柱面)!当找到了指定的柱面,那么盘片就会旋转,旋转的本质就是将对应的扇区转到磁头下,然后磁头就可以将数据读取上来了!
2. 磁盘存储的逻辑抽象结构
我们的磁盘是一个圆形结构,但是如果我们将磁盘拉展开来呢?所以我们可以将磁盘盘片抽象成一段线性的空间!假设我们的磁盘是两片四面的结构,那么我们就可以根据盘面将它抽象成一个具有四个区间的线性空间,如下图:
那么每一个面不是有很多磁道吗?没关系,我们也可以在一面中继续给磁道划分空间,如下图:
但是我们的磁道里面也划分成了许多扇区呀!所以我们也可以将一个磁道继续划分成许多个扇区!如下图:
所以整个磁盘我们可以把它抽象成由无数个扇区构成的一个数组!即以扇区为单位大小的一个数组!而数组都是有下标的,所以我们可以给它设定下标,例如,假设 1~100000 为第一面,100001~200000 为第二面,以此类推;而在第一面中,1~10000 为第一个磁道,10001~20000 为第二个磁道,以此类推。
所以对磁盘的管理,就变成了对数组的管理!例如我们的下标为 1234,那么它对应的盘面下标为 1234/100000 = 0,即是第一个盘面;而对应磁道的下标为 1234/10000 = 0,即是第一个磁道;对应的扇区则是 1234%10000 = 1234,即是第 1234 个扇区!所以以上三个参数就称为 CHS地址!
此外,操作系统也可以基于文件系统,按照文件块为单位进行数据存取,因为操作系统认为每次访问一个扇区太小了,为了减小和磁盘IO的次数,规定以8个扇区为基本单位,称之为文件块,为什么是8个呢?因为8个扇区的大小为4KB。所以我们以后需要寻址定位某一个块,只需要知道起始下标就可以了,因为块的大小为8个是规定!
这个文件块就是我们未来保存文件属性和内容的基本单元,我们把以这8个块为起始地址我们称为 LBA,即 Logical Block Address,逻辑块地址。所以,从此以后我们不再关注扇区,站在文件系统角度,只需要关注以 4KB 为基本单元的 Blocks 的数组即可,每一个4KB都有它的LBA地址,从此往后,对于磁盘的管理,对于文件系统的管理,就转换成了对该数组进行管理!
所以我们得出结论,对存储设备的管理,在操作系统层面,转换成了对数组的增删查改!
二、理解 Ext2 文件系统
1. 初步理解文件系统
假设我们需要在磁盘上管理 500GB,我们应该怎么管理呢?首先肯定不能将500GB看作一个整体管理,因为太大了,所以我们应该将它进行分区管理,假设将它分成 100GB、100GB、100GB、200GB,此时我们只需要将 100GB 管理好就可以了,同样的我们可以将管理这 100GB 的方法应用在其它分区上。
那么我们应该如何管理好这 100GB 呢?100GB 还是太大了,所以我们可以继续给它划分,我们可以将它分为许多个组,假设我们以 2GB 为一个组,可以给它分成 50 个组。所以要把 500GB 管理好,我们只需要把 2GB 管理好即可,这就是我们学过的分治思想。例如下图,我们将 100GB 放大看,:
上图只是我们描述的分组,所以在操作系统内核中我们所分的组其实是如下的:
如上图,在第一个分区中,第一个并不是组,而是 Boot Block,启动块,一般启动块是在磁盘的第一个扇区,它是负责启动的。
而后面的就全部都是划分的块组,假设我们以 Block group 0 为例,即第一个块组,它里面又划分为很多东西,这个我们后面详细说。
现在我们需要认识,在磁盘中,无非就存两类信息,一是我的文件信息,二是很多的文件管理的数据。所以在每个块组中,都会存两类信息,就是文件信息和文件管理信息。
而我们的文件信息中,包括内容和属性,内容和属性都是数据,而操作系统在文件系统层面将它们分开存储。而文件管理的数据需要管理块组有多大、还剩多少空间、将对应的内容和属性也要管理起来。所以块组里面就包括内容和属性,还有文件管理的数据。一般来说,在使用文件系统之前,在每个块组中,首先需要将管理数据进行写入,因为一般先要有管理者才能有被管理者!而将这些管理数据写入每个块组的工作我们就叫做格式化!所以格式化是清除数据没有错,但是清空的是我们的数据,再对管理数据恢复出厂设置即可!
2. 深入理解文件系统
假设我们目录下有以下这些文件和目录:
但是当我们带上 -i
选项之后,会多出一列数据,这些数据是什么呢?如下:
其实这就是文件的 inode 编号,一般情况一个文件对应一个 inode,而且每个文件必须要有。在整个分区具有唯一性,Linux 内核中,识别文件,和文件名无关,之和 inode 有关!
接下来我们介绍每个分区中的组块的组成内容。
(1)inode Table
- inode Table:i 节点表,存放文件属性,如文件大小,所有者,最近修改时间等。
因为每一个文件都有属性,属性的种类是有限的,而且每一个文件的属性都一样,所以保存文件属性是通过 inode 保存的!inode 是文件的属性集,我们可以把 inode 想像成一个结构体,里面包含文件的大小、权限、拥有者、所属组、ACM时间、inode 编号等,如下:
struct inode
{
大小、权限、拥有者、所属组、ACM时间、inode 编号等
}
在文件系统当中,这个 inode 结构体的大小是固定的,是 128 字节。inode Table 当中,会有非常多的 inode,所以假设 inode Table 的大小是 4KB * 1000,即有 1000 个文件块单位,所以就会有 32000 个 inode!因为每个 inode 的大小是固定的,所以我们可以根据偏移量(以128字节为单位)来确定每个 inode 的编号。所以在我们看来, inode Table 就是一个数组,而 inode 编号就是数组下标,我们很快就能定位到一个 inode.
那么怎么确定 inode 在整个分区上具有唯一性呢?其实在每个分区的起始位置,都有一个起始的 inode 位置,叫做 start_inode_number
,比如第一个分区的 start_inode_number 是 0,第二个分区的 start_inode_number 是 32001,所以在组块内确定每一个 inode 编号之后,需要加上当前分区的 start_inode_number 才是最终的 inode 编号!而想要确定当前 inode 在当前组块内是第几个 inode 就用最终的 inode 减去 start_inode_number 即可!
(2)Data blocks
- Data blocks:数据区,存放文件内容
由于 文件 = 内容 + 属性,属性我们已经可以找到了,所以当系统申请一个文件,所以先会申请一个 inode 块,即上面所说的结构体,标识这个 inode 块用的是 inode 编号,但是文件需要保存属性还要保存内容呀!
所以 Data blocks 里面没有任何管理数据,里面是一个非常大的以 4KB 为数据块的数据块区域!我们在找这些数据块的时候也很好找,它们也有自己对应的类似于数据块编号的号码,那么我们应该怎么找到当前文件的内容呢?为了方便对应文件的数据块,所以 inode 块当中,会帮我们维持一个数组,这个数组中存的是该文件对应的 Data blocks 中的数据块编号!如下:
struct inode
{
大小、权限、拥有者、所属组、ACM时间、inode 编号等;
int blocks[15] = {1, 2, 4};
}
所以当我们需要读取一个文件,我们只需要找到一个文件的 inode,找到 inode 之后,文件的属性就全部都有了,要读取数据,就读取 blocks 数组中的内容,将对应的数据区的数据块加载到内存中即可。
(3)inode Bitmap
我们对应的属性和数据块已经有了,但是系统怎么知道,当前的 inode 中哪些已经被使用哪些没被使用呢?数据块中哪些被使用哪些没被使用呢?所以我们继续介绍块组中的其它内容。
- inode Bitmap:inode位图(inode Bitmap),每个比特位表示一个 inode 是否空闲可用,即用比特位的位置表示 inode 编号;比特位的内容(0/1)对应的 inode 是否被使用。
所以以我们上面所假设,一个 inode Table 里面有 32000 个 inode 块,也就是 4KB * 1000 大小,所以在 inode Bitmap 中,用一个比特位标识一个 inode 块是否可用,也就是用 32000 个比特位标识,而刚好 4KB 就是 4000 字节,也就是 32000 个比特位,所以 inode Bitmap 中只需要用一个文件块单位就可以管理整个 inode Bitmap 了!
(4)Block Bitmap
那么类似于 inode Bitmap,数据块也是可以用位图进行标识的!
- Block Bitmap:块位图(Block Bitmap),Block Bitmap 中记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被使用。
比特位的位置表示 block 的编号,比特位的内容(0/1)对应 block 是否被使用。
所以当我们在磁盘上新建一个文件,并向里面写入 hello,world,首先是要先查 inode Bitmap,检测最近一个没有被使用的比特位,并置1,然后把偏移量编号记录下来,根据这个偏移量直接去 inode Tables 找到对应的 inode ,然后把文件属性往里面一写;然后在 Block Bitmap 找到一个位置,假设用一个块,找到之后将该比特位置1,将偏移量记录下来,那么数据块号就有了,然后往 inode 块中的 block 数组中写入,有了块号之后就可以找到对应的块号,将 hello,world 往对应的块号直接做刷新,写入里面。最后将 inode 编号返回给上层即可。
如果我们需要删除一个文件系统是怎么做的呢?只需要将 inode Bitmap 中对应的比特位由1置0即可!并且还要将 inode 块中的 block 数组的内容,即数据块的编号在 Block Bitmap 中由1置0!所以删文件只需要改位图即可!
(5)Group Descriptor Table
- Group Descriptor Table:GDT,块组描述符,描述块组属性信息。
GDT 是描述当前整个块组的信息,比如我们上面所说的 start_inode_number、整个组中一共有多少 inode、一共有多少 inode 被使用了… 它整体保存的是整个块的管理信息。
(6)Super Block
基本上每一个块组都有我们上面所介绍的五个区域,以上五个区域就够建成了一个块。而
- Super Block:超级块,存放文件系统本身的结构信息。记录的信息主要有:block 和 inode 的总量,整个分区未使用的 block 和 inode 的数量,一个 block 和 inode 的大小,最近一次挂载的时间,最近一次写入数据的时间,最近一次检验磁盘的时间等其他文件系统的相关信息。Super Block 的信息被破坏,可以说整个文件系统结构就被破坏了。
Super Block 不属于某一个块组,假设我们一个分区分为100个块组,可能也就在前面的个别块组里有 Super Block,它是存放管理整个分区的管理信息!那么它为什么会只存在个别块组呢?当一个 GDT 损坏,它影响的可能也就是一个块组,它的影响也有限。但是当一个 Super Block 损坏,就会导致整个分区损坏,所以为了防止这个事故发生,它就在几个块组多副本的保存几个,如果系统识别到我们经常使用的 Super Block 出问题了,我们只需要在其它块组的 Super Block 拷贝过来即可,这也就是自动修复功能。
3. 扩展
(1)文件过大
我们上面说过,一个 inode 块中的 block 数组是存放数据块的编号,这个数组的大小一般是 15,那么一个文件块单位是 4KB,15 个最多也就 60KB,如果一个文件超过了 60KB 怎么办呢?不用担心,这个 block 数组的最后几个数据块的编号对应的数据块并不保存数据信息,而是继续保存更多的块列表,这可是 4KB 呀,也就是这个 4KB 的空间用来保存更多的块列表,一个整数是 4Byte,所以这个 4KB 的空间可以再放 1000 个整数,用来找到更多的数据块编号,做二次索引,从而找到更多的数据块,如下图:
而二次索引后的数据块,也可以不保存数据信息,而是继续保存更多的块列表,继续做三次、四次… 索引,从而找到更多的数据块,从而将空间变大,可以容下更大的文件。
(2)Ext2 文件系统
以上我们所介绍的文件系统,也就是100GB这个分区,每个分区都由文件系统去管理,上面这种文件系统在 Linux 中称为 Ext2 文件系统,它是一个承上启下的文件系统。
(3)目录
我们上面所说的文件系统,适用于目录吗?在 Linux 下一切皆文件,所以也适用于目录!目录也有自己对应的 inode,如图:
目录也有自己的属性,但是目录的数据块内容存的是什么呢?答案是存的是自己目录内部直接保存的文件的文件名和 inode 的映射关系。在用户角度,用户只用文件名,而在系统内核角度只用 inode 编号,所以文件名和 inode 必须要有对应的映射关系!
所以同一个目录下不允许存在同名文件,因为文件名是用来做 key 用的;由于 inode 编号在整个分区具有唯一性,所以文件名和 inode 互为键值,大家可以互查。目录也是普通文件,只是目录存的内容是映射关系。
- 那么我们以前说过,在一个目录下,新建、删除、修改一个文件对于这个目录我们应该需要有什么权限呢?
w 权限,为什么是 w 权限呢?因为我们新建目录、文件是要在该目录下新建映射关系,删除就是去掉映射关系!
- 那么我们是如何查找一个文件的呢?
首先我们需要找到当前目录,在当前目录下找到对应的文件名和 inode 的映射关系,然后根据 inode 编号找到 inode 块,根据里面的 block 数组找到对应的数据块加载到内存中即可。但是我们应该如何找到当前目录呢?当前目录也是文件呀!那我们是如何找到当前目录这个文件的呢?我们要找到当前目录的 inode 才能找到当前目录,但是当前目录的 inode 需要找到上级目录呀,那么我们就需要一直往上级目录去找,所以我们就需要一直找到根目录。所以我们要找到一个文件,就要从根目录开始,找到根目录,然后一直往下找,就可以找到一个文件,所以找到一个文件就可以根据这样一个路径找到了。所以一个文件的路径结构就非常重要了。但是这个路径是谁给的呢?进程!我们的进程中存在一个 cwd 的当前工作路径!
- 访问文件的时候,最开始怎么知道这个文件是在哪一个分区里面呢?
其实一个磁盘被分区格式化后,Linux 中要使用这个分区,就要把这个分区进行挂载,这个挂载就是路径的前缀,所以每一个文件,都有路径,可以通过路径的前缀判断出我们的路径在哪一个分区下。
所以我们总结一下,假设我们打开一个文件,这个过程是怎么样的呢?首先确认的是,打开的时候是进程打开它,而进程有自己的 cwd,然后就结合进程的 cwd 和我们传入的路径,假设使用 fopen("./log,txt", "r")
,我们传入的路径就是 ./log.txt,就可以定位这个路径在磁盘的哪里,根据路径可以确定在哪一个分区里面,根据路径就能找到它的上级目录的 inode 和文件内容,目录找到了就可以找到文件名和 inode 的映射关系,也就是 inode 找到了,就可以找到文件的属性,就可以将属性加载到内存中,在内存里构建 struct file 结构体,把 inode 属性填充到结构体里面;然后根据 inode 找到文件的数据块,将数据块预加载到文件里,缓冲区就有了;如果我们要读怎么办呢?系统将缓冲区的数据拷贝到应用层我们就拿到了!
三、软硬链接
1. 软链接
我们先来看看在 Linux 中如何进行软链接,假设我们当前目录下有一个 log 文件,对它进行软链接,指令如下:
ln -s log log.soft.link
其中 ln -s log
代表对 log 进行软链接,而后面的 log.soft.link
表示用 log.soft.link 对前者进行软链接。链接完成后如下:
我们看一下它们的 inode 编号:
我们可以看到它们的 inode 编号是不一样的,即是两个独立的文件。
为什么要有软链接呢?软链接又是什么呢?这就好比 Windows 下的快捷方式,快捷方式一般是放在桌面上, 我们一点就能打开,但是我们也可以通过它的路径找到可执行程序直接运行,也能打开它。但是有时候某个应用的可执行程序放在 C盘 的很深处,我们需要费很大功夫才能找到它,所以我们可以创建一个软链接指向它,可以直接打开它。
在 Linux 下,比如我们写了一个程序 test.c,一般都是打包到一个文件夹给别人使用,所以我们编译好打包放入 proj 中的 bin 目录下,如下:
proj 下的目录结构和 bin 的目录结构如下:
此时我们每次执行 test 程序都要在 bin 路径下执行,这种方法很不好,所以我们可以用软链接进行对 test 进行链接,可以快速定位到这个文件,如下:
那么我们该如何理解软链接呢?其实它就是独立文件,有独立的 inode,软链接内容是指向的目标文件的路径。
2. 硬链接
我们也先看看在 Linux 中使用硬链接,例如对当前路径下的 file 文件进行硬链接,指令如下,注意此时不用带 -s 选项:
ln file file.hard.link
链接完成后如下:
我们再看看它们的 inode:
我们可以看到,硬链接后它们的 inode 是一样的;并且和软链接的区别还有,上述中绿色和蓝色框中的数字也增加了。
所以我们得出结论,软链接是一个独立的文件;硬链接不是一个独立的文件,因为它没有独立的 inode 编号。
那么硬链接是什么呢?它的本质就是在指定目录内部的一组映射关系,即文件名和 inode 的映射关系!所以建立硬链接就是增加了一个文件名和 inode 的映射关系!那么如果我们删除硬链接呢?删除硬链接的做法如下:
unlink file.hard.link
然后我们查看属性,该列的数字又变回1了,如下:
但是该文件还在,因为它的 inode 还在,那么一个文件什么时候应该真正被删除呢?没有文件名和 inode 映射时(没有人用了)!在文件系统层面,目标文件怎么知道没有文件名指向我了呢?其实 inode 块里面还包含一个引用计数 ref count
,如下:
struct inode
{
int ref count;
大小、权限、拥有者、所属组、ACM时间、inode 编号等;
int blocks[15] = {1, 2, 4};
}
该引用计数统计的是有多少个文件名指向该 inode!即表明有几个文件名映射关系!
文件名在目录里面具有唯一性,文件名其实有点像指针,该文件名指向 inode,多一个文件名指向 inode 引用计数就加一,少一个就减一,减到0说明没有文件指向该 inode,就需要删除该 inode 编号。
其实我们上面所看的属性中,有一列我们从来没有介绍过,就是硬链接数,如下框中的数字就是表示硬链接数:
接下来我们创建一个 newdir 目录和一个 newfile,如下,为什么新创建的目录硬链接数是 2,而新创建的文件硬链接数是 1呢?
那么我们就进入 newdir 看一下,我们知道,每个目录下默认都有两个隐藏文件,如下:
我们知道,.
表示当前目录,为什么它代表当前目录呢?很简单,因为它的 inode 和该目录本身的 inode 是同一个!我们可以从上面两个图中观察到,所以 .
表示当前目录!
接下来我们在当前目录下再新建一个 dir,然后我们退回上级目录再看一下 newdir 的属性:
我们发现,它的硬链接数又变成了 3 ,这是为什么呢?这肯定是有新的文件名指向它的 inode,我们猜想它一定和在 newdir 中新建的目录 dir 有关,所以我们直接到 dir 中查看:
我们发现,在 dir 中有一个 ..
的隐藏目录,该目录的 inode 也指向了 newdir 的 inode,因为 ..
代表的是当前所处目录的上级目录!
所以硬链接的应用场景经常用作路径切换和回退。
最后,有一个结论就是用户无法对目录建立硬链接,因为会导致环状搜索问题。
四、页框和页帧
我们已经知道,操作系统需要对文件进行管理,对文件管理必然离不开对内存进行管理,所以操作系统也要对内存进行管理。物理内存的本质就是对数据的临时存取,所以在系统层面上可以把物理内存看作一个非常大的缓冲区。物理内存必定要和磁盘有一定的关联,因为磁盘的数据都需要加载到物理内存里。
为了更好地进行物理内存和磁盘之间的数据交互,操作系统内部将物理内存划分为基本单位,一般这个基本单位的大小为 4KB,我们称这个基本单位为页框。而磁盘中的可执行程序也被划分为以 4KB 为单位的小的数据段,我们将这个单位称为页帧。所以物理内存和磁盘在进行数据交互时是以 4KB 为单位进行交互的!
为什么是4KB呢?硬件层面上可以减少IO的次数,减少访问外设的次数。如果我们的数据不够4KB呢?如果我们只需要访问其中的100字节,操作系统也会把4KB加载进来,因为基于局部性原理的预加载机制,也就是我们方法这100个字节,很大可能还会访问附近的空间。