Ext 文件系统基础:Linux 存储基石入门(下)

在上一篇文章中,我们从最底层的硬件出发,完整认识了磁盘的物理结构、CHS 与 LBA 寻址方式,并最终把复杂的机械磁盘抽象成了一个可以用数字下标直接访问的"一维数组"。我们明白了扇区是磁盘的最小存储单元,理解了操作系统如何通过 LBA 定位数据,也清楚了磁盘硬件是如何完成从物理位置到逻辑地址的转换。

但仅仅拥有可寻址的扇区,还远远不够。

一堆零散的扇区无法直接构成文件、目录,更无法实现数据的高效存储、查找、删除与权限管理。真正让磁盘从" raw 存储设备"变成"可用的数据空间",让用户可以新建、读写、管理文件的,是**文件系统**。

作为 Linux 世界最经典、应用最广泛的文件系统,**Ext 文件系统**是整个 Linux 存储架构的基石。它定义了磁盘如何分区、如何管理块与索引节点、如何组织目录结构、如何记录文件属性与数据位置,也是内核与磁盘 IO 交互的核心逻辑所在。

本篇我们将在上一期硬件基础之上,正式进入**Ext 文件系统**的世界,从超级块、索引节点、数据块等核心结构入手,揭开 Linux 文件系统的底层原理,看看操作系统究竟是如何在磁盘之上,搭建起一整套稳定、高效、可靠的存储体系。

1.引入文件系统

1.1引入"块概念"

我们在上一篇文章讲过扇区是磁盘的基本存储单位,它的大小一般是512字节,但是实际情况下操作系统读取磁盘时不会一个扇区一个扇区的进行读取因为这样太慢了,效率太低,操作系统会一次性读取多个扇区,即一次性读取一个(block)。

前面讲过一块磁盘会有分区 比如我们windows下的C盘,D盘,这些分区往往是会被划分成一个个的"块",一个块的大小是由格式化 的时候确定的,并且是不可更改的,最常见的是4KB(即8个扇区组成一个块),而块是文件存储的最小单位(即操作系统进行文件读写操作时的最小单位)。

在了解了块之后结合我们前面学习过的,磁盘本质上是一个三维数组,我们可以将其看成一个一维数组数组下标就是LBA,每个元素都是扇区。

8个扇区是一个块那么每一个块的地址我们也能算出来。

块号 = LBA /8

LBA = 块号*8 +n (n是第几个扇区)

因此我们最终就可以将磁盘抽象成一个个块组成的"一维数组"。

1.2引入"分区"概念

分区想必大家并不陌生,我们前面也讲过在Windows中,比如我们的笔记本电脑它只有一块磁盘但是实际我们可以分出C盘,D盘,那么这里的C盘D盘就是分区。分区本质上是对硬盘的一种**格式化。**我们知道对于Linux来说一切皆文件,那么该怎么进行分区呢?

柱⾯是分区的最⼩单位,我们可以利⽤参考柱⾯号码的⽅式来进⾏分区,其本质就是设置每个区的起 始柱⾯和结束柱⾯号码。此时我们可以将硬盘上的柱⾯(分区)进⾏平铺,将其想象成⼀个⼤的平⾯,如下图所⽰


这里有一个需要注意的点,柱面大小是一致的扇区数一致的,所有我们只需要知道分区的起始柱面号和结束柱面号,知道每一个柱面有多少个扇区那么就能确定该分区有多大


1.3引入"inode"概念

我们前面说过文件 = 内容(数据)+属性,当我们使用ls -l时除了能看到文件名还有文件属性

从左到右:

文件拥有者所属组其他所拥有的权限。

硬连接数

文件所有者

文件所属组

文件大小

文件最后修改时间

文件名

ls -l 本质上在我们使用后,ls指令本身会变成进程然后读取磁盘的文件信息,然后打印出来

