揭秘Ext文件系统:从块组到inode的底层逻辑

📅 发布时间:2026/7/4 9:06:23 👁️ 浏览次数:
揭秘Ext文件系统:从块组到inode的底层逻辑
个人主页Cx330❄️个人专栏《C语言》《LeetCode刷题集》《数据结构-初阶》《C知识分享》《优选算法指南-必刷经典100题》《Linux操作系统》:从入门到入魔《Git深度解析》:版本管理实战全解心向往之行必能至Cx330的简介目录前言文件系统为何存在一、三大基础概念块、分区、inode​1.1 块Block文件存取的最小单位1.2 分区Partition磁盘的逻辑隔离​1.3 inode文件属性的 “身份证”二. Ext 系列文件系统核心块组Block Group2.1 块组的整体结构2.2 块组内部核心构成2.2.1 超级块Super Block文件系统的 “总配置文件”2.2.2 块组描述符表GDTGroup Descriptor Table2.2.3 块位图Block Bitmap块的 “占用状态记录表”2.2.4 inode 位图Inode Bitmapinode 的 “占用状态记录表”2.2.5 inode 表Inode Tableinode 的 “存储仓库”2.2.6 数据块Data Blocks文件内容的 “存储区域”三. 文件系统的核心工作流程创建一个文件的底层逻辑前言文件系统为何存在磁盘的最小物理存储单位是扇区512 字节但直接操作扇区会面临两大问题一是效率极低频繁切换内核态二是管理混乱无法通过文件名定位数据。文件系统就像磁盘的 管理员通过划分块、分区、inode 等逻辑结构将零散扇区组织成有序体系让文件的创建、读写、删除变得高效可控。​Ext 系列Ext2/3/4是 Linux 最经典的文件系统其核心设计思想至今影响深远。本文将从三大基础概念入手深入剖析块组结构的底层逻辑帮你彻底搞懂 Ext 文件系统的工作原理。一、三大基础概念块、分区、inode​这三个概念是 Ext 文件系统的基石理解它们才能看懂后续的块组设计。1.1 块Block文件存取的最小单位块的引入原因扇区是磁盘的最小物理存储单位512 字节但频繁操作单个扇区会导致系统调用过多每次 IO 都要切换内核态效率极低。因此文件系统引入 “块” 的概念将连续的多个扇区打包成一个块作为文件存取的最小单位。核心特性​块大小由格式化时指定常见 4KB8 个扇区一旦确定不可修改块是文件系统层面的逻辑概念屏蔽了扇区的物理细节文件的内容数据会被分割成若干个块存储不足一块则占用一块。验证块大小通过stat命令可查看文件的块相关信息输出关键信息解读Blocks: 8文件占用 8 个 “磁盘块”不同系统块计数规则可能不同IO Block: 4096文件系统的块大小为 4KB即 8 个扇区。注意磁盘就是⼀个三维数组我们把它看待成为⼀个⼀维数组数组下标就是LBA每个元素都是扇区每个扇区都有LBA那么8个扇区⼀个块每⼀个块的地址我们也能算出来。知道LBA块号 LBA/8知道块号LAB块号*8 n(n是块内第几个扇区)1.2 分区Partition磁盘的逻辑隔离​本质与作用​分区是将磁盘按柱面划分的独立逻辑区域相当于 磁盘中的磁盘。每个分区可单独格式化、挂载实现分区的本质分区的最小单位是柱面每个分区有独立的起始和结束柱面从逻辑上看分区就是磁盘的 “切片”每个切片都是一个独立的存储区域Linux 中分区设备文件以/dev/sda1、/dev/vda1等形式存在如/dev/vda1表示第一块虚拟磁盘的第一个分区。实战查看分区信息通过fdisk命令查看系统分区Start/End分区的起始 / 结束扇区号Blocks分区总块数1 块 1KB此处 41941999 KB≈40GBSystem分区的文件系统类型83 对应 Linux 系统。柱面是分区的最小单位我们可以利用参考柱面号码的方式来进行分区其本质就是设置每个区的起始柱面和结束柱面号码。 此时我们可以将硬盘上的柱面分区进行平铺将其想象成一个大的平面如下图所示其实分区还可以继续分为一个个组这个我们后面还会再讲的。1.3 inode文件属性的 “身份证”inode 的核心作用文件 属性元数据 内容。文件的内容存储在块中而文件的属性所有者、权限、大小、创建时间、块映射关系等需要单独存储 —— 这就是 inode索引节点的职责。inode 的关键特性inode 结构核心字段Ext2 示例每个文件对应唯一的 inode包含唯一的 inode 号inode 大小固定常见 128 字节或 256 字节与文件内容大小无关文件名不存储在 inode 中而是存储在目录的块数据中目录本质也是文件我们之后再来详细了解inode 中最核心的字段是i_block[EXT2_N_BLOCKS]15 个指针记录文件内容所在的块编号。struct ext2_inode { __le16 i_mode; // 文件权限如0644 __le16 i_uid; // 所有者ID __le32 i_size; // 文件大小字节 __le32 i_atime; // 最后访问时间 __le32 i_mtime; // 最后修改时间内容 __le32 i_ctime; // 最后变更时间属性 __le16 i_gid; // 所属组ID __le16 i_links_count; // 硬链接数 __le32 i_blocks; // 占用块数 __le32 i_block[15]; // 块映射指针12直接1一级间接1二级间接1三级间接 };实战查看 inode 信息通过ls -li命令可查看文件的 inode 号开头的号码就是该文件的 inode 号通过 inode 号可唯一定位文件。二. Ext 系列文件系统核心块组Block GroupExt2/3/4 文件系统的核心设计是 “块组”—— 将一个分区进一步划分为多个大小相等的块组类似 “小区划分”每个块组包含完整的管理结构和数据存储区域。这种设计的优势是分散风险单个块组损坏不影响全局、提升效率IO 操作集中在单个块组减少磁头移动。2.1 块组的整体结构一个 Ext2 分区的结构如下从磁盘起始位置到末尾Boot Block固定 1KB存储分区表和启动信息文件系统不可修改每个 Block Group 结构完全相同包含超级块、块组描述符表、块位图、inode 位图、inode 表、数据块。2.2 块组内部核心构成每个块组就像一个 “独立的小文件系统”内部结构层层递进各司其职我们先来看看整体图示解析(很重要)再继续往下看关键问题解答知道一个文件 inode 号怎么在分区内找到这个文件包括属性和内容读取一个文件的超级块获取每个块组的 inode 数量通过inode 号 / 每个块组的 inode 数量快速计算出 inode 位于哪个块组然后在该块组的 inode 表的固定位置读取到 inode 结构体通过其中的数据块指针数组映射表找到对应的文件块号读取对应的内容。inode Table 和 Data Blocks 会不会出现一个用完另一个没用完的情况会的因为 inode 表和 Data Blocks数据块是文件系统内两个独立的资源池它们的分配相互独立。这会导致两种典型情况inode 用尽但数据块有剩余常见于存放海量小文件如 1KB的场景。每个文件消耗 1 个 inode 和少量数据块。当文件数量达到上限时即使磁盘还有空间也无法创建新文件。数据块用尽但 inode 有剩余常见于存放少量超大文件的场景。这些文件占满了所有数据块但占用的 inode 数量很少此时无法再写入任何数据。Super Block 为什么在 group1 里面其他组有没有难道不应该放在分区的开头嘛不属于任何一个组为啥放在组里Super Block 的主要副本确实在分区开头但是为了安全防止找不到。它的备份副本被分散存储在其他多个甚至所有块组的固定位置。当系统挂载时它只需读取任意一个完好的副本即可。这就是典型的 “鸡蛋不放在一个篮子里” 的设计。知道特定的文件的 inode 号如何去理解文件的增删查改已知文件的 inode 号后对它的 “增删查改” 操作本质上是围绕其 inode 结构和数据块配合两个核心位图Bitmap完成的查通过目录的 “文件名→inode 号” 映射找到 inode再通过 inode 的指针读取数据块增分配空闲 inode 号建立文件名与 inode 的映射写入内容时将数据块地址记录到 inode改修改内容仅更新 inode 的数据块 / 修改时间修改属性仅更新 inode 元数据inode 号不变删删除目录项映射若链接数为 0 则标记 inode 和数据块为空闲通过两个核心位图inode 号和数据块未被覆盖前可恢复。本质上文件的所有操作都是对 inode 号、inode 表、目录项、数据块、核心位图的管理inode 是贯穿始终的核心。Linux 文件删除后如何恢复以及文件删除后需要注意什么Linux 文件删除后核心是立即停止写入以避免数据被覆盖通过extundelete、testdisk等工具恢复未被覆盖的 inode 与数据块而最关键的是日常做好备份因为恢复只是最后手段。2.2.1 超级块Super Block文件系统的 “总配置文件”核心作用存储整个分区的文件系统全局信息是文件系统的 “大脑”关键存储内容块大小、inode 大小块总数、inode 总数、空闲块数、空闲 inode 数最近挂载时间、最近写入时间、最近磁盘检查时间魔数s_magicExt2 魔数为 0xEF53用于识别文件系统类型冗余设计超级块会在多个块组中备份Block Group 0 必存防止单点损坏导致整个文件系统崩溃。部分源码(选择性的看)/* * 超级块结构 */ struct ext2_super_block { __le32 s_inodes_count; /* Inodes总数 */ __le32 s_blocks_count; /* 块总数 */ __le32 s_r_blocks_count; /* 保留块数 */ __le32 s_free_blocks_count; /* 空闲块数 */ __le32 s_free_inodes_count; /* 空闲Inodes数 */ __le32 s_first_data_block; /* 第一个数据块 */ __le32 s_log_block_size; /* 块大小对数形式 */ __le32 s_log_frag_size; /* 片段大小对数形式 */ __le32 s_blocks_per_group; /* 每组的块数 */ __le32 s_frags_per_group; /* 每组的片段数 */ __le32 s_inodes_per_group; /* 每组的Inodes数 */ __le32 s_mtime; /* 挂载时间 */ __le32 s_wtime; /* 写入时间 */ __le16 s_mnt_count; /* 挂载次数 */ __le16 s_max_mnt_count; /* 最大挂载次数 */ __le16 s_magic; /* 魔数签名0xEF53 */ __le16 s_state; /* 文件系统状态 */ __le16 s_errors; /* 错误检测时的行为 */ __le16 s_minor_rev_level; /* 次修订级别 */ __le32 s_lastcheck; /* 上次检查时间 */ __le32 s_checkinterval; /* 检查最大间隔时间 */ __le32 s_creator_os; /* 创建操作系统 */ __le32 s_rev_level; /* 修订级别 */ __le16 s_def_resuid; /* 保留块的默认用户ID */ __le16 s_def_resgid; /* 保留块的默认组ID */ /* * 以下字段仅用于EXT2_DYNAMIC_REV版本的超级块 * * 注意兼容特性集与不兼容特性集的区别在于 * 如果不兼容特性集中有内核不认识的位内核应拒绝挂载该文件系统。 * * e2fsck的要求更严格如果它不知道兼容或不兼容特性集中的某个特性 * 它必须中止并且不尝试修改它不理解的内容... */ __le32 s_first_ino; /* 第一个非保留inode编号 */ __le16 s_inode_size; /* inode结构大小 */ __le16 s_block_group_nr; /* 当前超级块所在的块组号 */ __le32 s_feature_compat; /* 兼容特性集 */ __le32 s_feature_incompat; /* 不兼容特性集 */ __le32 s_feature_ro_compat; /* 只读兼容特性集 */ __u8 s_uuid[16]; /* 128位卷UUID */ char s_volume_name[16]; /* 卷名称 */ char s_last_mounted[64]; /* 上次挂载的目录 */ __le32 s_algorithm_usage_bitmap; /* 压缩算法使用位图 */ /* * 性能提示。仅当EXT2_COMPAT_PREALLOC标志开启时 * 目录预分配才会生效。 */ __u8 s_prealloc_blocks; /* 尝试预分配的块数 */ __u8 s_prealloc_dir_blocks; /* 为目录预分配的块数 */ __u16 s_padding1; /* 填充 */ /* * 日志支持当EXT3_FEATURE_COMPAT_HAS_JOURNAL设置时有效 */ __u8 s_journal_uuid[16]; /* 日志超级块UUID */ __u32 s_journal_inum; /* 日志文件的inode编号 */ __u32 s_journal_dev; /* 日志文件的设备编号 */ __u32 s_last_orphan; /* 待删除inode列表的起始 */ __u32 s_hash_seed[4]; /* HTREE哈希种子 */ __u8 s_def_hash_version; /* 默认使用的哈希版本 */ __u8 s_reserved_char_pad; /* 保留字符填充 */ __u16 s_reserved_word_pad; /* 保留字填充 */ __le32 s_default_mount_opts; /* 默认挂载选项 */ __le32 s_first_meta_bg; /* 第一个元数据块组 */ __u32 s_reserved[190]; /* 填充到块末尾 */ };2.2.2 块组描述符表GDTGroup Descriptor Table核心作用每个块组对应其中一个描述符记录该块组的详细属性信息关键存储内容块位图的块编号bg_block_bitmapinode 位图的块编号bg_inode_bitmapinode 表的起始块编号bg_inode_table该块组的空闲块数、空闲 inode 数、目录数冗余设计与超级块类似GDT 也会在多个块组中备份块组0: | 超级块 | GDT | 块位图 | inode位图 | inode表 | 数据块 | 块组1: | 超级块备份 | GDT备份 | 块位图 | inode位图 | inode表 | 数据块 | 块组3: | 超级块备份 | GDT备份 | 块位图 | inode位图 | inode表 | 数据块 | 块组5: | 超级块备份 | GDT备份 | 块位图 | inode位图 | inode表 | 数据块 | 块组7: | 超级块备份 | GDT备份 | 块位图 | inode位图 | inode表 | 数据块 | ...部分源码(选择性的看)/* * 块组描述符结构磁盘级 */ struct ext2_group_desc { __le32 bg_block_bitmap; /* 块位图所在块号 */ __le32 bg_inode_bitmap; /* Inode位图所在块号 */ __le32 bg_inode_table; /* Inode表起始块号 */ __le16 bg_free_blocks_count; /* 空闲块数量 */ __le16 bg_free_inodes_count; /* 空闲Inode数量 */ __le16 bg_used_dirs_count; /* 目录数量已使用的目录项 */ __le16 bg_pad; /* 填充对齐 */ __le32 bg_reserved[3]; /* 保留字段 */ };补充说明每个块组在GDT组描述符表中都有一个对应的ext2_group_desc结构这个结构描述的是特定块组的元数据位置和统计信息所有块组的描述符按顺序组成GDT存储在多个位置进行备份2.2.3 块位图Block Bitmap块的 “占用状态记录表”核心作用用一个 bit 位标记一个块的状态0 空闲1 已占用高效性查找空闲块时只需遍历位图中的 bit 位无需扫描所有块示例若块位图的第 3 个 bit 为 1表示该块组的第 3 个块已被占用。字节0: [bit7][bit6][bit5][bit4][bit3][bit2][bit1][bit0] 第8块 第7块 第6块 第5块 第4块 第3块 第2块 第1块 ↑ ↑ ↑ ↑ ↑ ↑ ↑ ↑ 最高位 ←-------- 字节内比特位顺序 --------→ 最低位2.2.4 inode 位图Inode Bitmapinode 的 “占用状态记录表”核心作用与块位图逻辑一致用一个 bit 位标记一个 inode 的状态0 空闲1 已占用关键特性inode 编号是分区级全局唯一的由块组号 块组内 inode 偏移量组成。2.2.5 inode 表Inode Tableinode 的 “存储仓库”核心作用连续的块组成 inode 表每个块存储多个 inode如 4KB 块可存储 32 个 128 字节的 inode关键特性inode 表的起始位置由 GDT 中的bg_inode_table指定通过 inode 号可快速计算出其在 inode 表中的位置。2.2.6 数据块Data Blocks文件内容的 “存储区域”核心作用存储文件的内容数据是块组中最大的区域存储类型普通文件直接存储文件内容目录文件存储该目录下的 “文件名→inode 号” 映射关系链接文件硬链接仅增加 inode 的链接数软链接存储目标文件路径。三. 文件系统的核心工作流程创建一个文件的底层逻辑结合上述结构我们以创建一个普通文件touch test.txt为例看看文件系统的底层操作流程查找空闲 inode遍历 inode 位图找到第一个值为 0 的 bit 位标记为 1占用分配对应的 inode写入 inode 属性将文件的属性所有者、权限、创建时间等写入 inode 表中对应的 inodei_block数组初始化为 0查找空闲块遍历块位图找到空闲块标记为 1占用若文件有内容则将内容写入该块并更新 inode 的i_block数组记录块编号更新目录数据在当前目录的数据块中添加一条 “文件名test.txt→inode 号” 的映射记录。整个过程中文件系统通过块位图、inode 位图快速定位空闲资源通过 inode 关联文件属性与内容通过目录维护文件名与 inode 的映射高效完成文件创建。实际示例4 个步骤存储属性内核找到一个空闲的 i 节点这里是263466并将文件属性如权限、时间戳、所有者等记录在其中。存储数据文件需要存储在三个磁盘块中。内核找到空闲块300、500、800并按顺序将缓冲区数据复制到这些块中。记录分配情况文件数据块按顺序300、500、800存放。内核在 i 节点的磁盘分布区记录这个块列表。添加文件名到目录文件名是abc。内核在当前目录的目录文件中添加一个条目入口(i节点号:263466, 文件名:abc)这个条目建立了文件名与文件内容和属性之间的连接。补充几个核心注意事项块和 inode 的数量在格式化时确定若 inode 耗尽如大量小文件即使有空闲块也无法创建新文件超级块和 GDT 的冗余备份是文件系统的 “救命稻草”若主超级块损坏可通过备份恢复目录也是文件其内容是 “文件名→inode 号” 的映射删除文件本质是删除目录中的该映射并将 inode 和块标记为空闲(关于这个问题我们下篇博客还会再继续讲的)。结语文件系统通过逻辑结构块、分区、inode管理物理磁盘扇区解决直接操作扇区效率低、管理混乱的问题。重点剖析块组Block Group设计将分区划分为多个独立块组每个块组包含超级块全局配置、块组描述符表元数据、位图资源状态及数据存储区inode表、数据块。通过inode关联文件属性与内容结合目录映射实现高效文件操作增删查改。冗余设计的超级块和块组描述符表确保系统容错性而位图机制则快速定位空闲资源。理解Ext文件系统的分层管理逻辑是掌握Linux存储机制的关键。