首先我们需要知道,文件的管理工作分为:
而以上两个部分我们称为文件系统!我们在上一章已经学习过了在系统中一个被打开的文件,而本章我们开始学习在磁盘中没有被打开的文件。
在理解文件系统之前,我们先了解一下磁盘这个硬件。
首先我们先认识一下磁盘的外观结构,我们观察以下图片:
如上图,图中两个部分分别称为磁头和盘片。其中磁头不止一个,盘片也不止一片,盘片是有正反两面的,而且一个磁盘有好几个盘片,所以一个磁盘也就有好几面盘面,而一个盘面就对应一个磁头,所以磁头和盘面关系是一比一的,如下图:
其中,文件的所有的数据,包括内容和属性,都存在盘面上,磁头通过左右摆动读取和写入数据,而盘片通过顺时针或逆时针转动,配合磁头的左右摆动,就可以读取整个盘面的数据。
其中我们我们看到的盘面是光滑的,其实它上面是凹凸不平的,因为它上面有许多分区,例如,我们拿一个盘面的俯视图来讲,如下图:
盘面上可以分为许多的同心圆,像上图中红色的这一圈我们称为磁道,这个盘面上的每一个同心圆的最外围都被称为磁道。而我们可以观察到,每个同心圆都有许多分界线,将它分为许多的扇形,其中上图中绿色的部分我们称为扇区。
所以我们知道了,一个盘面可以有很多的同心磁道;一圈磁道可以有很多扇形的扇区!而扇区是磁盘的最小存储单元,其大小为 512字节。其中,哪怕我们在系统当中需要改变某个扇区中的一个比特位,无论是读或者修改,必须把整个扇区加载到内存里!在进行刷新时也必须要以 512字节 为单位进行刷新,这就是磁盘在进行读写时的基本单位必须是 512字节。所以我们把磁盘这样的设备称为块设备!
那么我们可以观察到,在同心圆内圈的扇区明显要比外圈的扇区要小,那么它们的大小还是 512字节 吗?是的!那么它是怎么做到的呢?其实是通过让二进制序列在扇区写入时疏密程度不一样就可以了!
如果我们想向一个扇区写入,我们该如何寻址或者定位呢?
所以我们需要向一个扇区写入,必须要知道以上的三个参数!所以以上的寻址方式我们称为 CHS寻址法,即 Cylinder(磁道、柱面) Head(磁头) Sector(扇区)。
既然我们可以向一个扇区写入,我们就可以向任意一个或者多个扇区写入,也可以连续多个扇区的写入,当然也可以随机写入。所以我们没有被打开的文件,它们都是存放在了以扇区为单位的磁道的盘面上,有的扇区存放的是内容,有的扇区存放的是属性,如果一个文件过大,就会被分为多个扇区存放!
接下来我们要知道,磁头的选择是很快的,一给磁头编号,我们就可以选择哪一个磁头。那么磁头在左右摆动的时候,本质是在干什么呢?其实它本质是在寻找磁道(柱面)!当找到了指定的柱面,那么盘片就会旋转,旋转的本质就是将对应的扇区转到磁头下,然后磁头就可以将数据读取上来了!
我们的磁盘是一个圆形结构,但是如果我们将磁盘拉展开来呢?所以我们可以将磁盘盘片抽象成一段线性的空间!假设我们的磁盘是两片四面的结构,那么我们就可以根据盘面将它抽象成一个具有四个区间的线性空间,如下图:
那么每一个面不是有很多磁道吗?没关系,我们也可以在一面中继续给磁道划分空间,如下图:
但是我们的磁道里面也划分成了许多扇区呀!所以我们也可以将一个磁道继续划分成许多个扇区!如下图:
所以整个磁盘我们可以把它抽象成由无数个扇区构成的一个数组!即以扇区为单位大小的一个数组!而数组都是有下标的,所以我们可以给它设定下标,例如,假设 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地址,从此往后,对于磁盘的管理,对于文件系统的管理,就转换成了对该数组进行管理!
所以我们得出结论,对存储设备的管理,在操作系统层面,转换成了对数组的增删查改!
假设我们需要在磁盘上管理 500GB,我们应该怎么管理呢?首先肯定不能将500GB看作一个整体管理,因为太大了,所以我们应该将它进行分区管理,假设将它分成 100GB、100GB、100GB、200GB,此时我们只需要将 100GB 管理好就可以了,同样的我们可以将管理这 100GB 的方法应用在其它分区上。
那么我们应该如何管理好这 100GB 呢?100GB 还是太大了,所以我们可以继续给它划分,我们可以将它分为许多个组,假设我们以 2GB 为一个组,可以给它分成 50 个组。所以要把 500GB 管理好,我们只需要把 2GB 管理好即可,这就是我们学过的分治思想。例如下图,我们将 100GB 放大看,:
上图只是我们描述的分组,所以在操作系统内核中我们所分的组其实是如下的:
如上图,在第一个分区中,第一个并不是组,而是 Boot Block,启动块,一般启动块是在磁盘的第一个扇区,它是负责启动的。
而后面的就全部都是划分的块组,假设我们以 Block group 0 为例,即第一个块组,它里面又划分为很多东西,这个我们后面详细说。
现在我们需要认识,在磁盘中,无非就存两类信息,一是我的文件信息,二是很多的文件管理的数据。所以在每个块组中,都会存两类信息,就是文件信息和文件管理信息。
而我们的文件信息中,包括内容和属性,内容和属性都是数据,而操作系统在文件系统层面将它们分开存储。而文件管理的数据需要管理块组有多大、还剩多少空间、将对应的内容和属性也要管理起来。所以块组里面就包括内容和属性,还有文件管理的数据。一般来说,在使用文件系统之前,在每个块组中,首先需要将管理数据进行写入,因为一般先要有管理者才能有被管理者!而将这些管理数据写入每个块组的工作我们就叫做格式化!所以格式化是清除数据没有错,但是清空的是我们的数据,再对管理数据恢复出厂设置即可!
假设我们目录下有以下这些文件和目录:
但是当我们带上 -i
选项之后,会多出一列数据,这些数据是什么呢?如下:
其实这就是文件的 inode 编号,一般情况一个文件对应一个 inode,而且每个文件必须要有。在整个分区具有唯一性,Linux 内核中,识别文件,和文件名无关,之和 inode 有关!
接下来我们介绍每个分区中的组块的组成内容。
因为每一个文件都有属性,属性的种类是有限的,而且每一个文件的属性都一样,所以保存文件属性是通过 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 即可!
由于 文件 = 内容 + 属性,属性我们已经可以找到了,所以当系统申请一个文件,所以先会申请一个 inode 块,即上面所说的结构体,标识这个 inode 块用的是 inode 编号,但是文件需要保存属性还要保存内容呀!
所以 Data blocks 里面没有任何管理数据,里面是一个非常大的以 4KB 为数据块的数据块区域!我们在找这些数据块的时候也很好找,它们也有自己对应的类似于数据块编号的号码,那么我们应该怎么找到当前文件的内容呢?为了方便对应文件的数据块,所以 inode 块当中,会帮我们维持一个数组,这个数组中存的是该文件对应的 Data blocks 中的数据块编号!如下:
struct inode
{
大小、权限、拥有者、所属组、ACM时间、inode 编号等;
int blocks[15] = {1, 2, 4};
}
所以当我们需要读取一个文件,我们只需要找到一个文件的 inode,找到 inode 之后,文件的属性就全部都有了,要读取数据,就读取 blocks 数组中的内容,将对应的数据区的数据块加载到内存中即可。
我们对应的属性和数据块已经有了,但是系统怎么知道,当前的 inode 中哪些已经被使用哪些没被使用呢?数据块中哪些被使用哪些没被使用呢?所以我们继续介绍块组中的其它内容。
所以以我们上面所假设,一个 inode Table 里面有 32000 个 inode 块,也就是 4KB * 1000 大小,所以在 inode Bitmap 中,用一个比特位标识一个 inode 块是否可用,也就是用 32000 个比特位标识,而刚好 4KB 就是 4000 字节,也就是 32000 个比特位,所以 inode Bitmap 中只需要用一个文件块单位就可以管理整个 inode Bitmap 了!
那么类似于 inode Bitmap,数据块也是可以用位图进行标识的!
比特位的位置表示 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!所以删文件只需要改位图即可!
GDT 是描述当前整个块组的信息,比如我们上面所说的 start_inode_number、整个组中一共有多少 inode、一共有多少 inode 被使用了… 它整体保存的是整个块的管理信息。
基本上每一个块组都有我们上面所介绍的五个区域,以上五个区域就够建成了一个块。而
Super Block 不属于某一个块组,假设我们一个分区分为100个块组,可能也就在前面的个别块组里有 Super Block,它是存放管理整个分区的管理信息!那么它为什么会只存在个别块组呢?当一个 GDT 损坏,它影响的可能也就是一个块组,它的影响也有限。但是当一个 Super Block 损坏,就会导致整个分区损坏,所以为了防止这个事故发生,它就在几个块组多副本的保存几个,如果系统识别到我们经常使用的 Super Block 出问题了,我们只需要在其它块组的 Super Block 拷贝过来即可,这也就是自动修复功能。
我们上面说过,一个 inode 块中的 block 数组是存放数据块的编号,这个数组的大小一般是 15,那么一个文件块单位是 4KB,15 个最多也就 60KB,如果一个文件超过了 60KB 怎么办呢?不用担心,这个 block 数组的最后几个数据块的编号对应的数据块并不保存数据信息,而是继续保存更多的块列表,这可是 4KB 呀,也就是这个 4KB 的空间用来保存更多的块列表,一个整数是 4Byte,所以这个 4KB 的空间可以再放 1000 个整数,用来找到更多的数据块编号,做二次索引,从而找到更多的数据块,如下图:
而二次索引后的数据块,也可以不保存数据信息,而是继续保存更多的块列表,继续做三次、四次… 索引,从而找到更多的数据块,从而将空间变大,可以容下更大的文件。
以上我们所介绍的文件系统,也就是100GB这个分区,每个分区都由文件系统去管理,上面这种文件系统在 Linux 中称为 Ext2 文件系统,它是一个承上启下的文件系统。
我们上面所说的文件系统,适用于目录吗?在 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 找到文件的数据块,将数据块预加载到文件里,缓冲区就有了;如果我们要读怎么办呢?系统将缓冲区的数据拷贝到应用层我们就拿到了!
我们先来看看在 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,软链接内容是指向的目标文件的路径。
我们也先看看在 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个字节,很大可能还会访问附近的空间。