Linux操作系统之文件(五):文件系统(下)

发布于:2025-07-07 ⋅ 阅读:(22) ⋅ 点赞:(0)

目录

前言:

一、再谈inode

二、GDT

三、Super Block

四、更加去理解文件系统

五、EXT2下的inode结构

​编辑 

六、目录与文件名

七、路径缓存

 八、挂载分区

总结:


前言:

我们上篇文章从硬件磁盘开始为大家引入了扇区,inode等知识点。但是有关于inode的知识点很多还没来得及说完。

本篇文章将会把上篇文章所遗漏的知识点补充完毕,并为大家带来新的内容,希望对大家有所帮助。

一、再谈inode

在上篇文章的末尾,我们曾提到了几个问题:

我们怎么知道哪些inode是被使用了?哪些没被使用?

答案是位图:

inode bitmap每一个比特位上的01都代表着对应编号的inode的使用情况。同样的。block bitmap也是如此的一个位图,记录的是对应的块是否被使用。

以写入一个文件为例子,整体的过程就是:我们要去inode bitmap中查找哪一个inode没有被使用,比如说我这里查到的是8号,于是我们就把inode bitmap上的第8位置设置为1。我要写hello world到文件里,需要一个块大小的空间,那么我就去block中申请这样大小的块,随后,我们去block bitmap中查找哪些块没有被使用,查到了5号块空着,就把对应的比特位设置为1,随后把我们的hello world写入到这个块中。最后找到对应的inode table中的inode,在他的存储自己数据的block数组中,写入5号块。

这样,我们写的hello world的这个文件的属性与内容就都被保存起来了。

在linux系统中,我们想要找一个文件,就是要找到这个文件的inode编号。

根据上面的原理,我们删除一个文件,就不需要傻乎乎的把内容进行覆盖销毁,只需要把他使用的block与inode的位图,对应位置设置为0,就代表这个文件已经被我们删除了。

(当你误删一个文件时,你就什么都不要做,你所误删的文件数据不被覆盖,那你就能够找回这个文件数据)


二、GDT

简称GDT,也是每个组里都会包含的东西,我们称之为:块组描述符

其本质也是一个结构体,里面有许多个属性。块组描述符在每个块组的开头都有⼀份拷贝。

例如:

  • 块位图(Block Bitmap)的位置

  • inode 位图(inode Bitmap)的位置

  • inode 表(inode Table)的起始块

  • 空闲块和 inode 的数量

 因为GDT的存在,可以让文件系统无需遍历整个磁盘,通过GDT可直接跳转到目标块组的元数据区域,极大提升操作效率。


三、Super Block

 Super Block(超级块)是文件系统的“大脑”,存储了整个文件系统的全局元数据,是文件系统挂载和操作的基石。他存放⽂件系统本⾝的结构信息,描述整个分区(而不是块组)的文件系统信息。

比如:该区域block 和 inode的总量,未使用的block和inode的数量,⼀个block和inode的大小,最近⼀次挂载的时间,最近⼀次写⼊数据的时间,最近⼀次检验磁盘的时间等其他文件系统的相关信息。Super Block的信息被破坏,可以说整个文件系统结构就被破坏了。

我们的文件系统是以分区为划分的。每个分区都可以有不同的文件系统,比如第一个分区可以使用ext2,第二个可以使用ext3.

在我们一个分区里,假如有一百个分组。我们不需要把每个分组里都弄一个超级块,但也绝不是只有一个超级块在这个分区(可能存在三四个),这几个超级块的数据是完全一样的,为的就是在一个超级块挂掉后,可以对他进行修复。

如果只存在一个超级块,那么要是他挂掉了,就相当于整个分区也挂掉了。因为分区的大部分信息都存储在超级块中的。


四、更加去理解文件系统

 在文件系统中,一个分区的inode与Block数量,在格式化时就已经确定好了,所以他俩的数量是确定的。

那有没有可能出现,inode用完了,block还没用完的情况呢?

答案是不可能出现这种情况,因为你的block要随着你inode的数量而分配嘛。

那有没有block,用完,但是inode还没用完的情况呢?

这个情况是肯定存在的。当我们存储一些大型文件的时候,一个inode的文件就会占据多个block,自然会出现这种情况。

