
一、引言在 Linux 系统编程的学习路上文件系统是绕不开的核心基石。而 ext2 作为 Linux 早期最经典的索引式文件系统不仅是 ext3/ext4 的前身更是理解文件如何在磁盘上存储的最佳入门范本。它简洁高效的块组架构、inode 与数据分离的索引模型至今仍是操作系统课程的经典案例。本文将带你走进 ext2 的世界拆解它的核心结构读懂文件在磁盘上的存储逻辑为后续深入 Linux 存储与 I/O 编程打下坚实基础。二、宏观认识我们想要在硬盘上存储文件必须先把硬盘格式化为某种格式的文件系统才能正常存放和管理文件。文件系统的核心目的就是高效组织和管理硬盘上的文件数据。在 Linux 系统中最常见的是 ext2 系列文件系统。其早期经典版本为ext2后续又逐步发展出 ext3 和 ext4。尽管 ext3 和 ext4 在 ext2 基础上做了诸多功能增强但底层核心设计思想并未发生根本改变。因此我们仍然以结构更简洁的 ext2 作为学习和演示对象。ext2 文件系统会将整个磁盘分区划分成若干个大小相同的块组Block Group如下图所示。上图中的启动块Boot Block/Sector大小是固定的为1KB由 PC 标准规定主要用于存储磁盘分区信息和启动相关信息任何文件系统都不能修改启动块。启动块之后才真正开始 ext2 文件系统的区域。三、Block GroupBlock Group 是ext2 文件系统的核心管理单元它把整个磁盘分区切分成多个大小相同的小块组每个块组都独立管理自己的 inode、数据块和元数据就像把一个大仓库分成了多个小仓库各自管理自己的货物。3.1块组的内部组成3.1.1超级块Super Block超级块是 Ext2 文件系统的总控中心它记录了整个文件系统的全局核心信息是文件系统挂载和运行的基础。可以把它理解为整个文件系统的身份证 配置清单。它记录了块大小Block Size通常为 1KB/2KB/4KB总块数、总 inode 数空闲块数、空闲 inode 数块组数量、每个块组的大小挂载时间、上次检查时间等状态信息注为了避免超级块损坏导致整个文件系统不可用ext2 会在每个块组Block Group都备份一份超级块3.1.2GDT全程Group Descriptor Table是块组描述符表它记录了每个块组Block Group的核心元数据位置是连接超级块和具体块组的关键桥梁整个分区分成多个块组就对应有多少个块组描述符。它记录了当前块组的数据块位图起始位置当前块组的 inode 位图起始位置当前块组的 inode 表起始位置当前块组的空闲块数、空闲 inode 数、目录数等统计信息注和超级块一样GDT 也会在每个块组中备份一份3.1.3块位图Block BitmapBlock Bitmap记录着Data Block哪个数据块已经被占用哪个数据块没被占用3.1.4inode位图Inode Bitmap每个bit表示一个inode是否空闲可用3.1.5i节点表(Inode Table)inode 表也叫索引节点表是 ext2 文件系统中存储所有文件 / 目录元数据的核心区域可以把它理解为文件的档案库inode 表的核心作用存储文件的元数据属性包括文件大小、所有者 UID、所属组 GID、权限模式、最近访问 / 修改 / 状态变更时间戳等。局部管理特性每个块组Block Group都维护独立的 inode 表仅管理当前块组内所有文件 / 目录的 inode 信息。inode 编号范围inode 编号以磁盘分区为单位进行全局分配不可跨分区使用。3.1.6Data Block数据区用于存放文件的实际内容由一个个数据块Block组成。根据文件类型不同数据区的存储方式也有所区别对于普通文件文件的数据内容直接保存在对应的数据块中。对于目录其下属的所有文件名与子目录名均存储在该目录对应的数据块中而 ls -l 显示的除文件名以外的信息则保存在对应文件的inode中。数据块编号以单个分区为单位进行管理不能跨分区使用。四、inode和datablock的映射inode 和 data block 的映射就是“文件属性”和“文件内容”之间的对应关系。inode 里专门有一段指针用来记录这个文件的数据存在哪几个 data block 里 。因为inode不存内容只存属性数据块地址而datablock里只存文件真实内容。我们来思考一个问题知道inode号的情况下在指定分区那么对⽂件进⾏增、删、查、改是在做什么呢我们给出结论查读 inode 读数据块增写 inode 分配 / 写数据块 更新位图改覆写 inode / 数据块 更新位图删减链接计数 → 若为 0 则释放 inode 和数据块五、目录与文件名我们现在有几个问题我们访问文件时都是用的文件名啊没用过inode号呀这其中是怎样的对应关系另外目录是文件吗该怎么理解呢答目录也是文件它是一种特殊的文件它和普通文件共用一套 inode 管理机制只是内容和用途不同。只不过数据块里存的内容不一样目录文件的数据块里存的是目录项列表文件名 → inode 号 的映射表而普通文件的数据块里存的是业务内容代码、文本、图片等。所以访问⽂件必须打开当前⽬录根据⽂件名获得对应的inode号然后进⾏⽂件访问六、路径缓存我们先引入三个问题问题 1Linux 磁盘中存在真正的目录吗答案不存在真正意义上的目录实体只有文件。磁盘上只持久保存文件的属性与文件内容。问题 2访问任何文件都要从根目录 / 开始进行路径解析吗答案原则上是的但完整从头解析效率较低因此 Linux会对历史路径结构进行缓存以加快访问。问题 3Linux 中的目录概念是如何产生的答案当打开的文件类型为目录时由操作系统在内存中维护并呈现出层级路径结构。Linux中在内核中维护树状路径结构的内核结构体是struct dentry每个 dentry 都绑定一个 inode但文件名本身只存在于 dentry 的 d_name 中inode 不记录文件名。目录在内存中就是由 dentry 构成的树形结构磁盘上只存 inode 和目录项数据内存中的 dentry 是为了加速路径查找而缓存的中间层。树形节点同时隶属于LRU最近最少使用结构用于实现节点的淘汰管理。树形节点同时隶属于哈希表以便实现快速查找。该树形结构整体构成了 Linux 的路径缓存。访问文件时会先在这棵缓存树中按路径查找若找到则直接返回对应的 inode 和文件内容若未找到则从磁盘加载路径并创建新的 dentry 节点。七、挂载分区挂载分区就是把一个磁盘分区“接入” 到 Linux 文件系统的某个目录下让这个分区里的文件能被系统访问和使用。问题inode不是不能跨分区吗Linux不是可以有很多个分区吗我怎么知道我在哪一个分区答inode 确实不能跨分区每个分区都有自己独立的 inode 编号空间同一个 inode 号在不同分区里代表完全不同的文件。那我是怎么知道我在哪个分区的呢其实是用挂载点 路径前缀匹配来判断路径所属分区它的原理就是我们上面提过的挂载点是分区的入口每个分区都挂载到目录树的某个节点挂载点比如 /、/home。系统会维护一张挂载表记录了挂载点 → 分区的对应关系。当你访问一个路径时内核会从根目录开始逐层向上匹配最长的挂载点前缀从而确定该路径属于哪个分区。八、文件系统总结在 Linux 中我们日常操作的 “文件” 和 “目录”本质上都是内核通过一套统一的数据结构管理起来的抽象对象。整个文件系统可以理解为三层结构磁盘上的真实数据 → 内存中的路径缓存 → 进程视角的文件句柄。8.1磁盘层面文件的本质是 inode 数据块磁盘上不存在真正的 “目录”只有两种核心元素inode索引节点记录文件的元数据权限、大小、属主、时间戳以及指向数据块的指针。每个文件都有唯一的 inode 号但 inode 不能跨分区每个分区都有自己独立的 inode 表。数据块Data Block真正存储文件内容的地方普通文件存数据目录文件存目录项列表。也就是说“目录” 只是一种特殊的文件它的内容就是一张文件名与 inode 的映射表。8.2内存层面路径缓存与目录项dentry为了避免每次访问文件都从磁盘解析路径内核会在内存中维护一棵目录项缓存树dentry cachestruct dentry目录项每个节点对应一个文件名指向它的父目录、子目录列表以及文件对应的 inode。这棵树会同时加入 LRU 淘汰链表和哈希表既可以快速按路径查找也能在内存不足时清理不常用节点。我们访问文件时内核会先在这棵缓存树中查找找到则直接返回 inode找不到再从磁盘加载并创建新的 dentry。8.3挂载多分区如何形成统一目录树Linux 支持多个磁盘分区但用户看到的是一棵统一的目录树关键就在于挂载每个分区都有自己的文件系统和独立的 inode 空间挂载就是将分区的根目录 “接入” 到主目录树的某个节点挂载点下。内核通过挂载表记录 “挂载点 → 分区设备” 的映射访问文件时会根据路径的最长前缀匹配自动确定它属于哪个分区再去该分区查找对应的 inode。Linux 把多个独立的磁盘分区像接插头一样 “接” 到同一棵目录树上让你感觉所有文件都在一个大目录里而不是分散在不同盘符里。8.4进程视角从路径到文件句柄的完整链路进程要读写文件需要通过一系列内核结构完成定位1.先知道自己在哪进程通过 task_struct 里的 fs_struct知道自己当前在哪个目录、用哪个分区根目录、当前工作目录、挂载信息。2.拿着文件描述符找句柄再通过 files_struct 里的文件描述符fd找到对应的 struct file。3.通过句柄定位文件struct file 里的 f_path 会指向对应的 dentry目录项缓存节点最终定位到文件的 inode。4.读写数据拿到 inode 后就能找到文件的元数据和数据块开始读写。8.5总结梳理这里我们用一个例子来梳理文件系统我们的程序怎么进行库函数的调用呢1.内核先通过路径查找找到对应的struct file2.顺着file-path-dentry-inode,拿到inode3.读出inode里的i_block数组找到库文件在硬盘的哪个扇区4.先把库从硬盘搬到内核手里这时候还没给进程用5.内核要给进程分一块虚拟地址空间来放库在共享区里创建一个新的vma包括虚拟地址范围、权限、关联的struct file然后内核更新页表映射虚拟地址到物理地址的转化6.内核吧这个新vma的起始地址返回给进程进程就知道哦库的代码和数据从这个虚拟地址开始九、结语本文完整梳理ext2磁盘结构、inode映射、路径缓存与分区挂载原理并结合库加载流程串联起磁盘文件到进程虚拟内存的完整链路。我们能看清VFS、dentry缓存、虚拟内存协同工作的底层逻辑理清文件访问的完整内核流程为后续IO、内存等高阶知识打下扎实底层基础。