前面我们讲过文件数据存储在数据"块"中,那么现在有个问题"文件属性应该存储在哪呢?"所以我们还要找一个地方来存储文件的元信息 (文件属性信息),比如上文提到的的文件创建者,创建日期,文件大小。我们将这种存储文件属性信息的区域称为inode ,中文名为索引节点

每一个文件都会有自己的inode,里面包含了与文件有关的信息(文件属性等等),这里我们了解了inode的作用显然是不够的如果想理解清楚何为inode我们还需要深入了解一下文件系统

Linux下⽂件的存储是属性和内容分离存储的

Linux下,保存⽂件属性的集合叫做inode,⼀个⽂件,⼀个inode,inode内有⼀个唯⼀ 的标识符,叫做inode号

下面带大家看看inode长什么样

这里简单介绍一下感兴趣的可以自己去查找一下

字段 类型 含义说明
i_mode __le16 文件类型与权限位(rwx 权限、文件类型如普通文件 / 目录 / 符号链接)
i_uid __le16 所有者 UID 的低 16 位
i_gid __le16 所属组 GID 的低 16 位
i_links_count __le16 硬链接计数,记录指向该 inode 的目录项数量
i_size __le32 文件大小(单位:字节),注意:目录文件的大小通常是目录项的总字节数
i_atime __le32 最后访问时间(Access Time)
i_ctime __le32 创建 / 状态变更时间(Change Time,如权限修改、链接数变化)
i_mtime __le32 最后修改时间(Modify Time,文件内容被修改的时间)
i_dtime __le32 删除时间(Deletion Time),文件被删除后记录该时间
i_blocks __le32 文件占用的磁盘块数(单位:512 字节扇区,不是文件系统块)
i_flags __le32 文件标志位(如只读、不可删除、压缩、同步写等特殊属性)

其中i_block中存储了该文件数据块对应的块号也是后面用来查找文件数据的关键,后续将为大家着重介绍。

再次注意:

⽂件名属性并未纳⼊到inode数据结构内部

inode的⼤⼩⼀般是128字节或者256,我们后⾯统⼀128字节

任何⽂件的内容⼤⼩可以不同,但是属性⼤⼩⼀定是相同的

到这里我相信大家肯定还是很疑惑的,不过不用担心这些在我们深入了解文件系统后都会迎刃而解,我相信现在大家就有两个问题

  1. 我们已经知道硬盘是典型的"块"设备,操作系统读取硬盘数据的时候,读取的基本单位 是"块"。"块"⼜是硬盘的每个分区下的结构,难道"块"是随意的在分区上排布的吗?那要怎么找到"块"呢?

  2. 还有就是上⾯提到的存储⽂件属性的inode,⼜是如何放置的呢?

