Linux_Ext系列文件系统
【Linux】Ext 系列文件系统一文吃透:磁盘寻址、inode、Block Group、路径解析与软硬链接
前言
学 Linux 文件系统时,很多同学会卡在几个看似分散的概念上:
- 扇区、磁道、柱面到底是什么
- CHS 和 LBA 有什么关系
- 为什么文件系统要有 block
- inode 为什么不存文件名
- 目录为什么也是文件
- 路径解析到底怎么找文件
- 挂载到底挂的是什么
- 硬链接和软链接到底差在哪
这些问题如果孤立看,确实有点碎。但如果从“磁盘如何被操作系统管理”这条线往下推,就会发现它们其实是一条完整链路。
这篇文章以 Ext2 文件系统为主线,串起磁盘物理结构、逻辑寻址、分区、格式化、inode、Block Group、目录项、路径解析、挂载和软硬链接。
一句话先压住全局:
文件系统的本质,是操作系统在磁盘这种块设备上组织文件内容和文件属性的一套管理方案。
一、先从硬件说起:磁盘是一个很慢但很大的外设
机械磁盘是计算机系统中比较特殊的硬件。
它有几个非常典型的特点:
- 它是外设
- 它是机械设备
- 它速度慢
- 它容量大
- 它价格相对便宜
- 它适合做持久化存储
CPU 和内存都是电子设备,速度非常快;机械磁盘需要磁头移动、盘片旋转,所以天然慢很多。
但文件最终要长期保存,就必须落到磁盘等持久化设备中。
所以文件系统要解决的第一个问题就是:
如何在一个慢速、容量巨大、按块访问的设备上,高效组织和管理文件。
二、磁盘物理结构:盘片、磁头、磁道、柱面、扇区
机械磁盘由多个盘片组成,每个盘片通常有上下两个面,每个面都有磁头负责读写。
几个核心概念:
- 磁头:负责读写某个盘面的数据
- 磁道:盘片上一圈一圈的同心圆
- 柱面:所有盘面上半径相同的磁道组成的集合
- 扇区:磁道被切分出来的小区域
- 盘片:实际存储磁性数据的圆盘
扇区是磁盘存储数据的基本单位,传统大小通常是 512B。
磁盘容量可以粗略理解为:
磁盘容量 = 磁头数 × 柱面数 × 每道扇区数 × 每扇区字节数
早期磁盘定位数据时,会使用 CHS 寻址。
三、CHS 寻址:用柱面、磁头、扇区定位数据
CHS 分别代表:
C:Cylinder,柱面H:Head,磁头S:Sector,扇区
通过三元组:
(Cylinder, Head, Sector)
就能定位磁盘上的一个扇区。
比如要读取某个位置的数据,先确定在哪个柱面,再确定哪个磁头,最后确定该磁道上的哪个扇区。
这种方式对早期磁盘很直观,但它有明显问题:容量表达能力有限。
例如传统 CHS 中,系统可能用:
- 8 bit 表示磁头
- 10 bit 表示柱面
- 6 bit 表示扇区
每个扇区 512B,那么最大容量大概就是:
256 × 1024 × 63 × 512B
大约只有 8GB 量级。
这显然无法适配后来的大容量磁盘。
于是,更适合上层使用的 LBA 出现了。
四、LBA:把磁盘抽象成一个一维数组
LBA 全称是 Logical Block Address,逻辑块地址。
它的核心思想非常漂亮:
不管磁盘物理上多复杂,上层都把它看成一个线性数组。
也就是说,磁盘可以抽象成:
sector[0], sector[1], sector[2], sector[3] ...
每个扇区都有一个线性编号,这个编号就是 LBA。
从操作系统视角看,不需要关心柱面、磁头、扇区这些物理细节,只要给出一个 LBA 编号,磁盘内部固件会自己把 LBA 转换成真正的物理位置。
CHS 转 LBA 的公式大概是:
LBA = C × (磁头数 × 每磁道扇区数) + H × 每磁道扇区数 + S - 1
其中扇区号通常从 1 开始,而 LBA 从 0 开始,所以最后要减 1。
LBA 转 CHS:
C = LBA / (磁头数 × 每磁道扇区数)
H = (LBA % (磁头数 × 每磁道扇区数)) / 每磁道扇区数
S = (LBA % 每磁道扇区数) + 1
但对于操作系统来说,重点不是公式,而是抽象:
从现在开始,磁盘就是一个元素为扇区的一维数组,数组下标就是 LBA。
这是后面理解 block、分区、文件系统的基础。
五、为什么要有 block
虽然扇区是磁盘基本存储单位,但操作系统不会每次只读写一个 512B 扇区。
原因很简单:太低效。
磁盘是典型块设备,操作系统通常一次连续读写多个扇区,这一组连续扇区就称为一个 block。
常见 block 大小是:
4KB = 8 × 512B
也就是说,一个 block 通常由 8 个扇区组成。
文件系统操作文件时,最小分配单位通常就是 block。
如果知道 LBA,可以得到 block 号:
block_no = LBA / 8
如果知道 block 号,也能大致计算块内扇区的 LBA:
LBA = block_no × 8 + n
其中 n 是块内第几个扇区。
所以磁盘管理从扇区上升到 block 后,文件系统就可以用更大的粒度组织数据,效率更高。
六、分区:把一块磁盘切成多个逻辑区域
磁盘可以划分成多个分区。
在 Windows 中,我们常见的 C 盘、D 盘、E 盘,本质上就是不同分区。
在 Linux 中,分区依然存在,只是设备都以文件形式呈现,比如:
/dev/sda1
/dev/sda2
/dev/vda1
分区本质上是磁盘上的一段连续区域。早期可以参考柱面号进行分区,指定某个分区从哪个柱面开始,到哪个柱面结束。
从 LBA 视角看,也可以理解为:
分区 = 一段连续的 LBA 范围
但注意,分区刚划出来还不能直接存普通文件。
还需要格式化。
七、格式化:写入文件系统管理信息
所谓格式化,不只是“清空磁盘”。
更准确地说:
格式化是在分区上写入文件系统的管理结构。
比如将某个分区格式化为 ext4:
mkfs.ext4 /dev/sdb1
这个过程会在分区里写入文件系统需要的元数据结构,比如:
- Super Block
- Group Descriptor Table
- Block Bitmap
- Inode Bitmap
- Inode Table
- Data Blocks
有了这些结构,操作系统才知道哪些块空闲、哪些块被占用,哪些 inode 可用,文件属性在哪里,文件内容在哪里。
所以:
分区只是磁盘空间的切分,格式化才是在这片空间上建立文件系统秩序。
八、inode:文件属性的核心载体
我们之前说过:
文件 = 内容 + 属性
文件内容存储在数据块中,那文件属性存在哪里?
答案是 inode。
inode 中文常叫索引节点,它保存文件的元信息,比如:
- 文件类型
- 文件权限
- 文件大小
- 所有者 UID
- 所属组 GID
- 访问时间
- 修改时间
- 属性改变时间
- 硬链接数
- 文件内容所在 block 的索引信息
可以通过 stat 查看文件的部分 inode 信息:
stat test.c
输出中会看到:
Size: 654
Blocks: 8
IO Block: 4096
Inode: 263715
Links: 1
Access: ...
Modify: ...
Change: ...
注意一个很关键的点:
文件名不在 inode 中。
inode 里存的是文件属性以及内容块的索引,文件名由目录维护。
这个结论后面解释目录和硬链接时非常重要。
九、inode 内部如何找到文件内容
Ext2 的 inode 结构里有一个非常重要的字段:
__le32 i_block[EXT2_N_BLOCKS];
其中:
#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)
也就是说,EXT2_N_BLOCKS = 15。
这 15 个位置用来记录文件内容所在的数据块。
可以粗略理解为:
- 前 12 个直接指向数据块
- 后面几个用于一级、二级、三级间接索引
这样小文件可以快速定位,大文件也能通过间接索引扩展存储范围。
这就是 inode 连接“文件属性”和“文件内容”的关键。
十、Ext2 文件系统的宏观结构
Ext2 文件系统会把整个分区划分为多个 Block Group。
整体结构可以简化为:
Boot Block
Block Group 0
Block Group 1
Block Group 2
...
Boot Block 大小通常为 1KB,由 PC 标准规定,用于存放启动和分区相关信息,文件系统不能随意修改。
真正的 Ext2 文件系统结构从 Boot Block 后面开始。
为什么要划分 Block Group?
因为一个分区可能非常大,如果只用一套全局管理结构,会导致管理成本高、局部性差。
划分成多个块组后,每个块组内部都有类似结构。只要能管理一个块组,就能推广到整个分区。
这很像城市管理:
国家太大,不可能所有事情都集中处理,于是划分省、市、区,每个区域维护自己的局部信息。
十一、Block Group 内部结构
一个典型 Block Group 内部包含:
Super Block
Group Descriptor Table
Block Bitmap
Inode Bitmap
Inode Table
Data Blocks
下面逐个拆开。
十二、Super Block:文件系统的全局说明书
Super Block 用来描述整个文件系统的整体信息,比如:
- inode 总数
- block 总数
- 空闲 inode 数量
- 空闲 block 数量
- block 大小
- inode 大小
- 每组 block 数
- 每组 inode 数
- 最近挂载时间
- 最近写入时间
- 文件系统状态
Super Block 非常重要。
如果 Super Block 损坏,整个文件系统结构都可能无法正确识别。
所以 Ext 文件系统会在多个 Block Group 中备份 Super Block。第一个块组必须有,后续块组可以根据策略保存副本。
这是文件系统的容灾设计。
十三、GDT:块组描述符表
GDT 全称是 Group Descriptor Table,块组描述符表。
它描述每个 Block Group 的布局信息,比如:
- Block Bitmap 在哪里
- Inode Bitmap 在哪里
- Inode Table 从哪里开始
- 当前组还有多少空闲 block
- 当前组还有多少空闲 inode
- 当前组中目录数量
Ext2 中对应结构大致是:
struct ext2_group_desc {
__le32 bg_block_bitmap;
__le32 bg_inode_bitmap;
__le32 bg_inode_table;
__le16 bg_free_blocks_count;
__le16 bg_free_inodes_count;
__le16 bg_used_dirs_count;
};
如果 Super Block 是整个文件系统的总说明书,那么 GDT 就是每个块组的地图索引。
十四、Block Bitmap 与 Inode Bitmap
Block Bitmap 用来记录 Data Blocks 中哪些块已经被使用,哪些块还空闲。
它的本质就是一张位图:
bit = 0 表示空闲
bit = 1 表示占用
Inode Bitmap 类似,只不过它记录的是 inode 是否可用。
这就是位图在操作系统中的典型应用:用一个 bit 表示一个资源的占用状态。
优势非常明显:
- 空间占用低
- 查询速度快
- 分配和回收简单
创建文件时,要先从 Inode Bitmap 中找空闲 inode,再从 Block Bitmap 中找空闲数据块。
删除文件时,也要把对应 inode 和 block 标记为可用。
十五、Inode Table 与 Data Blocks
Inode Table 保存当前块组内所有 inode。
每个 inode 保存一个文件的属性信息和数据块索引。
Data Blocks 保存真正的文件内容。
对普通文件来说,Data Blocks 中存的是文件数据。
对目录文件来说,Data Blocks 中存的是目录项,也就是:
文件名 -> inode 号
这句话很关键:
目录也是文件,目录的内容就是文件名和 inode 的映射关系。
十六、创建一个文件,文件系统做了什么
假设执行:
touch abc
文件系统大致会做几步。
第一步,分配 inode。
内核在 Inode Bitmap 中找到一个空闲 inode,比如 263466,然后在 Inode Table 中写入文件属性。
第二步,分配数据块。
如果文件需要内容,内核会在 Block Bitmap 中找到空闲 block,比如 300、500、800。
第三步,记录映射关系。
inode 中会记录文件内容所在的 block 列表。
第四步,添加目录项。
当前目录也是一个文件,它的数据块里会新增一条记录:
abc -> 263466
这样文件名就和 inode 建立了联系。
所以创建文件不是“凭空多一个文件名”,而是:
分配 inode -> 分配 block -> 建立 inode 和 block 映射 -> 添加目录项
十七、目录为什么也是文件
Linux 中目录也是文件。
只不过目录文件的内容比较特殊,它保存的是目录项。
目录项可以理解为:
文件名 + inode 号
比如执行:
ls -li
可以看到文件名前面的 inode 号。
也可以用 readdir() 读取目录内容:
#include <stdio.h>
#include <dirent.h>
int main(int argc, char *argv[])
{
DIR *dir = opendir(argv[1]);
struct dirent *entry;
while ((entry = readdir(dir)) != NULL) {
printf("Filename: %s, Inode: %lu\n",
entry->d_name,
(unsigned long)entry->d_ino);
}
closedir(dir);
return 0;
}
这段代码能直接把目录下的文件名和 inode 打印出来。
所以我们平时访问文件名,其实内核要先打开目录文件,从目录文件内容中找到文件名对应的 inode,再根据 inode 找到文件属性和数据块。
十八、路径解析:从根目录一路查到目标文件
假设要访问:
/home/code/test/test.c
路径解析大致是:
/ 根目录
-> home 在 / 目录项中找 home 的 inode
-> code 在 home 目录中找到 code 的 inode
-> test
-> test.c
每一级目录都要通过“目录内容中的文件名 -> inode 号”来向下推进。
如果是相对路径,则会从当前进程的当前工作目录开始解析。
当前工作目录是谁提供的?
进程提供的。
每个进程都有自己的 CWD,执行 open("test.c") 时,如果没有写绝对路径,就会基于当前进程的 CWD 做路径解析。
所以:
文件访问不是直接靠文件名,而是靠路径逐级解析出 inode。
十九、根目录为什么特殊
路径解析如果每次都要找父目录,那最终一定需要一个起点。
这个起点就是根目录 /。
根目录的 inode 是系统启动后必须知道的,它是整个路径结构的入口。
如果没有根目录,路径解析就没有入口。
所以 Linux 的路径树,本质上是从根目录开始,由系统和用户共同构建出来的一棵目录树。
二十、dentry:Linux 的路径缓存
如果每次访问文件都从磁盘上的根目录开始一层一层解析,效率会非常低。
所以 Linux 会缓存路径解析结果。
内核中用于维护路径缓存的重要结构是 struct dentry。
它大致包含:
struct dentry {
struct inode *d_inode;
struct dentry *d_parent;
struct qstr d_name;
struct list_head d_subdirs;
struct list_head d_alias;
struct super_block *d_sb;
};
dentry 可以理解为目录项缓存。
注意,它不只缓存目录,也可以对应普通文件。
多个 dentry 在内存中会形成类似树状结构,同时还可能被挂到:
- hash 表中,方便快速查找
- LRU 链表中,方便淘汰长期不用的缓存
所以访问文件时,内核会优先在 dentry cache 中查找路径。如果命中,就不用每次都从磁盘重新解析。
这就是路径缓存。
二十一、挂载:把一个文件系统接到目录树上
前面说 inode 编号以分区为单位,不可跨分区。
那问题来了:Linux 有多个分区,用户访问路径时怎么知道目标文件属于哪个分区?
答案是挂载。
挂载就是:
把一个已经格式化好的分区或块设备,关联到 Linux 目录树中的某个目录上。
比如:
dd if=/dev/zero of=./disk.img bs=1M count=5
mkfs.ext4 disk.img
mkdir /mnt/mydisk
sudo mount -t ext4 ./disk.img /mnt/mydisk/
这里 disk.img 是一个普通文件,但可以通过 loop device 被当成块设备使用。
挂载后:
df -h
可能看到:
/dev/loop0 4.9M 24K 4.5M 1% /mnt/mydisk
这表示 disk.img 对应的文件系统已经被挂载到了 /mnt/mydisk。
之后访问:
/mnt/mydisk/a.txt
就会进入这个新挂载的文件系统中查找。
所以:
分区写入文件系统后不能凭空使用,必须挂载到某个目录,才能融入 Linux 的统一路径树。
卸载则是:
sudo umount /mnt/mydisk
二十二、路径前缀如何决定所在分区
挂载之后,Linux 可以根据路径前缀判断目标文件属于哪个文件系统。
比如:
/home/a.txt
/mnt/mydisk/a.txt
它们看起来都在同一棵目录树下,但可能属于不同分区或不同文件系统。
这就是 Linux 路径设计很高级的地方:
多个分区通过挂载点统一到一棵目录树中,用户不需要像 Windows 一样关心 C 盘、D 盘。
从用户视角看是一棵树;从内核视角看,不同路径前缀可能对应不同 super block、不同 inode 空间和不同底层设备。
二十三、硬链接:多个文件名指向同一个 inode
真正定位文件的不是文件名,而是 inode。
既然目录项保存的是:
文件名 -> inode
那完全可以让多个文件名指向同一个 inode。
这就是硬链接。
示例:
touch abc
ln abc def
ls -li abc def
可能看到:
263466 abc
263466 def
abc 和 def 的 inode 号相同,说明它们本质上指向同一个文件。
硬链接的特点:
- 多个文件名对应同一个 inode
- 没有主次之分
- 修改任意一个,另一个看到的内容也变
- inode 中的链接计数会增加
删除硬链接时,内核并不是马上释放文件数据,而是:
- 删除目录中的文件名记录
- inode 链接数减 1
- 如果链接数为 0,再释放 inode 和数据块
所以:
rm 删除的不是文件内容本身,而是删除目录项,并减少 inode 引用计数。
二十四、软链接:一个独立文件,内容指向另一个路径
软链接也叫符号链接,创建方式:
ln -s abc abc.s
查看:
ls -li
可能看到:
263563 -rw-r--r-- abc
261678 lrwxrwxrwx abc.s -> abc
可以看到,软链接和目标文件 inode 不同。
这说明:
软链接是一个独立文件,它保存的是目标文件的路径名。
它更像 Windows 中的快捷方式。
软链接特点:
- 有自己的 inode
- 文件类型是符号链接
- 内容通常是目标路径
- 可以跨文件系统
- 目标文件删除后,软链接可能失效
硬链接是 inode 层面的引用;软链接是路径名层面的引用。
二十五、硬链接和软链接对比
| 对比项 | 硬链接 | 软链接 |
|---|---|---|
| inode | 与目标文件相同 | 与目标文件不同 |
| 本质 | 目录项映射到同一 inode | 独立文件,保存目标路径 |
| 是否有主次 | 没有 | 有,指向目标 |
| 跨文件系统 | 通常不行 | 可以 |
| 目标删除后 | 只要链接数不为 0,文件仍在 | 软链接可能失效 |
| 常见用途 | 文件备份、目录中的 . 和 .. |
快捷方式、版本切换、路径跳转 |
其中 . 和 .. 也是硬链接思想的典型体现。
.指向当前目录..指向上级目录
这也是为什么目录本身天然和路径结构强相关。
二十六、文件的三个时间
Linux 文件通常会涉及三个时间:
- Access:最后访问时间
- Modify:文件内容最后修改时间
- Change:文件属性最后修改时间
简称:
atime
mtime
ctime
注意 ctime 不是创建时间,而是 inode 属性变化时间。
比如:
- 读取文件可能更新 atime
- 修改文件内容会更新 mtime
- 修改权限、所有者、链接数等会更新 ctime
这个点在排查文件变化、构建工具、备份系统时都很常见。
总结
Ext 系列文件系统这一章,看似从磁盘硬件讲到软硬链接跨度很大,但主线其实非常清楚。
磁盘物理上由盘片、磁头、磁道、柱面、扇区构成;早期使用 CHS 寻址,后来通过 LBA 把磁盘抽象成线性数组。操作系统不会按单个扇区低效读写,而是以 block 为单位访问磁盘。分区只是磁盘空间的切分,格式化才是在分区中写入文件系统管理结构。
在 Ext2 中,分区被划分成多个 Block Group,每个块组中包含 Super Block、GDT、Block Bitmap、Inode Bitmap、Inode Table 和 Data Blocks。inode 保存文件属性和数据块索引,Data Blocks 保存文件内容。目录也是文件,它的内容是“文件名到 inode 号”的映射。
文件访问并不是直接拿文件名找内容,而是通过路径逐级解析目录项,最终拿到目标 inode。为了避免每次都从磁盘重复解析路径,Linux 在内核中维护 dentry cache。多个文件系统则通过挂载统一到一棵目录树中,路径前缀决定了访问哪个挂载点和哪个文件系统。
最后,硬链接和软链接的差别也能自然解释:硬链接是多个文件名映射到同一个 inode;软链接是一个独立文件,内容保存目标路径。
一句话收尾:
文件系统不是“把文件放到磁盘上”这么简单,而是操作系统围绕 block、inode、目录项、路径缓存和挂载点构建出来的一整套磁盘资源管理体系。
理解到这一层,再看 ls -li、stat、ln、mount、df -h,就不是在背命令,而是在观察 Linux 文件系统真实运行的痕迹。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)