【Linux】Ext系列文件系统_文件系统
hello~ 很高兴见到大家! 这次带来的是Linux系统中关于Ext系列文件系统这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页: 默|笙

一、ext2文件系统
1.1 引入“块”概念
- 现在我们知道,磁盘就是一个三维数组,我们将它看作一个一维数组,这个数组的下标就是LBA,它的元素就是每个扇区。但是,要知道,一个扇区的大小只有512字节,用来进行数据的存取是不是效率有点太低了。所以,操作系统读取硬盘数据的时候,它不会一个一个扇区进行读取,而是一次性读取多个扇区,即一次性读取一个“块”。
- 硬盘的每个分区被划分为一个个的“块”,一个块往往由8个扇区组成,大小即为4KB。这个块的大小是在格式化的时候确定的,确定了之后就不可以再更改了。“块”是文件存取的最小单位。格式化将在下文进行讲解。
- 而每一个扇区都有自己对应的LBA,有了LBA我们就能够算出这个扇区对应的块号。块号=LBA/8。知道块号,同时知道它是这个块里面的第几个扇区,也能够算出这个扇区对应的LBA。LBA = 块号*8 + n(n是块内第几个扇区)。

- 引入块这个概念最主要的原因就是提升效率。其次就是能够做到软硬件解耦,无论硬件商如何升级它们的磁盘,比如从512字节扇区升级到1024字节扇区,但只要它们对外提供的“块”接口不变,上层的操作系统不用做任何修改就能够正常工作。
2.2 分区、分组
-
一个磁盘往往是会被分为多个区的,以windows系统来看,你的磁盘会被分为C/D/E盘。而这个C、D、E就是分区。假设一个磁盘它的容量是800GB,把它分成三个区域进行管理,第一个区域200GB,第二个和第三个300GB。
-
而每一区又会分为多个组进行管理。这些组的大小通常一样,加入我将上面200GB的区域分成10个组,那么一个组的大小就是10GB。
-
一个组里面会被分为6个区域,这六个区域大小不同,功能也各不相同。它们分别是超级块(Super Block),块组描述符表(GDT),数据位图(Block Bitmap),索引节点位图(inode Bitmap),inode表(inode Table)和数据块区域(Data Blocks)。之后会对这些区域的作用一一讲解。

-
为什么要对磁盘进行分区分组?一个磁盘容量那么大,如果当成一整块去管理,会非常混乱、非常费力。就像我们国家国土面积很大,不可能直接统一管理,而是分成省、市、区分级管理。磁盘也是一样:整个磁盘 → 对应国家,磁盘分区 → 对应省,分区里再分成组 → 对应市、区。这样分级、分片管理,找数据更快、出问题只影响一小块、维护也更简单。也就是运用了分治思想。
-
能够管理一个组自然能够管理多个组,能够管理多个组就能够管理好一个区,能够管理好一个区就能够管理好多个区,能够管理好多个区就能够管理好一整个磁盘。
-
OS内部既有区的概念又有组的概念,那么OS想要管理好的话,就得先描述再组织。也就是区和组都有对应的结构体对它们进行管理。
2.3 Block Group(块组)
一个块组里面这6个区域在大体上可以分为两个区,数据区和管理区。
- 管理区:就是前 4 个区域
负责管空间、管分配,对应市里的管理人员。- 数据区:第 5 区(inode 表,文件属性)+ 第 6 区(数据块,文件内容)
负责存实际数据,对应市里的普通居民。

1. 数据区
我们知道,文件 = 属性+内容。但无论是文件的属性还是内容它们都是数据,是数据就要进行存储,那么它们存储在哪些地方呢?----数据会存储在数据块区域,而属性会存储在inode表中。
Data Blocks(数据块区域)
- 前面提到“块”是进行文件存取的最小单位,所以在这个区域以及接下来要讲的inode Table区域里面它们的空间都被分为了一个个的块。这些块就负责存储文件的内容。同时这些块都有对应的块号。
inode Table(inode表)

