一、文件系统
并不是所有的文件都是打开的,上一篇文章中讲到的内容都是文件系统管理被打开的文件的操作,但是大部分文件都是未被打开的,这些未打开的文件都存在于磁盘中,这些文件同样也需要被管理,那么接下来要讲解文件系统对于在磁盘中未被打开的文件的管理,对于这部分文件,文件系统最重要的工作就是快速定位这些文件,也就是通过路径快速找到我们所以需要处理的文件。
二、磁盘的物理存储结构
下图为磁盘的物理存储结构,一个磁盘中有多个盘片,一个盘片有两个盘面,每一面对应着有一个磁头,一个盘面由许多同心磁道组成,一个磁道中又对应则多个扇区,通过磁臂的左右移动与磁盘的旋转,磁头可以读取到磁盘的任意位置,我们将磁盘上所有相同半径的磁道的集合叫做柱面。
一个盘面有很多同心磁道,一个磁道有很多扇区,扇区是磁盘的最小存储单位,通常为512字节。
如果我们想向一个扇区中写入东西,需要通过三个条件进行寻址:
- 选择哪一面 ---- 本质上是选择磁头
- 选择该面上的哪一个磁道
- 选择该磁道上的哪一个扇区
也就是CHS定位法,CHS定位法(Cylinder, Head, Sector)是一种用于定位硬盘上数据的方法。CHS定位法通过三个参数来确定硬盘上的具体位置:柱面(Cylinder)、磁头(Head)和扇区(Sector)。
我们可以向一个扇区中写入,就可以向任意一个/多个连续的扇区中写入,也可以随机写入。
三、磁盘存储的逻辑抽象结构
3.1 逻辑抽象
磁带相信很多人都见过,磁带与磁盘是同一时代的产物,磁带中黑色的线就是用来存储信息的,可以通过录音机来读取到磁带中的信息,通过下面的图片可以看出磁带可以看成有很多同意组成,也可以将其拉直变成一根直线。
那么我们也可以将磁盘的盘片抽象成为一个线性空间
对上图进行高度抽象,将整个磁盘抽象为以扇区为基本单位的数组。
假设我们就设定下标1 ~ 100000这段区间为第一面,100001 ~ 200000这段区间为第二面,以此类推。设定1 ~ 10000 这段区间为第一磁道,10001 ~ 20000 这段区间为第二磁道,以此类推。
所以对磁盘的管理就变成了对扇区数组的管理了。只要我们知道了数组的下标,就能够找到对应在磁盘的哪一个位置。也就让下标转换成了CHS地址,也就是哪个盘面、哪个磁带、哪个扇区。
操作系统可以按照扇区为单位进行存取,也可以基于文件系统,按照文件块为单位进行存取,操作系统认为按照一个扇区进行存取,每次只能访问512字节,有点小,为了减少与磁盘IO的次数,操作系统规定8个扇区为一个文件块也就是4KB,操作系统以文件块为基本单位进行磁盘级别的IO,这个文件块的起始地址我们称之为逻辑区块地址(Logical Block Address, LBA)
最后操作系统对存储设备的管理,转换为了对文件块数组的增删查改,与底层的磁盘、柱面、扇区再无直接关系。
3.2 分区与格式化
一个磁盘的大小通常512G以上,那么磁盘中就有非常多的4空KB,为了方便管理整个磁盘,通常会将磁盘分为几个区域,也就是我们常说的分区,我们电脑中的C/D/E/…盘就是分区后的结果,每一个分区的结构都是相同的,所以我们只要管理好一个分区,其他分区就可以使用同样的方法被管理好。将磁盘分区后,一个分区的大小大概为一两百G左右,这里以100G举例,但是我们发现一个区有100G也并不是很好管理,那就将分区再进行分组,假设一个组是2G,就可以将一个分区分为50个组,又每一个组的结果也是相同的,所以我们只需要管理好一个组,其他组就可以使用同样的方法被管理好,最终要把磁盘的512个G管理好,转换为了将一个组2G管理好。
3.2.1 分区的组成
一个分区是由Boot Block和多个分组组成。
3.2.1.1 Boot Block(启动块)
Boot Block我们称之为启动块,通常在第一个盘面上的第一个磁带上的第一个磁带上的第一个扇区中,一般来说只有第一个分区上才有Boot Block,它是负责启动的。当计算机开机时,BIOS(基本输入输出系统)会首先进行自检,随后寻找可启动的存储设备。一旦找到,BIOS就会将控制权交给该存储设备上的启动块。启动块中包含了引导程序,这个程序负责加载操作系统内核,进而启动整个计算机系统。值得注意的是,由于启动块的重要性,它通常被设计为具有高度的可靠性和稳定性。即使硬盘的其他部分受到损坏,只要启动块保持完好,计算机就有可能通过其他方式(如使用启动盘或恢复工具)来恢复或重装操作系统。
3.2.2 格式化
我们在磁盘中只会存储两类东西:我们自己的文件和很多文件管理的数据。
我们自己的文件分为内容和属性,内容和属性都是数据需要分开存储。
这里我举个例子来帮助大家理解文件管理的数据,我们在上学的时候,就会有普通的学生,还有管理学生的学生(班长、副班长…),而这些管理者通常是很早之前就会被选取出来,只有这些管理者被选取出来后,一个班级才能够正常的运行,对应到我们这里所讲的内容,所以一个文件系统要被运行起来,首先要将管理信息先写入到块组中。
当我们要使用整个磁盘之前,需要进行分区,在使用每个分区之前还需要向分区中写入管理数据,这个工作我们称之为格式化。格式化只会将用户的信息进行清除,并不会清除管理数据。
3.3 分组的组成
3.3.1 inode Table(i节点表)
当我创建了几个文件和目录,并使用ls -li
指令来查看文件信息,我们发现文件信息最前面多出来一列数据,这个数字我们称之为inode编号,一般情况下,一个inode对应一个文件,基本上,inode编号每个文件都需要有,并且inode编号在整个分区中具有唯一性,在Linux内核中,文件系统识别文件与文件名无关,只与文件的inode编号有关。
文件的属性种类是有限的,并且每个文件的种类是相同的,在文件系统中是通过inode来存储一个文件的属性的,inode就是文件的属性集,我们也可以理解为一个结构体,一个inode的固定大小为128个字节。
inode Table(i节点表):inode Table中存储了大量的inode,当一个inode在整个分区中为第n个时,那么它的inode编号就是n,又一个inode的大小是固定的,所以文件系统可以通过inode编号计算得到inode在inode表中的偏移量,并且在每个块组中都有一个变量记录inode的起始位置,通过其实位置和偏移量可以很容易定位到对应的inode在inode表中的位置,在我看来这个inode Table就很像一个数组,而它对应的inode编号就是下标。
struct inode
{
// 文件的大小
// 文件的权限
// 文件的拥有组
// 文件的所属组
// ACM时间
// inode编号
// ...
}
3.3.2 Data blocks(数据块)
文件不仅仅要存储文件的属性,还需要存储文件的内容,Data blocks就是一个以4KB为基本单位的数据块区域,里面是用来存储文件的内容的,每一个数据块都有它自己的编号。
我们把内容写入到了数据块中,那么文件应该如何想要找到它所对应的数据块呢?inode中维护了一个数组,用来记录文件内容存储在数据块的编号或地址,数组中存储数据的顺序,就是文件内容的顺序,当我们需要读取文件内容时,就可以将数组中所有的块,按照顺序读取到内存中。
struct inode
{
// ... 其他
int block[15];
}
通常来说inode中维护的数据块数据大小为15,一个数据块的大小为4KB,也就是说一个文件的大小最大为60KB?而现实中有很多情况下文件都是大于60KB的,所以用inode维护的数组与数据块直接映射是有上限的
在大多数文件系统中,inode中维护的15个指针通常会分配如下:
直接指针(Direct Pointers):通常占前12个指针,每个指针直接指向一个数据块,因此可以直接访问的数据块大小为 12 * 数据块大小(在4KB数据块的情况下是48KB)。这是直接指向数据块的区域,用于存储小文件的数据。
单级间接指针(Single Indirect Pointer):第13个指针指向一个“间接块”,间接块中存储的是指向数据块的指针。假设一个数据块大小为4KB,每个指针大小为4字节,则一个间接块可以存放 4KB / 4B = 1024 个数据块指针。因此,单级间接指针可以增加 1024 * 数据块大小 的存储能力(约4MB)。
双级间接指针(Double Indirect Pointer):第14个指针指向一个“双间接块”,该块指向多个单间接块,每个单间接块指向多个数据块。根据前述计算,一个双间接指针可以增加 1024 * 1024 * 数据块大小 的存储能力(约4GB)。
三级间接指针(Triple Indirect Pointer):第15个指针指向一个“三间接块”,该块指向多个双间接块。一个三级间接指针可以增加 1024 * 1024 * 1024 * 数据块大小 的存储能力(约4TB)。
小结
因此,通过多级间接指针机制,文件系统可以有效地管理大文件。直接块、单级间接、双级间接和三级间接指针共同扩展了inode可以管理的数据量,远超过最初的15个指针直接映射数据块的限制。
一个组能存储的文件大小是有限的,若是一个文件的大小超过了一个组能存储的大小,那么文件系统就会采取跨块组存储机制,在实际存储过程中,文件系统可以通过inode结构来跟踪每个数据块的位置,即使这些数据块位于不同的块组。inode中记录了文件数据块的具体地址,无论这些数据块分布在哪个块组,inode都可以通过指针来引用它们,保证文件的完整性和可访问性。
3.3.3 inode Bitmap(inode位图)
当我们要创建一个文件时,文件系统需要给我分配一个inode,将文件的属性写入到inode的中,文件系统是怎么知道inode是否被使用呢?
这里就要提出新的概念inode Bitmap(inode位图),位图上比特位的位置代表着inode编号,比特位上的内容(0/1)代表着对应的inode是否被使用,有了inode位图的存在,文件系统就能很明确的知道inode的使用情况。
3.3.4 Block Bitmap(块位图)
当我们要创建一个文件时,文件系统需要给我分配数据块,将我们文件的内容写入到数据块中,文件系统是怎么知道数据块是否被使用呢?
这里就要提出新的概念Block Bitmap(块位图),位图上比特位的位置代表着block编号,比特位上的内容(0/1)代表着对应的block是否被使用,有了块位图的存在,文件系统就能很明确的知道block的使用情况。
当我们创建一个文件并且文件内容只需要一个块时,文件系统首先遍历inode位图,找到最近一个没有被使用的位置,记住位置并将其置为1,根据位置找到inode表中的inode,将文件的属性写入到inode中,再遍历块位图,找到最近一个没有被使用的位置,记住位置并将其置为1,根据位置找到Data blocks中对应的block,并将文件内容写入到block中,最后把inode编号上传给上层即可。
当我们删除一个文件时,根据inode编号找到inode,将inode维护的数组中记录block编号对应到Block Bitmap的比特位,并将比特位上的内容全部置为0,再将inode编号对应到inode Bitmap中的比特位,并将比特位上的内容置为0即可。所以删除文件只需要修改位图即可,并不需要删除inode和block中的内容。
如果说我们误删了一个文件,想要将这个文件恢复,就必须要知道这个文件的inode编号,根据inode编号在inode Bitmap中对应的比特位置为1,再找到inode并将inode维护的数组中记录block编号对应到Block Bitmap的比特位置为1,即可恢复文件。
3.3.5 Group Descriptor Table(块组描述符)
Group Descriptor Table简称为GDT,它是用来描述整个块组的使用情况的GDT包含每个块组的起始块位置,用于定位关键结构,如 inode 表和数据块区域的起始地址,方便文件系统快速找到特定数据。GDT中标记数据块的使用情况,已用或未用,便于分配和回收。GDT也标记 inode 的使用情况,管理文件和目录的创建、删除和查找。GDT还记录每个块组中剩余的空闲数据块数量,用于分配新文件数据。
3.3.6 Super Block(超级块)
上面的五个内容时每个块组都必须要有的,接下来讲到的超级块则不是,假设一个分区有100个组,那么就可以有4/5/6…个超级块,在分区中,只有一个主超级块,用于存储和管理分区的主要元数据。其他位置保存的是主超级块的副本。如果主超级块损坏,文件系统可以从备份超级块中恢复数据和结构信息,保证文件系统的可靠性。
Super Block用于存储整个文件系统的全局信息和元数据。它包含关于文件系统状态、大小和结构的重要信息。例如:文件系统的总大小、每个块和每个块组的大小、inode和数据块的总数量及已用和空闲的数量、块组的数量和布局、文件系统创建和最近挂载的时间信息。
四、目录文件
4.1 目录文件的基本概述
通过上面的学习我们知道,操作系统标识一个文件只认inode编号,而在现实生活中用户标识文件却是用的文件名,所以需要将inode编号与文件名对应起来,这里就要提到目录文件了。
目录文件中存储的是该目录下文件的文件名与inode编号的对应关系,这就是为什么目录中不能有同名文件,在目录中文件名是作为key值来存储的,inode编号在整个分区中又是唯一的存在,所以文件名和inode编号可以互为键值进行互相查找,那么我们想访问一个文件时,就可以打开该文件所在的目录,根据文件名找到对应的inode编号,就可以访问一个文件了。
因为目录也是文件,所以上级目录中也可以存储下级目录与inode编号的对应关系,所以从根目录开始根据目录名和inode编号的关系一层一层的打开目录,就形成了路径。
在之前的文件权限那一篇文章中,我们知道了向一个目录中新建文件、删除文件和修改文件名的时候,对于一个目录来说需要w权限,这是因为进行上述操作时,需要向目录文件中新增/删除/修改目录中文件名与inode编号的对应关系,所以需要w权限。
有人会有疑问,我们在使用ll指令打印文件属性的时候有文件名,那么文件名属于文件属性吗?在Linux中,文件名并不是文件属性,所以inode中不会存储文件名,文件名是存储在目录中的。
4.2 如何查找一个文件
例如在下面这张图片中,应该如何找到process.c文件呢?我们只需要找到它的上级目录File_Operation,再通过目录中文件名与inode编号的对应关系找到inode编号,最后根据inode编号定位文件的inode,就可以通过inode访问文件的属性和内容了。
一切都显得那么合理,但是上面的方法显然忽略我们从哪里找到它的上级目录File_Operation呢?目录也有上级目录,我们可以通过上级目录找到目录File_Operation,那么怎么找到File_Operation目录的上级目录呢?我们就一直向上找就会找到根目录,那么我们就可以从根目录开始一层一层的向下查找,就可以找到对应的文件了,也就是说我们要找到一个文件就可以通过路径的方式来找到文件。
我们找到文件需要路径,那么文件路径从哪里来呢?我们之前打开一个文件的时候,没有给文件传路径时,也将文件还是被打开了,所以文件的路径是进程给的,进程在运行起来的时候会记录它所在的路径,操作系统就可以根据进程所在的路径进行解析找到对应的文件,在我们打开一个文件的时候,也可以带上路径,操作系统就可以根据我们所给的路径进行解析来找到对应的文件。
那么是否我们每一次打开一个文件都需要从根目录开始来找到对应的文件呢?操作系统对高频访问的路径进行缓存,所以当我们第二次去找同一个文件时,就可以去内存中获取到文件的路径,再根据路径来找到对应的文件。
Linux操作系统中存在一个核心数据结构dentry,dentry提供了一种方法来缓存文件路径的各个部分,从而加速对文件系统的访问,当我们开机启动的时候,dentry就会默认的将我们常用的路径、文件名和inode的映射关系导入到内存中。
但是这里还有一个问题,inode编号只是在一个分区中是唯一的,但是我们可能有很多个分区,那么我们该如何确定一个文件在哪一个分区中呢?一个磁盘被分区格式化以后,Linux要访问这个分区就需要将这个分区进行挂载,例如后面这个指令mount /dev/sdb1 /sdb-u
就是将 /dev/sdb1
这个分区与/sdb-u
这个目录产生关联,每个文件都有路径,当文件在/sdb-u
这个目录的时候,也就知道文件在 /dev/sdb1
这个分区中了。df -h
命令可以查看分区挂载情况,我的机器只有一个分区,这个分区与根目录进行了关联。
当用户使用fopen函数来打开一个文件,操作系统需要干什么呢?例如:FILE* fp = fopen("./fortest.txt","r");
,操作系统会根据进程的CWD和文件名进行组合获取到路径,找到文件的上级目录,根据上级目录的inode中的文件名与inode编号的对应信息找到文件的inode编号,通过inode编号找到文件的inode,操作系统创建一个struct file对象,将inode中文件的属性信息写入到file对象中并分配对应的文件描述符,再根据inode维护的数组找到存储文件的数据块,将里面的内容写入到文件缓冲区中,再将文件缓冲区中的内容拷贝到用户缓冲区中,最后返回一个FILE的指针,用户就可以通过FILE指针读到文件的内容了。
五、软链接与硬链接
5.1 ln指令
格式:ln [options] target [link_name]
target是要链接的文件或目录的路径,link_name是创建的链接的名称。如果不指定link_name,则默认使用target的文件名作为链接名。
常用选项:
- -s:创建软链接
- 不带选项:创建硬链接
下面我分别创建两个文件,并分别创建软链接和硬链接,实验结果如下,我们发现创建软链接的文件和软链接文件的inode编号不同,但是创建硬链接的文件和硬链接文件的inode编号却相同。
目前能够得到的结论就是:软链接形成的文件是独立的文件,硬链接形成的文件不是独立的文件。
5.2 为什么要有软硬连接
我们在使用Windows的时候,通常安装一个软件后,会在桌面上创建一个快捷方式,这个快捷方式的属性就是这个软件的路径,我们点击这个快捷方式就可以打开这个软件,通过路径找到应用程序再点击也可以打开这个软件,那么这个快捷方式存在的意义是什么呢?因为我们安装软件后,这个应用程序通常会是一个很长的路径,不方便用户去启动,所以可以创建一个链接文件,放在一个显眼的位置,方便用户去启动这个软件。
在Linux中也会有同样的场景,当我们完成一个项目以后,这个项目通常不会是裸露的,而是有很多目录结构,当我们使用这个项目的时候,还需要一个一个目录去找到它以后才能使用,这样就会很麻烦,所以我们可以通过创建链接的方式,很方便的使用我们的项目。
5.3 软硬链接是什么?
软连接
Linux中的软链接就类似于Windows中的快捷方式,它是一个独立的文件,有独立的inode,软链接文件中存储的内容就是指向目录文件的路径。
硬链接
硬链接不是一个独立的文件,它是指定目录内部的一组文件名与inode编号的对应关系。
这里给文件属性提一个新概念:硬链接数
硬链接数有多少个不同的文件名指向同一个inode。创建一个文件时,这个文件的硬链接数为1,为其创建一个硬链接,那么它的硬链接数就加一,若是删除文件或是硬链接文件,它的硬链接数就减一,inode中有一个引用计数就是用来记录有多少个文件名指向它的,当引用计数为0时,这个文件才算被删除,若一个文件在有硬链接文件的前提下,删除文件,那么这个文件并不算被删除,我们可以算作为文件重命名。文件名在目录中具有唯一性,这样看来文件名很想我们学过的“指针”了。
下面我分别创建一个文件Newfile和一个目录Newdir,这时候我们发现文件的硬链接数为1,这是因为只有一个文件名指向文件,而目录的硬链接数为2,这是因为除了Newdir是指向目录的,在目录中有一个隐藏文件.
也是指向目录的,我们之前说.
代表的是当前目录,凭什么?凭它的inode和Newdir的inode是同一个。
当我在Newdir中再创建一个目录dir,返回上级目录查看文件属性,我们发现Newfile目录的硬链接数变为了3,现在相信大家都知道原因了吧,就是dir中有一个文件..
与Newdir的inode是同一个。所以我们cd ..
能回退到上级目录,就是因为..
表示的就是当前目录的上级目录。
5.4 用户无法对目录产生硬链接
下面我先对根目录进行软链接,然后进入到这个软链接文件中,再查看目录中文件的详细信息,发现这些文件确实是根目录的文件。
当我对根目录进行硬链接时,则提示我不能对根目录进行硬链接,然后我再对我自己创建的目录进行硬链接,同样提示我不能硬链接。
这是因为Linux的目录结构通常被描述为一个多叉树,硬链接文件可以是目录文件,当我们对目录进行硬链接以后,例如我在一个目录中对根目录进行硬链接,那么这个目录中的硬链接文件就指向了根目录,那么就产生了环形路径,会导致某些查找命令变为死循环。
当我们查看目录中的隐藏文件时,就会发现./..
这两个硬链接文件是指向目录的,这两个硬链接存在的原因是为了定位当前目录和上级目录,否则会为用户切换路径造成很大的麻烦,又这两个文件的名字是固定的,虽然形成了环形路径,但是在使用查找命令时,对这两个文件名进行特殊处理,所以操作系统不允许用户对目录进行硬链接,但是创建目录时,则目录中必定会有两个硬链接文件指向目录。
六、ACM时间
这里的Access、Modify 和 Change 分别是访问时间,最近修改时间和最近更改时间,我们将文件的时间简称为ACM时间。
6.1 Access时间(Atime)
Access时间(Atime):指最近访问文件内容的时间,当文件内容被读取时,atime会被更新。这包括使用如cat、more、less等命令查看文件内容时。注意,并非所有读取操作都会更新atime,这取决于文件系统挂载时使用的选项(如noatime、relatime等),这些选项可以减少对磁盘的写入操作,从而可能提高系统性能。
6.2 Modify时间(Mtime)
Modify时间(Mtime):指最近修改文件内容的时间。也就是说,当文件的内容发生任何变化时,Mtime都会更新。例如,使用文本编辑器对文件进行编辑并保存,或者通过命令行工具(如echo、cp等)修改文件内容时,mtime会发生变化。
6.3 Change时间(Ctime)
Change时间(Ctime):指最近一次更改文件状态或属性的时间。这里的“状态或属性”包括但不限于文件的权限、所有权、链接数等元数据。因此,当使用如chmod、chown、ln等命令修改文件的这些属性时,Ctime会更新。值得注意的是,即使文件的内容没有发生变化,只要其属性被修改,Ctime就会改变。
6.4 更新机制
当仅读取或访问文件时,Access时间(Atime)会改变,但 Mtime 和 Ctime 不会改变。
当修改文件内容时,Mtime 和 Ctime 都会改变,但 Atime 不一定改变(这取决于具体的文件系统和挂载选项)。
当修改文件权限属性时,只有 Ctime 会改变,而 Atime 和 Mtime 不会改变。
结尾
如果有什么建议和疑问,或是有什么错误,大家可以在评论区中提出。
希望大家以后也能和我一起进步!!🌹🌹
如果这篇文章对你有用的话,希望大家给一个三连支持一下!!🌹🌹