操作系统不会让它们胡乱排序那样不方便管理所以操作系统会想办法对其进行管理,而文件系统就是对这些进行组织管理的即(先描述再组织

2.ext2文件系统

2.1宏观认识

文件 = 内存 + 属性,经过上面学习我们知道文件内容存储在数据块中而属性存储在inode中,那么多文件肯定是需要管理的,是时候认识一下文件系统了。我们如果想要在硬盘上存储文件,就必须把硬盘格式化为某种格式的文件系统才能存储文件。而文件系统的目的就是组织和管理硬盘中的文件。 在Linux系统中,最常见的就是ext2文件系统。早期版本为ext2,后来又有ext3,ext4.但是主体上并没有太大的变化,这里以ext2作为介绍对象。

ext2文件系统将整个分区分成若干个同样大小的块组,只要能管理一个分区就能管理所有分区,也就可以管理整个磁盘了

上图中启动块(BootBlock/Sector)的⼤⼩是确定的,为1KB,由PC标准规定,⽤来存储磁盘分区信息和启动信息,任何⽂件系统都不能修改启动块。启动块之后才是ext2⽂件系统的开始。感兴趣的可以自己去了解这里不作为重点接下来我们将从Block Group为大家进行介绍带大家揭开文件系统的面纱。

2.2Block Group

ext2文件系统大家这里可以将一个分区理解成一个文件系统,而ext2文件系统会根据分区大小将分区划分成若干个Block Group。每个Group都由相同的结构组成。


这里给大家举个例子方便理解:大家可以将一个分区理解成一个国家而一个政府想要管理国家显然太累,所以各个省份都有自己的政府帮助管理对应的各个省份下面的各个市也有市政府管理自己的城市,在文件系统也一样一个分区划分成一个个组,而一个个组内有自己的管理层来管理文件


2.3 块组内部组成

文件系统(如ext2/ext3/ext4)将存储空间划分为多个块组(Block Group),每个块组包含以下核心组成部分,用于高效管理磁盘空间和文件元数据。


2.3.1 超级块(Super Block)

存放⽂件系统本⾝的结构信息,描述整个分区的⽂件系统信息。记录的信息主要有:bolck和inode的总量,未使⽤的block和inode的数量,⼀个block和inode的⼤⼩,最近⼀次挂载的时间,最近⼀次写⼊数据的时间,最近⼀次检验磁盘的时间等其他⽂件系统的相关信息。可以将超级块理解成为一个国家的枢纽,如果被破坏了那么这个国家也就瘫痪了,SuperBlock的信息被破坏,可以说整个⽂件系统结构就被破坏了

这里有一个需要注意的点超级块在每一个块组开头都有一份拷贝(第一块一定要,后面块组可以没有)。这样做是为了保证文件系统在磁盘部分扇区出现物理问题时仍能正常工作,这样就必须保证super block信息在这种情况下也可以正常访问,所以一个文件系统的super block会在多个块组进行备份防止某个存储超级块信息的扇区出现问题时文件系统还能正常运行,而这些super block区域的数据保持一致。


2.3.2 GDT(Group Descriptor Table)

上面讲述的超级块是用来描述整个分区的"中央政府",而接下来讲的GDT就是描述一个块组的"地方政府"。

GDT又名块组描述符表,用来描述块组的属性信息,一个分区有多少个块组就有多少个GDT。每个GDT内部存储了一个块组的描述信息,例如这个块组中哪里开始是inode Tabel,哪里开始是DataBlocks,类似进程虚拟地址空间描述哪到哪是堆区是栈区,它还存储了空闲的inode和数据块有多少个等等。块组描述符表在每个块组开头都会有一份备份,这样做也是为了防止GDT区域受损时不至于该块组瘫痪。


2.3.3 块位图(Block Bitmap)

  • 作用:标记块组中每个数据块的使用状态(1=已分配,0=空闲)。
  • 大小:通常占用一个块,每个比特对应一个数据块。
  • 示例 :若块大小为4KB,一个块位图可管理 4KB * 8 = 32K 个块。

2.3.4 inode位图(Inode Bitmap)

  • 作用:标记inode表中哪些inode已被使用(1=已分配,0=空闲)。
  • 与块位图区别:仅管理inode,不涉及数据块。

2.3.5 i节点表(Inode Table)

  • 作用:存储当前分组所有文件属性(如权限、大小、时间戳)和数据块指针。
  • inode结构
    • 直接指针(指向数据块)。
    • 间接指针(二级/三级索引,用于大文件)。
  • 固定大小:通常为128字节或256字节(取决于文件系统)。

2.3.6 Data Block

  • 作用:实际存储文件内容或目录条目。
  • 类型
    • 常规文件:存储用户数据。
    • 目录:存储文件名和对应inode号的映射表。
    • 符号链接:直接存储目标路径(短链接)或间接通过数据块存储(长链接)。
  • 分配策略:优先选择同一块组内的块,减少磁盘寻道时间。

补充说明

这里有一个需要注意的点


inode编号和块号都是以分区为单位的,是不可跨分区的,比如说在同一个分区内比如将1-10000的inode号分配给了组1那么组2就是10001-20000。同理数据块也是这样子的。这时就有人要问了,这样做有什么好处呢?,因为在真实情况下可能inode号和块号无法做到一一对应,有可能出现一个文件就将一个组内的所有块号占满了可能还不够这时我们就需要存储到其他分组的数据块中,所以inode号和block号都是以分区为单位的

2.4inode与databiock的映射

前面讲了Linux中有可能出现大文件导致一个组的数据块无法进行存储,我们前面讲过文件的i弄得中有一个i_block,它里面存储了文件的数据块块号

我们这里可以看到这个数组大小是15那么这时有人就说那一个文件最多只有15x4KB=60KB ,根本产生不了大文件啊,相信很多小伙伴都是非常疑惑的,下面为大家介绍i_block

i_block它本身就是用来进行inode和block映射的

从表中不难看到该数组前12个元素都是直接存储数据块编号,但是第13个元素一级间接块索引表指针,名字很吓人,大家可以把他理解成一个指针数组区别于前面12个元素,这个块存储了每个数据块的地址也就是说对于它来讲它可以存储的文件大小就是4MB

4KB x 1024 / 4 = 1024 个数据块

1024 x 4 /4 = 4MB

往后的2级三级以此类推就是4GB,4TB,因此就解决了Linux无法产生大文件的问题

了解完这些后为大家正式介绍什么是格式化

格式化

分区之后的格式化操作,就是对分区进⾏分组,在每个分组中写⼊SB、GDT、Block Bitmap、InodeBitmap等管理信息,这些管理信息统称:⽂件系统

只要知道⽂件的inode号,就能在指定分区中确定是哪⼀个分组,进⽽在哪⼀个分组确定 是哪⼀个inode

拿到inode⽂件属性和内容就全部都有了

格式化的本质就是写入管理信息我们平时不管是Windows还是Linux,格式化的本质就是写入管理信息。

在学完上述之后那么有一个问题问大家:

知道inode号的情况下,在指定分区,请解释:对⽂件进⾏增、删、查、改是在做什么?

这里以创建为例为大家讲解一下

创建⼀个新⽂件主要有以下4个操作:

  1. 存储属性 内核先找到⼀个空闲的i节点(这⾥是263466)。内核把⽂件信息记录到其中。

  2. 存储数据 该⽂件需要存储在三个磁盘块,内核找到了三个空闲块:300,500,800。将内核缓冲区的第⼀块 数据复制到300,下⼀块复制到500,以此类推。

  3. 记录分配情况 ⽂件内容按顺序300,500,800存放。内核在inode上的磁盘分布区记录了上述块列表。

  4. 添加⽂件名到⽬录 新的⽂件名abc。linux如何在当前的⽬录中记录这个⽂件?内核将⼊⼝(263466,abc)添加到 ⽬录⽂件。⽂件名和inode之间的对应关系将⽂件名和⽂件的内容及属性连接起来

2.5目录与文件名

这时肯定会有疑问,上述情况都是在已知inode后的情况下才能实现文件的增删查改,但是我们怎么知道文件的inode号呢?

前面说过Linux下一切皆文件,那么目录是文件吗?该怎么理解?我们以前访问文件都是用的文件名呀,没用过inode号啊?

这里为大家解答:

目录也是文件,磁盘上没有目录的概念只有文件属性+文件内容的概念

目录区别与文件,目录的属性与文件相同但是内容存储的是文件名与inode的映射关系

cpp 复制代码
 
// readdir.c
#include <stdio.h>
#include <string.h>
#include <stdlib.h>  
#include <dirent.h>  
#include <sys/types.h>  
#include <unistd.h>  
int main(int argc, char *argv[]) {  
if (argc != 2) {  
fprintf(stderr, "Usage: %s <directory>\n", argv[0]);  
exit(EXIT_FAILURE);  
}  
DIR *dir = opendir(argv[1]);  // 
系统调⽤,⾃⾏查阅
 
if (!dir) {  
perror("opendir");  
exit(EXIT_FAILURE);  
}  
struct dirent *entry;  
while ((entry = readdir(dir)) != NULL) {   // 
系统调⽤,⾃⾏查阅
if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") 
== 0) {  

continue;  
}  
printf("Filename: %s, Inode: %lu\n", entry->d_name, (unsigned 
long)entry->d_ino);  
}  
closedir(dir);  
return 0;  
}