- 首先我们来认识什么是 inode。磁盘里面的文件肯定非常多,OS 想对这些文件进行管理,那一定得先描述再组织,所以 inode 本质上是一个结构体,它负责记录文件的各种属性。inode 里还保存了一个关键信息:这个 inode 对应的文件其实际数据所存放的数据块块号。inode 号是系统给每个 inode 的唯一编号,就像它的身份证,由 inode 在表中的位置决定,并不存放在 inode 结构体内部。而 i_block 里面存放的是对应的数据块块号,毕竟要让文件的内容和属性联系起来,要能够通过 inode 找到文件的内容和属性。
- 文件名不属于文件属性,它不存放在 inode 里面。为了节省空间并保证每个 inode 大小一致,inode 结构体里面没有char*这类可变长类型,它的长度是固定的。一般一个 inode 结构体的大小是 128 字节。inode 表里面也是一个一个的块,这些块存储的就是一个个的 inode 结构体。一个块是 4096 字节,一个 inode 结构体是 128 字节,这就意味着一个块能够存储 32 个文件的属性。一般一个文件对应一个 inode 结构体。
2. 管理区
Data Bitmap(数据位图)
- 想要把数据存到一个块里面,首先得知道哪个块能够存储数据,哪个块已经存储了内容,不能再进行存储了。而数据位图就是做这方面的工作的,每一个块都对应一个比特位,这个比特位为0则代表能够进行存储,若为1则代表这个块已经被使用了。
inode Bitmap(索引节点位图)
- 与数据位图的原理完全一样,只不过它管理的是 inode:其中的每一个 bit,用来表示对应的 inode 是否空闲可用。
GDT(块组描述符表)
- 一个组整体的信息应该存在哪里?像是这个块组的起始和结束位置、从哪里开始是inode Table、从哪里开始是Data Blocks、组里一共有多少空闲数据块、多少空闲 inode。这些信息都存在对应的块组描述符中,每一个块组都有一个块组描述符,而所有块组的描述符统一存放在 GDT(块组描述符表) 里。GDT 专门记录每个块组的全局整体信息,方便系统快速查询和分配各组的空闲资源。即所用块组共用一张块组描述符表。
Super Block(超级块)
- 那么一个分区整体的信息应该存在哪里?比如整个分区的起始和结束位置、总共有多少个块、总共有多少个 inode、还有多少空闲块和空闲 inode 等全局信息。这些内容都存放在超级块中。
- 超级块并不是每个块组都有一份,它跟其他块组的区域不同,为了安全和容错,它只存放在少数几个关键块组里(通常是第一个块组,以及部分后续块组做备份)。这样一个块里面的超级快出问题了,还能够从其他块的超级快那里拷贝一份过来。
格式化
- 在介绍完了一个块组里面的6个区域后,现在来谈一谈什么叫做格式化。格式化就是写入一个全新的文件系统,把管理信息预先写入到磁盘上面。管理信息就是上面所提到的4个管理区的信息。
二、深入了解文件系统
2.1 如何理解创建、删除、修改和查看一个文件?
-
首先来谈一谈文件的创建过程。即便是创建一个内容为空的文件,也必须先把它的文件属性保存下来。要存储属性,首先要找到一个空闲的 inode,这时我们就需要查看 inode 位图,找到可用的 inode 并得到它的 inode 编号。之后在 inode 表中,根据这个 inode 号找到对应的 inode,把文件属性存进去。如果文件有内容需要存储,就再查看数据位图,找到可以使用的数据块块号,在数据块区域把内容数据存入对应块中,最后把这些块号记录到这个文件的 inode 里建立起文件属性和内容之间的联系。
-
再谈删除,删除一个文件是否真的要将它在磁盘里面的内容和属性都清空?完全不用。只需要将这个文件inode在 inode 位图里对应的bit置0,数据位图也是一样,其中块号通过inode来找,这样就完成了文件的删除。所以删除文件并没有真正的把数据清理掉,也就是说这个文件是能够恢复的,只需要将inode位图和数据位图里面所要恢复文件的inode号和块号置为1就好。当我们一不小心误删了一个文件之后,最好就是什么都不要做,尤其是创建文件,避免原有数据被覆盖,之后再通过专业方法恢复删除的文件。
3.再来看文件的修改。无论是修改文件属性,还是修改文件内容,都必须先知道这个文件对应的 inode 编号。只有拿到这个编号,才能找到对应的 inode,对文件属性进行修改;再通过 inode 里记录的块号,找到存放文件内容的数据块,对文件内容进行修改。查看文件的过程也是一样的道理。
2.2 目录与文件名
- 从上面的讲解我们可以知道,无论是删除、修改还是查看文件,都需要先知道这个文件对应的 inode 编号。但我们平时操作文件,都是直接通过文件名来进行的,而不是使用 inode 编号;并且我们前面也强调过,文件名并不存放在 inode 结构体中。那么,系统究竟是怎么通过文件名,找到对应文件的 inode 编号的呢?
- 再就是如何理解目录?目录它是什么,Linux系统下一切皆文件,所以目录它也是文件,是文件就会由内容和属性组成,它的属性会放在inode结构体里面进行存储,那么它的内容是什么?
-
在磁盘和文件系统的角度,存储目录和存储普通文件有什么区别吗?—>答案是没有任何区别,Linux系统下一切皆文件。
-
目录中存储的,是该目录下所有文件的文件名与对应inode 编号之间的映射关系。这种映射关系本身就是数据,因此也会由数据块来存储。一个文件名对应唯一的一个 inode 编号,系统会根据文件名,在所在目录的映射关系里找到对应的 inode 编号。也正因如此,同一个目录下的文件名不能重复,否则一个名字会对应多个 inode 编号,系统就无法区分了。

