Linux Ext 系列文件系统(一):文件系统的初识
探讨 Linux 文件系统底层基础。解析磁盘物理结构、存储原理及逻辑结构,对比 CHS 与 LBA 寻址方式。阐述操作系统向磁盘写入全过程,包括指令发起、地址转换、执行阻塞及完成唤醒。介绍文件系统核心概念,包括块、分区本质及 inode 索引节点结构,说明文件属性与内容分离存储机制。

探讨 Linux 文件系统底层基础。解析磁盘物理结构、存储原理及逻辑结构,对比 CHS 与 LBA 寻址方式。阐述操作系统向磁盘写入全过程,包括指令发起、地址转换、执行阻塞及完成唤醒。介绍文件系统核心概念,包括块、分区本质及 inode 索引节点结构,说明文件属性与内容分离存储机制。

没有打开的文件是储存在磁盘中的。但是如何储存的呢?

我们先来看看磁盘的物理结构,如下图

注意:盘片的盘面看起来很光滑,其实并不是如此。它的表面有许多密密麻麻的小凸起,这些就是用来存储数据的。
上面我们讲到其实盘片的盘面并不是光滑的,上面其实有很多小凸起是用来储存数据的。那么是如何储存的?

由于磁盘的价格便宜,储存空间大,所以大公司会用磁盘储存数据,它们则统一在机房,一旦运行终身运行。如果达到年限或者出意外损坏则会进行磁盘销毁而不是出售,因为里面存储的都是数据。


即扇区是硬盘存储的最小单位。我们就可以把磁盘看做由很多个扇区构成的储存介质。
这种方法通常称作为 CHS 寻址方式(C:Cylinder 柱面,H:Header 磁头,S:Sector 扇区)
这样我们对磁盘的机械运动就有了新的理解:在磁盘上读取数据的核心在于定位扇区,机械部件移动的距离越远、次数越多,效率就越低;反之则越高。
正是因为这个物理特性,在软件设计上,我们通常会将相关联的数据尽可能存放在连续的扇区或同一个柱面内。这样在读取时,磁头不需要频繁大幅度移动,就能连续访问所需数据,从而极大地提高了读写效率。





就如我们吃的山楂卷一样虽然它是一层一层卷起来的,但当你拿在手里看的时候,你看到的是一圈圈的同心圆。我们在逻辑上把硬盘看作是由外到内、像山楂卷一样'卷'起来的一层层空心圆柱,而不是一张张平铺的盘片。

即是一维数组

即是二维数组,且柱面上每个磁道扇区个数一样

整个磁盘:

**即整个磁盘在逻辑上可以被看作是一个巨大的三维数组
[柱面][磁头][扇区]。虽然物理上它是由多张盘面堆叠而成的,但我们将所有盘面上相同半径的磁道抽象为一个柱面。因此,从逻辑视角看,磁盘就像我们吃的山楂卷一样,是由一层层柱面'卷'起来的。
寻址时,我们先确定柱面(数组第一维),再确定磁头(数组第二维),最后定位扇区(数组第三维),这就是 CHS 寻址。**
而 LBA 则是将这个三维数组在逻辑上'拍平',变成了 C/C++ 中我们熟悉的一维数组。

所以,每一个扇区都有一个下标,我们叫做 LBA(Logical Block Address) 地址,其实就是线性地址。所以怎么计算得到这个 LBA 地址呢?

操作系统只需使用 LBA 地址(如 LBA=1000)即可完成磁盘寻址,LBA 与 CHS 地址的互相转换,完全由磁盘自身的固件(硬件电路、伺服系统)负责执行。
上面我们说到 LBA 与 CHS 地址的互相转换,完全由磁盘自身的固件(硬件电路、伺服系统)负责执行,我们接下来我们通过一个形象的比喻和底层的逻辑推导,彻底把这两个概念讲透。
基于上面的'山楂卷'结构,早期的硬盘寻址采用了 CHS 模式。
CHS 的本质:
它就像是一个 三维数组disk[C][H][S]。
要找到一个数据,你需要三个坐标:先找柱面,再找磁头,最后找扇区。
随着硬盘容量越来越大,三维的 CHS 寻址变得越来越麻烦(比如不同磁道的扇区数可能不同)。于是,LBA (Logical Block Addressing) 诞生了。
核心思想:把'山楂卷'拍平,LBA 的想法非常简单粗暴: 忽略物理结构,把所有的扇区从 0 开始,一直往后数。
**LBA 的本质:**它就像是一个 一维数组
disk_lba[N]。要找到一个数据,你只需要一个下标(Index)。
LBA 的优点:
既然 CHS 是三维的,LBA 是一维的,它们之间必然存在数学上的换算关系。
假设硬盘参数:
逻辑:先算出前面所有柱面的扇区数 + 前面所有磁头的扇区数 + 当前扇区的偏移(注意扇区从 1 开始,需减 1)。

逻辑:通过整除和取余来'拆解'地址。
// 表示整除(取商)% 表示取模(取余数)



假设某硬盘参数:
问题:LBA = 1000 对应的 CHS 是多少?
1000 // 1008 = 0 (说明还在第 0 个柱面里)
1000 % 1008 = 1000
1000 // 63 = 15 (说明在第 15 个磁头的位置)
(1000 % 63) + 1 = 55 + 1 = 56
结果:
LBA 1000 对应的 CHS 地址是 (0, 15, 56)。
结论:
CHS 与 LBA 的互相转换,完全由硬盘内部的固件(硬件电路)自动完成。对操作系统来说,磁盘就是一个巨大的一维数组,下标就是 LBA 地址。