大家可以自行尝试最后运行结果如下

这里我们就可以得到两个结论

所以,访问⽂件,必须打开当前⽬录,根据⽂件名,获得对应的inode号,然后进⾏⽂件访问

所以,访问⽂件必须要知道当前⼯作⽬录, 内容!

所以这里我们就可以解答为什么目录没有x权限我们就进不去,本质是无法获取文件名与inode的映射关系所以我们无法进去!

2-6路径缓存(Path Cache)

路径缓存是ext2文件系统用于加速路径查找的机制。当用户或应用程序访问文件时,系统需要从根目录或当前目录开始逐级解析路径名。路径缓存存储最近访问过的目录项(dentry)和inode映射关系,避免重复的磁盘查找操作。

Linux中,在内核中维护树状路径结构的内核结构体叫做: struct dentry

路径缓存的核心是dentry缓存(目录项缓存)。每个dentry包含文件名、父目录指针和对应的inode指针。当路径被首次解析时,dentry会被创建并缓存。后续访问相同路径时,系统直接从缓存中获取dentry,无需访问磁盘。

这里有几点需要注意的点

每个⽂件其实都要有对应的dentry结构,包括普通⽂件。这样所有被打开的⽂件,就可以在内存中 形成整个树形结构

整个树形节点也同时会⾪属于LRU(LeastRecentlyUsed,最近最少使⽤)结构中,进⾏节点淘汰