-
可以用ls命令的-i选项来查看文件的inode编号。
-
这也能解释,为什么当目录没有 w 权限时,就无法在里面创建文件。因为创建文件时,操作系统需要把文件名和 inode 编号的映射关系写入到目录的数据块中,而这一步操作本质上是修改目录内容,所以必须要有目录的写(w)权限才行。同样,目录没有 r 权限时,我们无法读取目录里的文件名与 inode 编号映射关系,也就无法列出这个目录下有哪些文件。
2.3 重谈inode编号和块号
-
inode编号和块号不是组内有效,而是在整个分区有效,也就是说,inode编号和块号在所在区内的号码是唯一的。但是不能跨分区,只能跨组。
-
在一个分区内部,一共有多少个 inode、多少个数据块,在格式化时就已经固定下来,是提前规划好的。这些 inode 和数据块在整个分区里都有唯一且连续的编号,系统正是依靠这些编号来精准定位、管理每一个 inode 和数据块。
-
至于有多少个块,有多少扇区之类的,只要知道总共有多少空间,就能够计算出来。
-
因为块号是整个分区内唯一且有效,所以在一个组的数据块不够用了的时候,是可以用其他组的空闲数据块的。
-
当一个分区内的数据块被完全耗尽时,即便 inode 还有大量剩余,也无法创建新文件。
2.4 路径解析和路径缓存

-
我们现在知道,要访问 test.c 这个文件,需要先从它所在的 learning_3_8 目录的数据块里,找到 test.c 对应的 inode 编号。但新的问题来了:要访问 learning_3_8 这个目录,就得先拿到它的 inode 编号 —— 而这个编号,存在 mosheng 目录的数据块里;mosheng 目录的 inode 编号,又存在 home 目录的数据块中;home 目录的 inode 编号,最终要到根目录(/) 的数据块里找。那根目录的 inode 编号该怎么获取呢?答案很简单:根目录的 inode 编号是固定的,为2。
-
也就是说,在我们访问任何文件的时候,OS都要为我们做从/开始的路径解析。正因如此,访问文件一定要有路径。
-
但是,难道每次访问文件,都要从根目录开始一步步解析路径吗?这样效率未免太低了。
如果能把已经解析过的文件名与 inode 编号的映射关系存起来,下次再需要这个文件的 inode 编号时直接取用,就能大幅提高效率。而 Linux 系统正是这么做的 —— 它会把用户访问过的路径、目录项信息缓存起来,这就是 目录项缓存(dentry cache),不用每次都重新从根目录遍历查找。 -
访问打开过的路径节点很多,Linux要不要进行管理?当然要。所以对于这些目录项信息也要先描述再组织形成结构体,这个结构体叫做dentry(目录项结构体)。
-
每个 dentry 结构体中,都会保存对应的文件名以及指向该文件 inode 的信息。由大量 dentry 组成的整体结构,是一棵动态变化的多叉树:新访问的路径节点会被加载到树中,而长期未使用的路径节点,系统会自动回收释放,从而合理占用内存。