那我们怎么存放一个超大型的文件呢?一个块组不够用,那就多个块组呗。我们的文件的数据存储是可以跨块组的。我们记录了每个块组的起始的block编号,在此基础上加上偏移量,就可以查找一个块,因为我们的块号是以分区为体系划分的,也就是说,哪怕是不同的块组,也会存在连续的块号。

假设当前在块组A,起始块号是1。以一万为单位,那么B 的起始块号就是10001,其他块组就是20001.......

如果我们在块组A,里面有个inode记录的block编号是10042,那我们就可以计算得知这个块是在块组B中,随后计算偏移量是42,从超级块中记录的块组B的bit block位图中知道该块的信息。


关于inode编号与block编号,我们需要知道,其实inode编号是以分区单位的划分的。在我们分配inode号时,只需要确定其实的inode号就行了,后续 inode 就可以顺序编号,无需复杂管理。

因为inode 是固定大小、连续存储的,inode 表本质上就是一个 inode 结构数组,每个 inode 占据固定字节(如 ext4 是 128 或 256 字节),所以只要知道 inode 表起始 block 和 inode 大小,第 n 个 inode 就能直接计算出偏移地址。


那么操作系统是如何管理文件系统的呢?

答案仍然是:先描述,再组织!

超级块与GDT都有对应的结构体,其又可以形成链表的结构,从此对文件系统的管理,就变成内存级的操作了!!


五、EXT2下的inode结构

我们前文说得block[NUM]数组,实际上再ext2文件系统下,只有15个大小(太大就不合理了)。

而这15个块指针,其中前12个是直接指向一个数据块,后三个划分为三级指针,其中一级指针指向了一个块,这个块存储的全部都是直接块地址指针,每个指针又指向了一个数据块。

二级指针指向一个块,这个块存储的是全是一级指针,依次类推。

 

这种设计简单但低效,后续文件系统(如 ext4)对其进行了大幅优化。


结果以上知识点,我们就可以得出以下结论:

1、分区之后的格式化操作,就是对分区进⾏分组,在每个分组中写⼊SB、GDT、Block 、Bitmap、Inode Bitmap等管理信息,这些管理信息统称:文件系统
2、只要知道⽂件的inode号,就能在指定分区中确定是哪⼀个分组,进⽽在哪⼀个分组确定
是哪⼀个inode
3、我们只要拿到inode号,文件属性和内容就全部都有了


六、目录与文件名

凭什么我们能够拿到inode?我们平时在linux系统内,不都是对文件名进行操作的吗?凭什么你ls -l -i一下,就能拿到inode号。

同学们,那么还记得前面我有说过的一点吗?

文件名,是不存在在inode结构体中的!!! 

文件分为两个类型:一个是普通文件,一个是目录文件!!

没错,目录其实也是文件的一种,那么目录也跟其他文件一样,有着属性与内容,也需要自己的inode与block。

而目录文件的block数据中,存储的就是这个目录底下,所有的文件与他的inode的映射关系。

我们都知道目录rwx三种权限,如果目录也是一种文件,存储的时目录里的文件与inode映射关系的话。我们就可以解释,

为什么我们对一个目录没有r权限时我们查不到这个目录里的内容?

为什么我们没有w权限时不能新建文件,因为不能对目录的映射信息写到目录数据块里

为什么没有x权限进不去这个目录,因为我们打不开目录


我们之所以要使用inode号,而不是文件名,是因为数字的比较明显比字符串的简单,这一切都是为了提高效率。

我们ll的时候,究竟做了什么呢?

:遍历文件名与inode映射关系,随后根据inode号,找到对应文件的一系列属性

那么,我们目录也是文件,目录的inode号,又是从哪里来的呢?

答案是:路径解析!!!

 

 我们每一个目录,它都可以追溯到自己上一级目录(上一级目录存储了它的inode),所以,我们就可以倒推到根目录底下。而根目录的inode,是固定编号的。

所以我们就理解了,为什么每一个文件都会有当前的路径cwd,因为没有路径,就不能进行路径解析,就找不到根目录。

所以,访问⽂件,必须打开当前⽬录,根据⽂件名,获得对应的inode号,然后进⾏⽂件访问
所以,访问⽂件必须要知道当前⼯作⽬录,本质是必须能打开当前⼯作⽬录文件,查看⽬录⽂件的
内容!