整个树形节点也同时会⾪属于Hash,⽅便快速查找

更重要的是,这个树形结构,整体构成了Linux的路径缓存结构,打开访问任何⽂件,都在先在这 棵树下根据路径进⾏查找,找到就返回属性inode和内容,没找到就从磁盘加载路径,添加dentry 结构,缓存新路径

2.7路径解析(Path Lookup)

在开始前有几个问题询问大家

问题:打开当前⼯作⽬录⽂件,查看当前⼯作⽬录⽂件的内容?当前⼯作⽬录不也是⽂件吗?我们访问当前⼯作⽬录不也是只知道当前⼯作⽬录的⽂件名吗?要访问它,不也得知道当前⼯作⽬录的inode 吗?

答案1:所以也要打开:当前⼯作⽬录的上级⽬录,额....,上级⽬录不也是⽬录吗??不还是上⾯的问 题吗?

答案2:所以类似"递归",需要把路径中所有的⽬录全部解析,出⼝是"/"根⽬录。

最终答案3:⽽实际上,任何⽂件,都有路径,访问⽬标⽂件,⽐如: /home/whb/code/test/test/test.c

都要从根⽬录开始,依次打开每⼀个⽬录,根据⽬录名,依次访问每个⽬录下指定的⽬录,直到访问到test.c。这个过程叫做Linux路径解析。

路径解析是将用户提供的路径字符串转换为实际inode的过程。ext2的路径解析遵循以下逻辑:

路径解析从根目录(/)或当前工作目录(.)开始。对于绝对路径(如/home/user/file),解析从根目录的inode开始;对于相对路径(如../dir/file),从当前目录的inode开始。

路径解析逐级处理每个分量(如homeuserfile)。对于每个分量,系统检查当前目录的dentry缓存。若缓存命中,直接使用缓存的dentry;若未命中,从磁盘读取目录块,查找匹配的目录项。

目录项包含文件名和inode号。系统通过inode号从inode表读取目标inode,验证权限后继续解析下一个分量。若遇到符号链接,解析链接内容并重新开始路径查找。