-
并不是只有目录才有dentry,每一个被访问的文件都会有dentry,只不过普通文件是这棵多叉树的叶子节点。
-
像我们搜索某个文件的时候,往往第一次会比较慢,第二次就会很快,这就是能体现出路径缓存存在的地方。
-
那么相对路径呢?它的底层原理和绝对路径完全一样,唯一的区别就是查找起点不同。相对路径不会从根目录开始查找,而是直接从当前工作目录出发;如果当前工作目录的节点还没有被缓存,系统就会先向前逐级解析、缓存好当前目录,再继续往下查找。
-
这棵多叉树其实是Linux系统目录树的一个子集。
-
那么最后一个问题,这个用来访问文件的路径由谁提供?—>由OS和用户提供。比如进程有cwd,我们open文件要提供路径,创建文件天然知道路径。
2.5 多级间接块寻址

#define EXT2_NDIR_BLOCKS 12
#define EXT2_IND_BLOCK EXT2_NDIR_BLOCKS
#define EXT2_DIND_BLOCK (EXT2_IND_BLOCK + 1)
#define EXT2_TIND_BLOCK (EXT2_DIND_BLOCK + 1)
#define EXT2_N_BLOCKS (EXT2_TIND_BLOCK + 1)
struct ext2_inode {
// ... 省略其他文件属性字段(如权限、大小、时间等) ...
// 核心:15个块指针数组(对应直接/一次/二次/三次间接块)
__le32 i_block[EXT2_N_BLOCKS];
// ... 省略其他字段 ...
};
- 之前我们讲过,i_block 数组用来存储文件对应数据块的块号。但从源代码可以看到,它最多只能存 15 个块号,而每个块的大小通常只有 4KB。这样一来,仅靠这 15 个直接指针,最多只能存 15 × 4KB = 60KB 的数据,那像 10GB 这样的大文件,又是怎么存得下的呢?
- 这 15 个块指针并非全部直接指向数据块,而是有明确的分工:前 12 个(i_block[0] 到 i_block[11]):是直接块指针,直接指向存储文件数据的数据块。第 13 个(i_block[12]):是一级间接块指针,它指向的块不存数据,而是存储了 1024 个直接块指针。第 14 个(i_block[13]):是二级间接块指针,它指向的块存储了 1024 个一级间接块指针。第 15 个(i_block[14]):是三级间接块指针,它指向的块存储了 1024 个二级间接块指针。简单来说,后 3 个指针是 “指针的指针”,通过层层嵌套,极大地扩展了可寻址的数据块数量。
- 直接的块容量是124KB = 48KB,一级间接块容量是10244KB = 4MB,二级间接块容量是4GB,三级是4TB,完全能够储存下10GB。
2.6 挂载分区
- 大家有没有想过,操作系统是怎么知道一个文件究竟在哪个分区里的?磁盘会先做分区,分区之后再格式化、写入文件系统。但分区就算格式化完成,也不能直接使用,必须先挂载到一个目录下,才能正常存放和访问文件。
- 正是因为分区挂载到了某个目录,操作系统才能通过路径和挂载点,清楚地知道这个文件属于哪个分区。就像 Windows 里的 C 盘、D 盘,文件路径前面会带上 C:\、D:\ 这样的盘符前缀,系统一看就知道这个文件在哪个物理分区。
三、软链接和硬链接
3.1 硬链接
介绍