而这个路径,又是谁给你的呢?

答案是,进程!!!

你访问⽂件,都是指令/⼯具访问,本质是进程访问,进程有CWD!进程提供路径。
你open文件,也提供了路径。

七、路径缓存

问题1:Linux磁盘中,存在真正的⽬录吗?
:不存在,只有⽂件。只保存⽂件属性+⽂件内容
问题2:访问任何⽂件,都要从/⽬录开始进⾏路径解析?
:原则上是,但是这样太慢,所以Linux会缓存历史路径结构
问题2:Linux⽬录的概念,怎么产生的?
:打开的⽂件是⽬录的话,由OS⾃⼰在内存中进⾏路径维护

Linux中,在内核中维护树状路径结构的内核结构体叫做: struct dentry ,Linux系统下,会通过dentry这个结构,进行路径的缓存,所以dentry是一个内存级的数据结构。通过路径缓存,就能大大加快我们解析文件路径解析的速率:

struct dentry {
    atomic_t d_count;               // 引用计数
    unsigned int d_flags;           // 状态标志(如 DCACHE_UNUSED)
    struct inode *d_inode;          // 关联的 inode(文件/目录的实际数据)
    struct dentry *d_parent;        // 父目录的 dentry
    struct qstr d_name;             // 文件名(如 "file.txt")
    struct list_head d_child;       // 兄弟节点链表(同一目录下的其他 dentry)
    struct list_head d_subdirs;     // 子目录链表(如果是目录)
    const struct dentry_operations *d_op; // 操作函数表
    struct super_block *d_sb;       // 所属的超级块
    void *d_fsdata;                 // 文件系统私有数据
    // ...
};(简化版)

dentry 主要维护 文件名到 inode 的映射关系,并缓存目录层级结构,以加速文件路径查找

而dentry这种映射是文件系统实现“按名称访问文件”的核心机制。

我们之前说得文件描述符如何与我们所学的文件系统关联起来呢?

其实就是文件描述符可以找到文件,而这个文件属性中,存在struct dentry的数据结构,这个结构,记录了当前文件(可能是目录文件,也可能是普通文件)与它的inode编号的映射(因为我们有d_child与d_subdirs,这也就是为什么我们说目录中存储了当前目录下的所有文件的inode映射关系),同时,会存在

 从而实现一个多叉树状的结构。

因为我们的操作系统本质上就是一个多叉树,我们可以通过tree命令来看一个目录的树状结构。

 八、挂载分区

我们已经能够根据inode号在指定分区找⽂件了,也已经能根据⽬录⽂件内容,找指定的inode了,在指定的分区内,我们可以为所欲为了。可是: inode不是不能跨分区吗?Linux不是可以有多个分区吗?我怎么知道我在哪⼀个分区???

 一块硬盘可以有多个分区(如/dev/sda1、/dev/sda2),每个分区可格式化为不同文件系统。

当访问路径如/home/user/file.txt  时,如何知道该路径属于哪个分区? 

Linux 的解决方案:挂载点(Mount Point)

挂载的本质是将分区的文件系统“嫁接”到目录树的某个位置(如将/dev/sda1 挂载到/home)。

挂载后,访问/home/user 实际访问的是/dev/sda1分区中的文件。而原目录/home 下的内容会被隐藏,直到卸载分区。

 我们可以通过df命令看一个文件系统挂载到了哪里​​​​

其他文件不用管,tmp的都是临时分区。我们可以看到,我们的vda2分区就挂载到了我们的根目录上。通过挂载,我们判断当前路径的前缀是否符合(比如最近一个符合前缀是/,那我们就知道他是在分区1上的),就能够知道我们在哪一个分区上!

分区写⼊⽂件系统,是无法直接使用的,需要和指定的⽬录关联,进⾏挂载后才能进行使⽤。

如何挂载本文就不在进行赘述,有需要的同学可以自己去搜一下。 

有三幅图,希望能够帮助大家梳理知识点:

总结:

本文正式结束了关于文件系统的核心知识点,希望大家理解inode与block的映射机制,以及把我们之间所说的fd文件描述符啊,struct file这些知识点,与进程,路径,目录,文件系统,硬件所联系起来,成为一套清楚明了的体系。

希望对大家有所帮助,有疑问的话欢迎私信或者评论区留言,谢谢各位的支持!!!