路径解析的优化包括RCU(Read-Copy-Update)锁减少竞争,以及预取机制提前加载可能需要的目录块。对于深层路径,ext2可能采用并行查找策略加速解析。

缓存与解析的交互

路径缓存和路径解析紧密协作。解析过程中,新发现的dentry会被加入缓存。缓存命中率直接影响性能,频繁访问的路径(如/usr/bin)会长期保留在缓存中。

缓存一致性通过事件通知维护。当文件被删除或重命名时,相关dentry会被标记为无效。后续访问会触发重新解析,确保缓存与磁盘状态同步。

2.9挂载分区

为大家扩展一个小知识点

我们已经能够根据inode号在指定分区找⽂件了,也已经能根据⽬录⽂件内容,找指定的inode了,在 指定的分区内,我们可以为所欲为了。

问题:inode不是不能跨分区吗?Linux不是可以有多个分区吗?我怎么知道我在哪⼀个分区???

结论:

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

所以,可以根据访问⽬标⽂件的 "路径前缀"准确判断我在哪⼀个分区。

想实验的可以参考下图:

/dev/loop0 在Linux系统中代表第⼀个循环设备(loopdevice)。循环设备,也被称为 回环设备或者loopback设备,是⼀种伪设备(pseudo-device),它允许将⽂件作为块设备 (blockdevice)来使⽤。这种机制使得可以将⽂件(⽐如ISO镜像⽂件)挂载(mount)为 ⽂件系统,就像它们是物理硬盘分区或者外部存储设备⼀样

结语

本篇我们从磁盘硬件的扇区、LBA寻址出发,一步步完成了**块、分区、inode**三大核心概念的铺垫,正式深入 Ext2 文件系统底层架构。我们拆解了块组的完整组成,读懂了超级块、块组描述符、各类位图、inode 表与数据块的分工逻辑,理清了 inode 元数据存储、多级指针映射数据块的核心原理,解答了大文件存储的实现逻辑。

同时我们也厘清了 Linux 独特的目录设计逻辑:目录本质是存储文件名与 inode 映射的特殊文件,目录权限的底层限制、路径解析的递归流程、dentry 路径缓存的加速机制,以及分区挂载的核心作用,完整串联起了「文件名→inode→数据块」的整套文件访问链路。

从物理磁盘的硬件寻址,到文件系统对存储空间的规范化管理,Linux 磁盘 IO 的底层基础已全部搭建完成。但在日常开发与程序运行中,代码的复用、程序的轻量化部署、资源高效链接都离不开**库文件**的支撑。

下一期,我们将基于本篇系统底层知识,过渡到程序编译与链接阶段,详细讲解**静态库与动态库**的底层原理、制作流程、使用方式,对比二者的优劣差异、内存占用与运行特性,带你搞懂程序链接的核心逻辑,进一步完善 Linux 底层技术知识体系。

相关推荐
Lumos_7771 小时前
Linux -- 进程
linux·运维·服务器
南境十里·墨染春水2 小时前
linux学习进展 进程间通讯——共享内存
linux·数据库·学习
李白你好2 小时前
RedTeam-Agent无需手动操作,AI 接管所有渗透工具,让安全测试真正自动化
运维·人工智能·自动化
小此方2 小时前
Re:Linux系统篇(五)指令篇 ·四:shell外壳程序及其工作原理
linux·运维·服务器
其实防守也摸鱼2 小时前
sqlmap下载和安装保姆级教程(附安装包)
linux·运维·服务器·测试工具·渗透测试·攻防·护网行动
焦糖玛奇朵婷2 小时前
解锁扭蛋机小程序的五大优势
java·大数据·服务器·前端·小程序
jingyu飞鸟3 小时前
Linux系统发送邮件,解决信誉等级低问题 docker compose修改启动一键使用
linux·运维·docker
Lumos_7773 小时前
Linux -- exec 进程替换
linux·运维·chrome
李白客3 小时前
国产数据库选型指南:从技术路线到实战要点
运维·数据库·数据库架构·迁移学习