- 图中圈出来的这个属性,就是文件的硬链接数。它也是我们用 ll 命令查看文件属性时,最后一个还没弄明白的属性。那到底什么是硬链接?
- 我们之前学过智能指针,其中一种智能指针可以让多个指针共同管理同一份资源,只有当所有指向它的指针都释放了,资源才会真正被回收;只删除其中一个,只是引用计数减 1 而已。硬链接的原理和它几乎一模一样:它不是复制文件,而是给同一个文件起多个别名,让多个文件名映射到同一个 inode 编号,指向磁盘上同一份数据。删除其中一个名字,只是硬链接数减 1,文件本身还在;只有当硬链接数变成 0,文件才会真正被释放。
- 我们可以通过 ln 命令来为一个文件起别名。如下:
ln 文件名 别名
- 可以看到,取完别名之后,这两个文件的硬链接数变成了2,且它们的inode编号完全相同。硬链接数就是指向这个inode编号文件名的个数。
. 和 .. 本质
- 上面所说的只是普通文件,那么目录呢?

- 观察图片可以发现,我只是创建了一个目录test,里面没有任何文件,但是它的硬链接数却是2,这是为什么?因为隐藏文件.和..。

- Linux 不允许我们手动给目录创建硬链接,但系统会自动给每个目录生成两个特殊的隐藏目录项:.:是当前目录的引用,指向目录自己的 inode;..:是上一级父目录的引用,指向父目录的 inode。也正是因为每个目录都自带一个 . 指向自己,所以空目录的硬链接数最少是 2:一个是目录本身的名字,另一个就是它里面的 .。

- 为什么Linux不允许我们手动给目录创建硬链接呢?因为手动给目录建立硬链接,非常容易在目录树中形成环形路径,导致系统在遍历、查找文件时出现死循环。而系统在进行查找的时候只需要忽略.和..这两个文件就好,这是这两个硬链接名字特殊的唯一的原因。如上图,如果在目录test下面创建对learning_3_9的硬链接,那么系统通过test目录进入hard_learning_3_9文件时就相当于回到了目录learning_3_9,然后系统又要进入test目录,就会无限循环下去。
3.2 软链接
- 硬链接是多个文件名对应同一个inode编号,而软链接则是对「路径」的引用,而不是对 inode的引用。它是一个独立的文件,它里面只存储目标文件的路径,相当于指向另一个文件的快捷方式。
- 可以用命令ln -s来创建软链接。

- 我们能清晰看到,软链接文件是有文件大小的(比如这里显示为 6),这个数值对应的就是它指向的目标文件路径的字符长度 —— 比如指向test.c时,test.c刚好是 6 个字符(t-e-s-t-.-c)。软链接本身不存储任何实际数据,它的全部内容就是目标文件的路径;当我们访问test-soft这个软链接文件时,系统会先读取里面存储的test.c路径,再根据这个路径找到并打开真正的test.c文件。比如如果软链接指向的是./src/test.c,那它的文件大小就会变成 9(对应./src/test.c的字符数),本质就是软链接文件的大小 = 目标路径的字符数。
四、文件加载进入内存的过程
- 文件的本质是「内容 + 属性」,要把文件加载到内存,本质就是把文件的属性(元数据) 和内容(数据) 都加载进来。
- 路径解析与缓存命中:系统先通过文件路径(绝对 / 相对),结合 dentry 路径缓存,解析出目标文件的 inode 编号。
- 加载文件系统元数据:把该文件所属分区的文件系统(超级块、块组描述符等)加载到内存。
- 验证 inode 有效性:通过 inode 编号查 inode 位图,确认这个 inode 是已分配、有效的,确保文件是存在的。
- 读取 inode 与数据块信息:根据 inode 编号找到对应的 inode 结构体(加载到内存),从 inode 的 i_block 数组中获取数据块的块号。
- 关联内核 file 结构体:内核为该文件创建 file 结构体,把 inode 与 file 绑定(file 记录文件的打开模式、偏移量等动态信息)。—>加载属性
- 加载数据块到内核缓冲区:根据 i_block 里的块号确认数据块位置后,把数据块的实际内容加载到内核缓冲区。—>加载内容
今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!
更多推荐





所有评论(0)