文章目录
- 一、前言
- 二、认识硬件——磁盘
- 2.1 磁盘的存储构成
- 2.2 磁盘的逻辑抽象
- 三、操作系统对磁盘的使用
- 3.1 再来理解创建文件
- 3.2 再来理解删除文件
- 3.3 再来理解目录
- 四、硬链接
- 五、软链接
- 六、结语
一、前言
在之前的【Linux取经路】文件系统之被打开的文件——文件描述符的引入一文中讨论了被打开的文件,今天讨论的话题则是没有被打开的文件。文件等于文件内容加文件属性,没打开的文件一定是存储在磁盘上的,并且 Linux 是将文件的属性和内容分开存储。文件内容以数据块的形式进行存储,文件属性以 inode 的形式进行存储。
二、认识硬件——磁盘
我们这里说的磁盘指的是机械磁盘,并非我们现在我们笔记本上使用的 SSD。机械磁盘是计算机上唯一的一个机械设备,也是一个外设。
小Tips:磁头是一面一个,磁头与盘面不接触。磁头通过向盘面进行充放电来完成数据的写入。磁盘叫做永久性存储介质,内存叫做掉电易失性存储介质。
2.1 磁盘的存储构成
每一个盘面由多个磁道构成,一个磁道又有多个扇区构成。磁盘被访问的最基本单元是扇区,一般扇区的大小是 512 字节,有的是 4KB。要修改磁盘中 1 字节的数据,需要把该字节所在的扇区都加载到内存中。可以把磁盘看成是由无数个扇区构成的存储介质。要把数据存储到磁盘,第一个需要解决的问题就是如何定位一个扇区,首先需要定位盘面,也就是确定用哪个磁头,因为一个磁头对应一个盘面,接下来需要定位磁道,最后定位扇区。所有的磁头都是同步运动的,在某一时刻,从从上向下看去,以磁头所在点为半径的不同盘面上的磁道就会形成一个叫做柱面的结构。磁头运动主要是去定位磁道,盘面旋转主要是去定位扇区,磁头的定位是由硬件电路进行控制。由此可见,磁盘的读取效率取决于磁头、盘面的运动速度和运动次数,运动越少,效率越高;运动越多,效率越低。因此,在软件设计上要求设计者一定要有意识的将相关数据放在一起。
2.2 磁盘的逻辑抽象
最终一个磁盘可以看作是基于扇区的数组,每一个扇区都对应有一个下标来唯一标识。通过这个下标(LBA 逻辑扇区地址),再结合每一面磁道的个数和每一个磁道上扇区的个数就可以定位到该扇区在磁盘上的位置(CHS地址)。
小Tips:不仅 CPU 有寄存器,其它外设也有,磁盘中也有寄存器。比如:控制寄存器,用来存储 CPU 下发的读写指令;数据寄存器,存储要写入的磁盘的数据;地址寄存器,存储 CPU 传送来的 LBA 地址;状态寄存器,存储磁盘的状态,操作系统通过检查该状态寄存器去判断读写是否成功。
三、操作系统对磁盘的使用
上图为 Linux ext2 磁盘文件系统图(内核内存映像肯定有所不同),磁盘是典型的块设备,操作系统首先会对磁盘进行分区,就如我们电脑中的 C 盘和 D 盘。接着,磁盘分区被划分为若干个块组(Block group),每个块组中有许多块(block),一个 block 的大小是由格式化的时候确定的,并且不可以更改,常见的是 4KB,即 4096字节。
-
Boot Block:通常存储操作系统启动的相关信息,比如:操作系统在什么位置、当前磁盘一共被划分成了多少个分区等。这些信息一般存储在磁盘的最前面,当然为了防止意外,这些内容在其它地方也会有备份。
-
Block Group:ext2 文件系统会根据分区的大小划分为数个 Block Group。而每个 Block Group 都有着相同的结构组成。
-
Super Block:存放文件系统本身的信息,这里面记录了整个分区的信息。例如:整个分区有多大、该分区里面每组的起始位置、每个组的大小、每个组的 inode 数量、每个组的 block 数量、每个组的其实 inode 编号、block 和 inode 的总量,未使用的 block 和 inode 的数量,一个 block 和 inode 的大小、文件系统的类型与名称、最近一次挂载的时间、最近一次写入数据的时间、最近一次检验磁盘的时间、该文件系统所拥有的字段以及字段的存储顺序和起始位置(也就是规定了一个组的空间划分)等其它文件系统的相关信息。Super Block 的信息被破坏,可以说整个文件系统结构就被破坏了。该字段并不是在每一个分组中都有,而是在部分组里面有,防止意外发生,操作系统通过“魔数”来判断是否是 Super Block。
-
Group Descriptor Table:块组描述符,存储块组属性信息,例如:当前分组的大小、使用情况、下一个文件描述符应该从哪开始。
-
Block Bitmap:块位图,将比特位的位置和块号映射起来,里面记录着 Data Block 中哪个数据块已经被占用,哪个数据块没有被占用。
-
inode Bitmap:每个 bit 表示一个 inode 是否空闲可用。
-
inode Table:一组 inode。每一个 inode 用来存放单个文件的所有属性,如:文件大小、所有者、最近修改时间等。每个 inode 的大小一般是 128字节,且每一个 inode 都有唯一的编号。一般而言,一个文件一个 inode。
-
Data block:数据区,存放文件内容。以块的形式呈现,常见块的大小是 4KB。每个块都有自己独一无二的块号。
小Tips:操作系统在访问磁盘的时候,会以块为基本单位进行访问。
格式化:每个组的前四个字段存储的都是一些文件系统的属性信息或者分组的使用情况信息,这些内容应该在我们使用磁盘之前都准备好。所以,每一个分区在被使用前,都必须将部分文件系统的属性信息提前设置进对应的分区中,方便后续对分区和分组的使用,这个动作就叫做格式化。
在 Linux 中,文件的属性里面是不包含文件的名称。在 Linux 系统里面标识文件用的是 inode 编号。一个 inode 表示一个文件的所有属性,文件名并不属于 inode 内的属性。
一个 inode 与 数据块的对应关系:
其中直接索引对应的块中存储的就是文件内容,二级索引对应的块中存储的不是文件内容而是块号。假设块的大小是 4KB,块号用 4字节。那么一个块就可以存储 1024 个块号,这 1024 个块号对应的块里面存储的是文件的内容,三级索引同理。这样做的目的是在 inode 里面用较少的空间就可以映射出更多的数据块。
小Tips:inode 编号是以分区为单位进行统一分配的,而且不能跨分区,即每个分区中的 inode 编号都是从 0 开始,且一个分区中的 inode 个数是有上限的,因此可能会存在一下情况:一个分区中的 inode 被用完了,但是数据块还没有被用完,这种情况对应的就是创建了非常多的文件,但是每个文件的内容非常小;一个分区中的数据块被用完了,但是 inode 还没有被用完,这种情况就是创建的文件并不多,但是每个文件的大小非常大。
3.1 再来理解创建文件
首先创建文件一定是在一个路径下(目录)进行创建,这个路径就会帮我们定位到一个分区,然后去从第一个分组开始查看当前分组的 GDT 字段,看该分组中 inode 的使用情况,若当前分组中的 inode 还有剩余,接着去读取 inode_Bitmap,获取最近一个未被使用的 inode 编号,然后拿着 inode 编号去 inode_Table 里面找到对应的 inode,将文件的属性信息一填。如果有文件内容,先拿着 inode 编号找到对应的分组,根据写入内容的大小去 Block_Bitmap 中找出对应数量未被使用的块号,然后将这些块号写入到 inode 对应的属性里面,然后拿着块号去 Data blocks 中进行写入。
3.2 再来理解删除文件
删除文件只要拿着该文件的 inode 编号,在 inode Table 中找到对应的 indoe,获取到里面的 blocks,即拿到该文件对应的所有块号,然后根据这些块号将 Block Bitmap 中对应的比特位置0(假设 0 表示对应的块未被使用)。最后再根据 inode 编号到 inode Bitmap 中将该 inode 对应的比特位置为0,至此,一个文件就被删除啦。可以发现从头到尾并没有去修改块中的内容,这也是为什么拷贝 4G 的文件很慢,删 4G 的文件很快。所以在理论上,一个被删除的文件,可以根据 inode 将其恢复出来。
总结:删文件就是去修改 inode_Bitmap 和 Block_Bitmap 中的字段。在计算机领域的删除并不等于清空,大部分情况下,删除都表示可覆盖。因为清空会导致效率大大下降。
3.3 再来理解目录
上面说的所有对文件的操作都离不开 inode 编号,但是我们作为普通用户平时好像也并没有关注过 inode 编号,我们一般是直接使用文件名,此时就必须再来理解一下目录了。目录也是文件,也有自己的 inode,目录也有属性。目录也有(数据块),目录的数据块里面存放的是这个目录下的所有文件名和该文件名对应的 inode 编号的映射关系,这是一种 key-value 结构,这就是为什么一个目录里面不允许出现同名文件。与此同时,和目录有关的一些历史问题也得到了解决,对于一个目录,没有 w,我们无法在该目录下创建文件,本质就是我们不能向目录对应的数据块中写入文件名和 inode 的对应关系;没有 r,无法产看该目录下的文件,本质就是不能读取目录文件的数据块。因此我们在查找一个文件时,首先需要知道该文件所在目录的 inode,要知道目录文件的 inode 编号,就需要知道目录文件所在目录的 inode 编号,如此递归一直到根目录,这就是为什么我们在操作一个任何一个文件的时候都需要知道它的绝对或者相对路径。如果每操作一个文件都要去这样递归一层层的查找,那么效率是非常低的。因此在 Linux 操作系统中有一个叫做 dentry 缓存(目录项缓存),里面记录了该用户经常访问的文件名和 inode 编号之间的映射关系。
四、硬链接
// 创建硬链接的指令
ln test.txt hard-link
硬连接不是一个独立的文件,因为它没有独立的 inode。所谓建立硬连接,本质其实就是在特定目录的数据块中新增文件名和指向的文件的 inode 编号的映射关系。上图也可以证明文件名不是 inode 中的属性,因为一个 inode 编号对应一个 inode,如果文件名是 inode 中的属性,那么上图中编号 1978740 的文件不可能对应两个文件名。
小Tips:任意一个文件,无论是目录,还是普通文件,都有 inode,每一个 inode 内部,都有一个叫做引用计数的计数器,这个计数器记录了有多少个文件名“指向”该文件(由硬链接可以得知,在 Linux 中可以让多个文件名对应于同一个 inode)。完整的删除文件过程就是先将特定目录数据块中的文件名与 inode 的映射关系删除,然后根据 inode 的编号,将对应 inode 中的引用计数减减,最终看其是否减到零,减到零再执行 3.2 小结的步骤。此外,创建一个普通文件,它的硬链接数默认是1。
创建目录文件的默认链接数为什么是 2 ?
硬链接数为2,说明与该文件 inode 编号有关的映射关系有两个,其中一个映射关系保存在 dir 所在目录文件的数据块,即 2023-11-06 目录文件中的数据块中,另外一个保存在 dir 目录自身的数据块中。
根目录的硬链接数减2就是根目录下创建的目录个数。根目录稍微有一点特殊,根目录中的 .
和 ..
文件名对应的 inode 编号是一样的,都是根目录。除去这俩文件名和 inode 编号的映射关系外,剩下的硬链接数就表示根目录下创建的目录个数,剩下的 18 个就是根目录中所有目录文件中 ..
与根目录 inode 编号的链接关系。可以这样计算的本质原因就是,根目录下所有目录中的 ..
都一定是指向根目录的,对应根目录的 inode 编号(也就是上图中的 2 )。这种计算方法也可以推行到其它任意目录。
总结:建立硬链接就是给一个已存在的文件创建别名,硬链接通常用来进行路径定位,采用硬链接,可以进行目录间切换。
小Tips:Linux 系统不允许用户对目录文件建立硬连接。只要是为了避免在文件搜索的时候发生环路问题,系统在搜索文件的时候并不会搜索 .
和 ..
,如果允许用户为目录文件创建硬链接,那么操作系统在进行文件搜索的时候就无法避免环路问题。
五、软链接
// 创建软链接的指令
ln -s file.txt soft-link
软链接是一个独立的文件,有独立的 inode,也有独立的数据块,它的数据块里面存的是指向文件的路径。软链接非常像 Windows 中的快捷方式。软链接的使用场景:一般发布的可执行程序可能存在一个较深的路径下面,要执行它的话就需要带很长一串路径,显得十分麻烦,此时我们就可以创建一个软链接指向该可执行文件,之后要想运行该可执行程序就直接去执行软链接即可。
小Tips:可以对任意类型的文件创建软链接。
// 删除软硬链接都可以用 unlink 指令
unlink soft-link
六、结语
今天的分享到这里就结束啦!如果觉得文章还不错的话,可以三连支持一下,春人的主页还有很多有趣的文章,欢迎小伙伴们前去点评,您的支持就是春人前进的动力!