当 CPU/操作系统需要向磁盘写入数据时,整个交互流程如下:
WRITE 命令),告知磁盘要执行写操作。
💡 提示:
- 磁盘就是一个三维数组,我们把它看待成为一个'一维数组',数组下标就是 LBA,每个元素都是扇区
- 每个扇区都有 LBA,那么 8 个扇区一个块,每一个块的地址我们也能算出来。
- 知道 LBA:块号 = LBA/8
- 知道块号:LAB=块号*8 + n (n 是块内第几个扇区)
完整示例(1 块 = 8 扇区)
- 已知 LBA=15 → 块号 = 15//8=1,块内序号 = 15%8=7;
- 已知块号 = 2,要找块内第 3 个扇区 → LBA=2×8+3=19;
- 块号 0 包含的扇区 LBA:0
7;块号 1 包含 LBA:815;块号 2 包含 LBA:16~23(以此类推)
正如你所说,分区的本质就是切山楂卷
Windows 给我们展示的是 C、D、E 这种带有盘符的逻辑驱动器,而 Linux 秉承**'一切皆文件'的哲学,它不使用盘符,而是把磁盘和分区都映射成**文件。
/dev/sda(表示第一块 SCSI/SATA 硬盘)。/dev/sda1(表示第一块硬盘的第 1 个分区)。/dev/sda2(表示第一块硬盘的第 2 个分区)。注意:这些文件(如 /dev/sda1)并不是普通的文本文件,它们是设备文件。当你向 /dev/sda1 写入数据时,Linux 内核会把它翻译成磁盘 I/O 指令,最终写入到对应的柱面区间里。
想象我们把那个 3D 的'山楂卷'(柱面堆叠)拿出来,然后把它像卷纸一样展开,拉直。
sda1。sda2。这样一来,无论是 Windows 的 C/D/E 盘,还是 Linux 的 /dev/sda1/sda2,本质上都是在这条'平铺的纸带'上划定了不同的区间。

📌 注意:柱面大小一致,扇区个数一致,所以我们只要知道每个分区起始和结束的柱面号。知道每一个柱面多少个扇区,那么该区分多大,解释 LBA 是多少也就清楚了。

我们之前说过:文件 = 内容 + 属性,我们使用ls -l的时候除了能够看到文件名,还能够看到文件的内容和属性。

ls -l 每一行的 7 列依次是:
模式(权限)、硬链接数、所有者、所属组、大小、最后修改时间、文件名
到这我们要思考一个问题,文件数据都储存在'块'中,那么很显然,我们还必须找到一个地方储存文件的元信息(属性信息),比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为'索引节点'。

每一个文件都有一个对应的 inode
我们来看看 inode 文件长什么样子!
/* * 磁盘上索引节点 (inode) 的结构定义 */ struct ext2_inode { __le16 i_mode; /* 文件模式(类型 + 权限) */ __le16 i_uid; /* 文件所有者 UID 的低 16 位 */ __le32 i_size; /* 文件大小(字节) */ __le32 i_atime; /* 最后访问时间 */ __le32 i_ctime; /* 节点创建/属性修改时间 */ __le32 i_mtime; /* 文件内容最后修改时间 */ __le32 i_dtime; /* 文件删除时间 */ __le16 i_gid; /* 文件所属组 GID 的低 16 位 */ __le16 i_links_count; /* 硬链接数 */ __le32 i_blocks; /* 文件占用的块数 */ __le32 i_flags; /* 文件标志 */ union { struct { __le32 l_i_reserved1; /* 保留字段 */ } linux1; struct { __le32 h_i_translator; /* 翻译器(Hurd 系统使用) */ } hurd1; struct { __le32 m_i_reserved1; /* 保留字段(Masix 系统使用) */ } masix1; } osd1; /* 操作系统相关字段 1 */ __le32 i_block[EXT2_N_BLOCKS]; /* 数据块指针数组 */ __le32 i_generation; /* 文件版本号(用于 NFS) */ __le32 i_file_acl; /* 文件访问控制列表 (ACL) */ __le32 i_dir_acl; /* 目录访问控制列表 (ACL) */ __le32 i_faddr; /* 碎片地址 */ union { struct { __u8 l_i_frag; /* 碎片编号 */ __u8 l_i_fsize; /* 碎片大小 */ __u16 i_pad1; /* 填充字段 */ __le16 l_i_uid_high; /* 以下 2 个字段 */ __le16 l_i_gid_high; /* 原为 reserved2[0](高 16 位 UID/GID) */ __u32 l_i_reserved2; /* 保留字段 */ } linux2; struct { __u8 h_i_frag; /* 碎片编号(Hurd 系统使用) */ __u8 h_i_fsize; /* 碎片大小(Hurd 系统使用) */ __le16 h_i_mode_high; /* 模式高 16 位(Hurd 系统使用) */ __le16 h_i_uid_high; /* UID 高 16 位(Hurd 系统使用) */ __le16 h_i_gid_high; /* GID 高 16 位(Hurd 系统使用) */ __le32 h_i_author; /* 作者 ID(Hurd 系统使用) */ } hurd2; struct { __u8 m_i_frag; /* 碎片编号(Masix 系统使用) */ __u8 m_i_fsize; /* 碎片大小(Masix 系统使用) */ __u16 m_pad1; /* 填充字段(Masix 系统使用) */ __u32 m_i_reserved2[2]; /* 保留字段(Masix 系统使用) */ } masix2; } osd2; /* 操作系统相关字段 2 */ }; /* * 数据块相关常量定义 */ #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) // 块指针总数(12+1+1+1=15) // 备注:EXT2_N_BLOCKS = 15
注意:
到目前为止,相信大家还有两个问题:
其实文件系统就是为了组织管理这些的!

以及此处的 EXT2 是什么?

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online
通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online
将JSON字符串修饰为友好的可读格式。 在线工具,JSON美化和格式化在线工具,online