ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

一般的文件系统样板(Ext2 和 Ext3)

一般的文件系统样板(Ext2 和 Ext3) 一、为什么 Ext2 和 Ext3 仍是理解文件系统的经典样板在今天的 Linux 环境中Ext4、XFS、Btrfs 已经占据了主流位置。很多开发者从接触 Linux 的第一天起使用的就是 Ext4 或 XFS对于更早期的 Ext2、Ext3 可能只停留在「听说过」的阶段。但从学习操作系统、内核实现和存储系统设计的角度来看Ext2 与 Ext3 依然是极难替代的教学样板。这不是因为它们今天还被大规模使用而是因为它们把文件系统最核心的问题用非常清晰、直观、可验证的方式暴露了出来。一个文件系统要解决的核心问题并不神秘如何把磁盘上一大块连续的、可以随机读写的存储空间组织成用户可以按名字访问、按目录组织、按字节读写的文件集合。围绕这个问题文件系统必须在磁盘布局、元数据管理、空间分配、目录索引、崩溃恢复、缓存与持久化等方面做出一整套设计决策。Ext2 的价值在于它把这些设计决策落实成了一套结构清晰、层次分明的磁盘格式。超级块、块组、块位图、inode 位图、inode 表、目录项、数据块这些概念环环相扣几乎每一层都可以用工具直接查看甚至可以手工计算。理解了 Ext2再看 Ext4 的 extent、XFS 的 B 树、Btrfs 的写时复制会发现很多设计思想并非凭空而来而是在 Ext2 的基础上逐步演进。Ext3 的价值则在于它用最小的改动解决了一个非常实际的问题崩溃一致性。它在保留 Ext2 全部磁盘结构的基础上增加了一个日志层。日志机制让「多个位置、多个步骤的元数据更新」具备了类似事务的原子性从而把异常断电后的恢复时间从可能长达几十分钟的全盘 fsck缩短到几秒到几十秒的日志回放。这个思路与数据库中的 WAL、预写日志如出一辙理解 Ext3 的日志也有助于理解更广泛的存储系统设计。此外Ext2 和 Ext3 并没有完全退出历史舞台。在许多嵌入式设备、容器只读层、initrd、启动分区、精简根文件系统以及旧的虚拟机镜像中它们仍然存在。遇到挂载失败、只读重挂载、inode 耗尽、断电后数据异常等问题时如果完全不理解 Ext2 与 Ext3 的底层结构排查会非常困难。本文的目标是围绕 Ext2 与 Ext3从背景、磁盘布局、数据寻址、目录结构、挂载过程、日志机制、三种日志模式、兼容性设计、实战工具到源码视角进行一次系统的、接近两万字规模的讲解。阅读完全文之后你应当能够回答几个关键问题Ext2 如何用 inode 组织文件多级间接块如何支撑大文件目录如何在磁盘上存储和查找Ext3 如何在不破坏 Ext2 的前提下加入日志ordered、journal、writeback 三种模式到底差在哪里以及这些机制在后来的 Ext4 中是如何演进的。二、文件系统要解决的基本问题在深入到 Ext2 的具体结构之前有必要先把文件系统要解决的问题抽象清楚。否则后面讲到超级块、inode、位图、间接块时容易陷入「记住了字段却不知道它为什么存在」的困境。从用户视角看文件系统提供了两套接口。一套是命名的层次结构也就是目录树和文件名另一套是对文件内容的线性访问也就是按偏移量读写字节。这两套接口非常自然但背后的磁盘却只提供线性块设备二者之间隔着很大的鸿沟。文件系统的任务就是在这条鸿沟上架起一座桥。更具体地说文件系统需要回答以下问题文件内容存放在哪些块中这是文件到数据块的映射问题。文件可能很大也可能很小映射结构需要兼顾空间占用和访问效率。哪些块是空闲的哪些块已经被占用这需要一套空间分配结构。分配结构要支持快速查找、快速修改并且在损坏时能够尽量重建。文件名如何对应到文件本身这需要目录结构。目录要支持创建、删除、重命名和查找同时还要支持硬链接等 Unix 语义。文件的属性存在哪里权限、属主、时间戳、大小等元信息需要独立于文件内容保存。文件系统的全局信息存在哪里总块数、总 inode 数、空闲数量、特性标志、挂载状态等需要集中保存在某处。异常断电后如何恢复到一致状态元数据更新往往涉及多步写盘任何一步中断都可能导致不一致因此需要一致性保障机制。Ext 系列对这些问题的回答形成了一套非常经典的组合超级块保存全局信息块组把大空间拆成可局部管理的单元位图管理空闲块和空闲 inodeinode 保存文件属性和数据块指针目录用特殊的文件保存「文件名到 inode 号」的映射日志则为元数据更新提供崩溃恢复能力。2.1 扇区、块与文件系统逻辑块磁盘的基本物理读写单位是扇区。传统硬盘的扇区大小是 512 字节后来高级格式化磁盘普遍采用 4096 字节的物理扇区。文件系统如果直接以 512 字节为单位管理空间会带来两个问题一是对于大容量磁盘用于记录空闲空间、数据映射的元数据会非常庞大二是每次只读写 512 字节无法充分利用顺序 IO 和缓存能力。因此文件系统引入了「块」的概念。块是文件系统分配空间和读写数据的最小逻辑单位。块大小通常是扇区大小的整数倍。在 Ext2 和 Ext3 中块大小在创建文件系统时指定常见取值有 1024 字节、2048 字节和 4096 字节。块大小的选择是一个典型的工程折中。块越大连续大文件的读写效率越高间接块的层级越少但小文件会浪费更多空间因为即使一个文件只有一个字节也要占用一个完整的块。块越小小文件的空间利用率越高但同样大小的文件需要更多的块号来描述间接块开销更高大文件的最大尺寸也更受限制。在 Ext2/Ext3 中块大小还会直接影响单个文件的最大尺寸和文件系统总容量。以 4KB 块大小为例单文件最大通常可达到 2TB 到 4TB 的理论上限而 1KB 块大小下单文件上限会急剧下降到 16GB 左右。这个限制来自 32 位块号和三级间接块的组合后文会详细推导。2.2 inode 的本质文件身份与属性Unix 系文件系统有一个非常核心的设计文件名不是文件的身份inode 才是。inode即 index node可以理解为文件的「档案」。它保存了除文件名之外的几乎所有文件元数据包括文件类型、权限、属主、属组、时间戳、链接计数、文件大小以及数据块指针。文件名和 inode 是分离的。目录项中保存的只是「文件名 inode 号」。多个目录项可以指向同一个 inode这就是硬链接。内核通过 inode 号找到 inode再通过 inode 中的数据块指针找到文件内容。只要 inode 的链接计数不为零文件数据就仍然有效当链接计数归零时inode 及其数据块才被标记为空闲。这个设计带来一个重要的推论删除文件的本质不是立即清空文件数据而是从目录中摘除目录项把 inode 的链接计数减一。如果链接计数归零再把 inode 和它指向的数据块标记为空闲。真正的数据在磁盘上并没有被立即擦除只是对应的块被纳入了空闲池等待后续分配复用。这正是数据恢复工具能工作的根本原因只要数据块尚未被新数据覆盖内容就仍然有机会被找回。inode 中包含的数据块指针是文件内容寻址的入口。Ext2/Ext3 的 inode 中有 15 个块指针分别由 12 个直接块、1 个一级间接块、1 个二级间接块和 1 个三级间接块组成。这个结构在数据寻址一章会详细展开。2.3 目录一种特殊的文件很多初学者会误以为目录是某种独立于文件的特殊结构但在 Ext 系列中目录本质上也是一种文件只是它的类型标记为「目录」其数据块中存放的内容不是普通文本而是一条条目录项。每条目录项至少包含inode 号、目录项长度、文件名长度以及文件名本身。为了加速类型判断Ext2 后期还增加了文件类型字段使内核在读取目录时不必额外读取 inode就能知道目标是普通文件、目录、符号链接还是其他类型。目录项采用变长结构。每条记录的长度会向上对齐到 4 字节记录之间可能留有填充。删除目录项时Ext2/Ext3 不会把后续目录项整体前移而是把被删除项的空间合并到前一个目录项的长度字段中。这样后续新建文件时可以复用这段空白空间同时也为数据恢复提供了机会。目录的存储方式决定了它的查找方式。Ext2/Ext3 的传统目录采用线性查找从目录数据块的开头逐条扫描目录项直到找到匹配的文件名。对于小目录这种方式完全够用但当目录中文件数量达到几万甚至几十万时线性查找的性能会急剧下降。这也是 Ext3 后来引入 htree 索引目录的原因之一。目录索引将在后文进一步讨论。2.4 块组局部管理与全局冗余一个文件系统如果只靠一份全局位图管理所有空闲块会带来两个明显问题。第一所有分配操作都要争用这张全局位图形成瓶颈第二一旦这张位图损坏整个文件系统的空间账目就完全丧失。反过来如果所有 inode 都集中存放在磁盘的一端文件数据却分散在另一端那么访问文件时需要来回寻道性能会很差。Ext2/Ext3 用「块组」来同时缓解这些问题。文件系统被划分为若干个大小相近的块组每个块组内部拥有自己的块位图、inode 位图、inode 表和数据块区。创建新文件时内核尽量在同一块组内分配 inode 和相邻的数据块以提升局部性。这样既分散了分配压力又减少了元数据集中损坏的风险。块组并不是完全孤立的。超级块和块组描述符表会在多个块组中保存备份从而在主要元数据区域损坏时仍然可以从备份恢复。备份采用稀疏策略而不是每个块组都存一份这是为了平衡容灾能力和空间开销。块组的具体布局将在第四章展开。三、Ext2 的历史背景与设计目标Ext2全称 Second Extended Filesystem由 Rémy Card 于 1993 年设计并实现。它取代了早期的 Ext 文件系统成为 Linux 在 20 世纪 90 年代中后期事实上的标准文件系统。在 Ext2 之前Linux 使用的 Minix 文件系统存在严重的限制例如文件名长度受限、分区大小受限、时间戳表达不足等。Ext 文件系统做了一些改进但很快暴露出性能和不稳定问题。Ext2 则在设计上向前迈出了一大步它吸收了当时 Berkeley Fast File System 的许多思想例如块组和位图的组织方式并将其改造成适合 Linux 内核的形态。Ext2 的设计目标可以概括为几个关键词稳定、可扩展、性能可接受、实现简单。稳定可靠文件系统是数据的基础设施任何不稳定都可能直接损害用户数据因此可靠性是第一优先级。可扩展磁盘格式需要为未来可能的增强预留空间。Ext2 的 inode 和超级块中保留了一些未使用字段后来被 Ext3、Ext4 用来实现日志、扩展属性、纳秒时间戳等功能。性能可接受通过块组、就地分配等机制降低碎片和寻道开销使得常见负载下表现良好。实现简单相比同时代的一些复杂文件系统Ext2 的磁盘布局和内核实现相对容易理解降低了开发和维护成本。Ext2 最大的特征是「没有日志」。在 Ext2 中元数据修改直接写到最终位置没有额外的日志区。这个设计在实现上最简单也没有日志写放大但代价是崩溃恢复能力很弱。一次典型的删除操作可能需要修改目录数据块、块位图和 inode 位图等多个位置。如果系统在其中某一步断电文件系统就可能处于不一致状态。此时唯一可靠的手段是运行 fsck对全盘进行一致性检查并尝试修复。对于 20 世纪 90 年代的小磁盘来说全盘 fsck 也许只需要几秒到几分钟但随着磁盘容量增长fsck 的时间可能膨胀到十几分钟甚至更久这对服务器可用性来说是难以接受的。因此Ext2 的局限并非一开始就是灾难而是随着硬件发展逐步暴露出来的。磁盘越来越大重启后等待 fsck 的时间越来越长用户对高可用性的要求也越来越高。这种压力最终催生了 Ext3。四、Ext2 磁盘布局全景理解 Ext2首先要理解它的磁盘整体布局。Ext2 将整个分区视为由若干个块组首尾相接组成。绝大多数的管理结构都发生在块组内部只有少数全局信息如超级块和块组描述符表会跨块组冗余备份。一个典型的块组内部布局顺序如下超级块记录文件系统的全局元信息块组描述符表记录所有块组的概要信息块位图标记本块组内数据块的占用情况inode 位图标记本块组内 inode 的占用情况inode 表本块组内所有 inode 的数组数据块区真正存放文件内容和目录内容的空间。需要特别说明的是超级块和块组描述符表并不在每个块组中都完整出现。为了节省空间它们采用稀疏备份策略只出现在块组 0、1 以及符合特定幂次方规则的块组中。具体哪些块组有备份可以通过 dumpe2fs 工具查看。下面逐个拆解这些结构的含义。4.1 超级块文件系统的总控信息超级块是文件系统最重要的元数据结构。内核挂载文件系统时首先读取超级块确定块大小、inode 数量、块组数量、特性标志等关键参数然后才能继续解析其他结构。如果超级块损坏文件系统就很难正常挂载。Ext2 超级块保存的主要信息包括inode 总数和块总数空闲 inode 数和空闲块数每个块组的块数和 inode 数块大小的对数值第一个数据块号挂载计数和最大挂载次数挂载状态标记文件系统当前是干净还是脏上次检查时间和检查间隔特性标志包括兼容特性、不兼容特性和只读兼容特性卷名、最后挂载点和 UUID错误处理策略等。挂载计数是 Ext2 中一个很有代表性的设计。它配合最大挂载次数一起工作当文件系统被正常挂载一定次数后即使没有检测到错误系统也会在下一次挂载前触发一次 fsck。这相当于给无日志的 Ext2 增加了一层定期体检机制。超级块中的挂载状态字段则用于快速判断上次是否干净卸载。正常卸载时内核会把状态标记为干净如果系统崩溃状态会残留为脏下次挂载时系统就会知道需要先做一致性检查。超级块中的特性标志也很重要。Ext 系列从一开始就设计了特性协商机制。一部分特性是兼容的旧内核不认识也可以继续安全读写一部分特性是不兼容的旧内核如果遇到就必须拒绝挂载还有一部分是只读兼容的旧内核可以只读挂载但不能写入。正是这些特性位为后来 Ext3 在不改变磁盘结构的前提下增加日志提供了可能。4.2 块组描述符表块组描述符表紧跟在超级块之后是每个块组一条记录组成的数组。每条块组描述符记录该块组的关键信息主要包括块位图所在块号inode 位图所在块号inode 表起始块号本块组空闲块数本块组空闲 inode 数本块组已用目录数。有了块组描述符表内核就能快速定位任意块组的位图和 inode 表而不必从头扫描。块组描述符表与超级块一起在多个块组中备份以提升元数据损坏时的恢复能力。4.3 块位图与 inode 位图位图是 Ext2 管理空闲资源的核心结构。每个块组有两张位图块位图和 inode 位图。块位图中的每一位对应本组中的一个数据块位值为 1 表示已占用0 表示空闲。inode 位图则对应 inode 表中的每一个 inode语义相同。位图结构有几个明显优点。首先是空间效率高每个块或 inode 只占一个比特其次是查询和修改都很直接通过位运算即可完成最后是局部性好每个块组单独管理自己的位图分配操作分散进行。位图也有脆弱的一面。如果位图损坏内核可能把已经占用的块再次分配出去造成数据覆盖或者把空闲块错误地认为已占用虽然不丢数据但会浪费空间。fsck 的一项重要工作就是通过遍历 inode 和目录重新推导出哪些块和 inode 应该被占用从而重建位图。因此只要 inode 和目录结构还基本可读位图损坏通常是可以修复的。在分配策略上Ext2 倾向于局部化。创建普通文件时内核会优先在当前目录所在块组中寻找空闲 inode如果该组没有空闲 inode再寻找相邻的、有空闲 inode 的块组。文件数据块也尽量与 inode 分配在同一块组以减少寻道距离。4.4 inode 表与 inode 结构inode 表是 inode 结构体的数组。每个块组内部有一个 inode 表inode 的个数在文件系统创建时就已经固定运行期间不能动态调整。这一点非常重要因为它意味着 Ext2/Ext3 存在 inode 耗尽的风险即使磁盘上还有大量空闲数据块只要 inode 已经全部用尽就无法再创建任何新文件或目录。Ext2 的 inode 结构中的主要字段如下模式文件类型与权限位区分普通文件、目录、符号链接、字符设备、块设备、FIFO、套接字等uid 与 gid文件属主和属组大小对于普通文件表示字节数对于设备文件有特殊含义时间戳访问时间 atime、变更时间 ctime、修改时间 mtime以及删除时间 dtime链接计数有多少个目录项指向该 inode块计数该文件实际占用的数据块数量标志位例如不可变、同步更新、追加写等15 个数据块指针12 个直接块指针、1 个一级间接块指针、1 个二级间接块指针、1 个三级间接块指针版本、ACL、扩展属性等字段为后续扩展预留。inode 中那些为将来扩展保留的字段后来在 Ext3 和 Ext4 中发挥了重要作用。例如Ext3 的日志 inode 正是复用了已有的 inode 和数据块机制Ext4 则利用 inode 中预留的空间实现了纳秒时间戳、扩展属性和 extent 树。这正是 Ext2 当初「可扩展」设计目标的回报。4.5 目录项结构目录数据块中存放一条条目录项。经典 Ext2 目录项包含 inode 号、记录长度、文件名长度和文件名。后来通过文件类型特性扩展在记录中增加了文件类型字段使按名查找时不必再读取目标 inode 就能判断类型显著提升了目录遍历效率。目录项采用变长记录。记录长度会向上对齐到 4 字节。删除文件时系统将被删目录项的空间合并到前一个目录项从而留下可用空洞。这种设计让删除操作非常快不需要移动后续记录同时也为后续创建提供了空间复用还为数据恢复留下了可能。目录项中保存的 inode 号并不会被实时验证。如果底层 inode 已经损坏目录项和 inode 之间就会出现悬空引用。fsck 的重要工作之一就是检查目录项引用的 inode 是否有效并修复或移除这些不一致项。五、Ext2 的数据寻址直接块与多级间接块文件系统最核心的任务之一是把「文件内的第 N 字节」映射到「磁盘上的第 M 块」。这种映射结构需要在空间效率和访问效率之间做平衡。如果为每个文件都预留大量直接块指针小文件会浪费 inode 空间如果只给少量指针又无法支持大文件。Ext2 的答案是经典的「直接块 多级间接块」组合。Ext2 inode 中有 15 个数据块指针。它们被分成四组前 12 个是直接块指针第 13 个是一级间接块指针第 14 个是二级间接块指针第 15 个是三级间接块指针。5.1 直接块前 12 个指针直接指向文件数据块。也就是说文件的前 12 个块可以完全直接寻址不需要任何额外的间接块。以 4KB 块大小计算文件前 48KB 的内容都可以通过直接块访问效率最高。对于系统中的大量小文件来说12 个直接块通常已经足够覆盖全部内容。许多日志文件、配置文件、源码文件的体积都在几十 KB 以内它们根本用不到间接块。这正是这个设计对常见场景的体贴之处。5.2 一级间接块inode 的第 13 个指针指向一个一级间接块。一级间接块本身是一个数据块但它内部存放的不是文件内容而是一组块号每个块号再指向一个真正保存文件数据的数据块。以 4KB 块大小和 4 字节的块号计算一个一级间接块可以存放 1024 个块号因此可以额外寻址 1024 个数据块约 4MB。加上 12 个直接块的 48KB此时文件可达约 4MB。5.3 二级间接块inode 的第 14 个指针指向二级间接块。二级间接块内部存放的是一组一级间接块的块号每个一级间接块又存放 1024 个数据块号。因此二级间接块可以寻址 1024 乘以 1024即 1048576 个数据块约 4GB。5.4 三级间接块inode 的第 15 个指针指向三级间接块。三级间接块指向二级间接块二级间接块指向一级间接块一级间接块再指向数据块。三级间接块最多可以寻址 1024 的三次方个数据块约 4TB。把四层加起来4KB 块大小的 Ext2 单个文件理论上限在 4TB 左右。但要注意实际限制还受到块号位宽的影响。早期 Ext2 使用 32 位块号因此文件系统总块数被限制在 2 的 32 次方以内。不同块大小下的理论限制大致如下块大小单文件最大大小文件系统最大容量1KB约 16GB约 4TB2KB约 256GB约 8TB4KB约 2TB 到 4TB约 16TB多级间接块方案有一个优雅之处小文件几乎不承担额外开销大文件则通过层级扩展获得巨大的容量。但它也有明显缺点。访问文件深处时需要先读取间接块可能造成多次额外 IO。三级间接块路径在最坏情况下为了访问一个数据块最多要先读三级、二级、一级三个间接块。正是这个缺点促使 Ext4 后来引入 extent 树用连续区间描述替代逐块指针大幅提升大文件的寻址效率。5.5 稀疏文件与逻辑大小Ext2/Ext3 支持稀疏文件。如果程序通过 lseek 跳到文件很靠后的位置再写入中间的空白部分不会被真正分配数据块。此时文件逻辑大小很大但实际占用的磁盘块很少。inode 中的大小字段记录的是逻辑大小块计数字段记录的是实际分配的数据块数。通过ls -l和du可以看到这种差异前者显示逻辑大小后者显示实际磁盘占用。稀疏文件对虚拟机镜像、数据库预分配、备份场景很有价值但也可能造成「逻辑空间充足、物理空间不足」的误判。六、Ext2 的挂载过程与一致性检查挂载 Ext2 文件系统的过程本质上是把磁盘上的静态结构逐步读入内存构建内核可操作的 inode 缓存和目录缓存。内核首先读取超级块校验魔数 0xEF53 和特性标志确认可以安全挂载然后读取块组描述符表定位根 inode并逐步完成根目录的挂接。此后用户的路径访问被逐级转换为 inode 查找和目录项扫描。由于 Ext2 没有日志元数据的修改是就地更新的。为了理解这一点可以设想一个删除文件的典型过程。删除文件需要同时更新多处元数据从目录中移除目录项、更新块位图、更新 inode 位图可能还要更新 inode 本身。如果系统在更新了目录项和块位图之后、更新 inode 位图之前断电重启后文件系统就处于账目不一致的状态。此时必须依赖 fsck 进行全盘检查。fsck 的工作包括检查超级块是否一致遍历所有 inode核对链接计数是否与目录项匹配遍历所有目录项检查 inode 号是否有效根据 inode 和目录项的实际使用情况重建块位图和 inode 位图发现孤儿文件将其放入 lostfound 目录。fsck 是 Ext2 的安全网但代价高昂。它的耗时与文件数量、磁盘大小和检查深度成正比。检查期间文件系统必须卸载或只读挂载这对需要持续在线的服务器来说是很大负担。正是在这种背景下日志机制成为刚需。Ext2 还有其他一些局限inode 数量固定格式化后不能在线调整块号为 32 位大容量支持有限目录采用线性查找大目录性能不良时间戳精度为秒级不够精细。这些问题中的一部分在 Ext3 中得到缓解其余则留给了 Ext4。七、Ext3 的历史背景与设计哲学Ext3 由 Stephen Tweedie 于 2001 年合入 Linux 内核。它的设计哲学可以用一句话概括在不改变 Ext2 磁盘结构的前提下加上一层日志。这个选择非常务实也带来了巨大的工程收益。首先Ext3 完全向后兼容 Ext2。一个 Ext2 文件系统可以原地升级为 Ext3数据不需要备份、重新格式化再恢复同样Ext3 也可以降级回 Ext2。已有的 Ext2 工具、驱动和用户环境几乎不需要改造。其次实现风险低。Ext3 不必重新发明块分配、目录管理、间接寻址等底层机制而是复用已经成熟的代码只增加日志层。最后迁移成本极低。用户只需要运行一条命令添加日志就能获得崩溃恢复能力。正是由于这些优点Ext3 在推出后迅速成为各大 Linux 发行版的默认文件系统并在很长一段时间内统治了 Linux 存储生态。即使在今天Ext3 的很多设计思想仍然活在 Ext4 中。Ext3 的核心改进集中在三个方面增加日志提供三种可选的日志模式支持在线扩容保留并强化了若干特性位定义。其中日志机制是理解 Ext3 的重中之重。八、Ext3 的日志机制原理与结构日志机制解决的核心问题是如何让一个涉及多步骤、多位置的元数据更新具备原子性。没有日志时删除文件需要修改目录项、块位图、inode 位图等多个位置任何一步中断都可能留下半成品。有日志之后做法发生了根本变化先把「即将要做的修改」写入日志区域写完后写入提交标记再把修改实际应用到文件系统的最终位置全部应用完成后从日志中清除该事务。如果系统在第一步之前崩溃说明变更从未被提交文件系统维持原状。如果系统在第二步中崩溃恢复时只需要回放日志把已经提交但尚未应用到最终位置的事务补完。因为日志是顺序写入且带提交标记恢复过程非常快通常只需几秒到几十秒而不必做全盘 fsck。日志机制的本质是把多个随机写转化为「先做一段顺序写再加承诺然后再做多个随机写」。顺序写天然具有性能优势并且崩溃后重放起来非常方便。这一思想与数据库的 WAL 机制高度一致。8.1 日志的磁盘结构Ext3 在文件系统中划出一个特殊的 inode通常是 inode 8作为日志 inode。日志数据就保存在这个 inode 指向的数据块中。这个设计很巧妙日志不需要单独的分区或保留区域也不需要改变块组布局只需要在超级块中记录日志 inode 号即可。日志区域内部是一组连续的日志块被组织成逻辑上的环形缓冲。Ext3 的日志主要由三种记录组成描述块说明本事务包含哪些块以及这些块各自对应文件系统中的什么位置数据块实际需要被写回文件系统最终位置的块内容提交块标记本事务完整提交恢复时以它为准。每个事务有一个递增的序列号。描述块中记录事务 ID 和块映射信息。恢复时系统按序扫描日志回放那些已经提交但尚未写回最终位置的事务。8.2 事务的三个阶段一个 Ext3 日志事务的生命周期可以概括为三个阶段写入阶段把本事务涉及的修改块写入日志区同时更新描述块提交阶段把提交块写入日志表示本事务完整且可以安全回放检查点阶段把日志中的内容写回文件系统的真实位置完成后释放日志块供后续事务复用。检查点策略直接影响性能。如果每笔事务都立即检查点日志的优势会被削弱如果检查点太懒散日志区可能被写满导致文件系统不得不暂停新的修改来等待回放。内核通过后台线程和水位控制来平衡这两者。8.3 日志大小与回收日志区大小通常为几 MB 到数百 MB 不等具体取决于文件系统大小和格式化参数。当日志区接近写满时内核会启动检查点将已经提交的事务写回最终位置并释放空间。如果日志区太小大事务可能无法整体放入内核会拆分事务。日志区写满的极端表现是文件系统出现短暂停顿。这是 Ext3 在高强度随机写负载下偶尔会遇到的性能问题。通过增大日志、减少同步写压力或调整提交周期可以得到缓解。九、三种日志模式journal、ordered、writebackExt3 最常被讨论的特性之一是它提供了三种日志模式。三种模式的本质区别在于日志中记录什么以及数据块何时写回最终位置。9.1 journal 模式在 journal 模式下数据和元数据都先写入日志。一个事务会包含文件的数据块、相关 inode、位图和目录项等所有变更。提交后数据和元数据再一起写回最终位置。这种模式的安全性最高文件内容和元数据都是先记录、后应用崩溃后回放日志可以恢复完整的一致性几乎不会出现元数据指向了但数据没到的情形。但它的代价也很高所有数据都要写两遍先写日志再写最终位置写放大最严重性能最低。journal 模式适合对数据完整性要求极高、又能接受性能下降的场景。不过要强调journal 模式保证的是文件系统级一致性并不等同于数据库的事务语义。应用层仍然需要 fsync 来确保数据真正落盘。9.2 ordered 模式ordered 模式是 Ext3 的默认模式也是一个精巧的折中日志只记录元数据但数据块必须在对应元数据提交之前先写回最终位置。也就是说在提交元数据事务前内核先把该事务关联的所有数据块刷盘然后再提交元数据日志。这种「先数据、后元数据」的顺序保证了即使崩溃并回放日志元数据也绝不会指向从未到达磁盘的数据。文件内容要么是修改前的完整旧内容要么是修改后的完整新内容不会出现半新半旧、元数据指向越界数据的情况。ordered 模式在安全性和性能之间取得了很好的平衡因此长期作为 Ext3 的默认模式。对于绝大多数桌面用户和常规服务器负载这个模式已经足够安全。9.3 writeback 模式writeback 模式与 ordered 类似日志也只记录元数据但它不保证数据块先于元数据提交写回。数据和元数据的写回顺序完全交给内核的延迟写机制。可能元数据先落盘而数据尚未落盘。这种模式性能最好因为顺序约束最少能最大限度利用写合并和缓存。但安全性最弱崩溃后可能出现元数据已经提交、数据还是旧的或空白的状况。历史上曾出现过崩溃后原本属于数据文件的块被错误标记为空闲的安全隐患。writeback 模式更适合对性能极其敏感、且数据可以从其他来源重新生成的负载。9.4 三种模式的对比属性journalorderedwriteback日志内容数据加元数据仅元数据仅元数据数据写回顺序先日志后最终位置先数据后元数据提交无顺序保证写放大高中中崩溃后内容一致性最高高较弱性能最慢均衡最快需要反复强调的一点是日志保证的是元数据和文件系统结构的一致性并不等同于应用数据持久化。即使使用最严格的 journal 模式如果应用没有调用 fsync数据仍可能只存在于页面缓存中断电即失。很多开发者误以为文件系统有日志就能替代应用的持久化保障这是一个常见误区。十、Ext3 的兼容性与升级降级Ext3 能从 Ext2 平滑升级关键在于超级块中特性标志的巧妙使用。Ext2 的超级块从一开始就定义了兼容特性、不兼容特性和只读兼容特性三组标志。Ext3 在此基础上新增了 has_journal 这个不兼容特性。当旧版 Ext2 驱动看到设置了 has_journal 的文件系统时因为不认识这个不兼容标志会拒绝挂载从而避免误写坏日志。当新版 Ext3 驱动看到没有该标志的文件系统时会把它当作纯 Ext2 正常挂载。升级操作的本质就是在文件系统中创建日志 inode并把 has_journal 标志写入超级块。日志 inode 则复用 Ext2 已有的 inode 和块指针机制来保存日志内容。因此从 Ext2 升级到 Ext3本质上是「加一个日志文件加翻转一个特性位」。数据全部保留不需要重新格式化。反过来降级也很简单卸载文件系统清空日志关闭 has_journal 标志。降级后文件系统回到与 Ext2 等价的状态但同时也失去了崩溃恢复能力。这种兼容设计使同一个文件系统可以在不同版本内核之间以 Ext2 或 Ext3 两种身份被挂载只要日志状态处理正确。这种弹性是 Ext3 设计最成功的地方之一。十一、Ext3 的在线扩容与 ORlov 分配策略除了日志Ext3 还引入了在线扩容能力。在 Ext2 时代扩展文件系统需要卸载后用工具离线调整业务中断不可避免。Ext3 配合 resize2fs 等工具可以在文件系统保持挂载、继续提供读写服务的同时扩大容量。在线扩容的大致流程是先扩展底层块设备再让内核感知新增的块组逐个初始化新块组的超级块备份、块组描述符、位图和 inode 表并把新块组加入分配池。整个过程对上层应用透明。需要澄清的是早期 Ext3 的在线扩容主要支持扩大而且通常要求底层有可扩展的卷管理支持例如 LVM。缩小文件系统要复杂得多往往仍需要离线操作。Ext3 还改进了 inode 分配策略引入了 ORlov 分配器。传统做法是尽量把同一目录下的文件分配在相近位置。ORlov 则更进一步它以目录树为单位将不同的直属子目录分散到不同块组使得创建大量子目录时 inode 不会都挤在一个块组。这样能减少碎片提升并发创建性能和后续读取局部性对大容量文件服务器尤其重要。十二、Ext2 与 Ext3 的深度对比把 Ext2 和 Ext3 放在一起对比有助于从宏观上把握二者的关系。12.1 结构和兼容性Ext2 和 Ext3 的文件系统本体磁盘布局几乎一致块组、超级块、块位图、inode 位图、inode 表、目录项和数据块寻址方式完全相同。差别仅在于 Ext3 多了一个日志 inode 和对应的超级块特性标志。因此两者可以双向转换工具生态高度复用。12.2 崩溃恢复Ext2 崩溃后需要全盘 fsck恢复时间与文件系统规模成正比。Ext3 崩溃后只需回放日志恢复时间主要与日志大小和待回放事务数量有关通常在秒级到分钟级。对服务器来说这意味着宕机后的服务恢复窗口大幅缩短。12.3 性能特征Ext2 没有日志开销对于写一次、读多次、断电损失可接受的场景可能比 Ext3 更快。Ext3 引入了日志写放大但通过批量提交、写合并等机制维持了可接受的性能。12.4 数据安全如果都正常关机两者对已成功写回的数据没有本质差别。差别体现在异常掉电时Ext3 的日志能保证元数据一致性并降低文件内容损坏风险尤其是 ordered 模式可以避免新文件内容为空或旧内容被误标为新。Ext2 则完全依赖事后 fsck风险更高。12.5 适用场景Ext2启动分区、只读系统映像、容器只读层、不希望有日志写放大的一次性数据盘等Ext3传统服务器数据盘、个人工作站、需要断电恢复能力但又不愿切换到新文件系统的场景。十三、实战操作创建、查看与升级降级本节通过实际操作演示 Ext2 和 Ext3 的常用命令。以下示例均基于 Linux 环境。请勿直接照搬到生产系统操作前务必确认目标设备并做好备份。13.1 创建 Ext2 文件系统使用 mke2fs 创建 Ext2 文件系统# 在 /dev/sdb1 上创建 Ext2 文件系统 mke2fs -t ext2 /dev/sdb1 指定块大小为 4096 字节 mke2fs -t ext2 -b 4096 /dev/sdb1 指定 inode 大小为 256 字节 mke2fs -t ext2 -I 256 /dev/sdb1 创建时指定卷标 mke2fs -t ext2 -L mydata /dev/sdb1注意mke2fs 默认创建的是 Ext2 还是 Ext3/Ext4取决于系统和配置文件。显式使用-t ext2是最可靠的方式。13.2 创建 Ext3 文件系统# 创建 Ext3 文件系统 mkfs.ext3 /dev/sdb1 显式指定日志大小为 64MB mke2fs -t ext3 -J size64 /dev/sdb1 将日志放在指定设备上生产环境较少使用 mke2fs -t ext3 -J device/dev/sdc1 /dev/sdb1日志大小需要根据写入负载调整。日志过小大事务下容易被写满导致性能抖动日志过大则占用更多空间。13.3 将 Ext2 升级为 Ext3假设 /dev/sdb1 上已有 Ext2 文件系统# 先卸载 umount /dev/sdb1 添加日志并升级为 Ext3 tune2fs -j /dev/sdb1 重新挂载 mount /dev/sdb1 /mnt 验证已识别为 Ext3 mount | grep sdb1tune2fs -j会创建日志 inode 并打开 has_journal 特性位。整个过程虽然需要卸载但不需要重新格式化数据全部保留。13.4 将 Ext3 降级为 Ext2# 卸载并清空日志 umount /dev/sdb1 tune2fs -O ^has_journal /dev/sdb1 建议执行一次 fsck e2fsck -f /dev/sdb1降级后文件系统回到无日志状态灾难恢复保障随之消失因此通常不建议对重要数据做降级。13.5 查看文件系统信息dumpe2fs 是查看 Ext 系列内部信息的利器# 查看超级块和所有块组信息 dumpe2fs /dev/sdb1 只看超级块信息 dumpe2fs -h /dev/sdb1输出中可以看到 inode 总数、块总数、每块组大小、日志 inode 号、特性标志、块大小、挂载状态等信息。下面是一段简化的输出片段Filesystem volume name: mydata Filesystem UUID: b3f0b6a2-4c7b-4b8a-9e72-0f1c6a8112f3 Filesystem magic number: 0xEF53 Filesystem features: has_journal ext_attr resize_inode dir_index filetype Block size: 4096 Inode size: 256 Inode count: 655360 Block count: 2621440其中has_journal表示这是一个 Ext3 文件系统。如果该标志缺失则会被当作 Ext2 处理。13.6 使用 debugfs 查看内部结构debugfs 可以直接读取文件系统内部结构对学习磁盘布局非常有帮助# 以只读模式打开文件系统并查看摘要 debugfs -R stats /dev/sdb1 查看 inode 12 的详细内容 debugfs -R stat 12 /dev/sdb1 查看根目录内容 debugfs -R ls -l / /dev/sdb1 导出 inode 12 的数据块 debugfs -R dump 12 /tmp/inode12.bin /dev/sdb1通过stat命令可以清楚看到 inode 的 15 个数据块指针包括 12 个直接块和 3 个间接块这对理解多级间接寻址很有帮助。13.7 查看和调整日志模式Ext3 的日志模式可以在挂载时指定# ordered 模式默认 mount -t ext3 -o dataordered /dev/sdb1 /mnt journal 模式 mount -t ext3 -o datajournal /dev/sdb1 /mnt writeback 模式 mount -t ext3 -o datawriteback /dev/sdb1 /mnt也可以在 /etc/fstab 中持久化设置/dev/sdb1 /mnt ext3 defaults,dataordered 0 2查看当前实际使用的模式cat /proc/mounts | grep sdb113.8 观察崩溃恢复学习日志机制时可以构造一个小实验在 Ext3 分区挂载状态下模拟异常断电观察重启后是否快速恢复。真实断电在虚拟机外较难复现但可以通过回填缓存、强制断电或使用崩溃注入工具来观察日志回放。对普通学习来说理解「干净卸载时状态正常、未干净卸载时挂载前会尝试日志回放」已经足够。十四、常见问题与故障排查在实际使用 Ext2/Ext3 的过程中有几类问题最为常见。14.1 文件系统被标记为脏重启后自动检查如果系统非正常断电超级块中的挂载状态可能残留为脏。对 Ext3 而言下次挂载通常会先尝试日志回放对 Ext2 则可能触发全盘 fsck耗时较长。遇到这种情况不要中断检查让程序自然完成强行中断可能造成更大损坏。14.2 只读挂载与错误重挂载当内核检测到文件系统损坏或 IO 错误时为了保护数据可能把文件系统强制重挂载为只读。此时系统日志中往往会出现 ext3_read_error、Aborting journal 等关键字。处理步骤通常是停止写入、备份关键数据、卸载、执行 fsck、检查磁盘健康状态。14.3 inode 耗尽inode 耗尽常见于小文件极多的场景例如邮件服务器、容器镜像缓存、代码仓库镜像等。可以用df -i查看 inode 使用情况df -i /dev/sdb1如果磁盘空间充足但 inode 已满新建文件会失败错误通常是 No space left on device。解决办法包括删除无用小文件、用更高 inode 密度的参数重新格式化或者改用支持动态 inode 的文件系统。14.4 位图损坏位图损坏会导致重复分配或错误空闲。fsck 可以通过遍历 inode 和目录重建位图但前提是 inode 和目录基本可用。定期 fsck或依赖挂载计数自动触发检查是早期 Ext2 环境的重要维护手段。14.5 日志写满导致性能抖动Ext3 在高强度随机写场景下如果日志区太小可能出现周期性写入停顿。通过增大日志、调整提交周期或迁移到 Ext4 可以缓解。14.6 被删文件的恢复Ext2/Ext3 删除文件并不会立即清空数据块。使用 debugfs 可以尝试恢复刚删除且尚未被覆盖的文件。基本思路是用lsdel列出已删除 inode再用dump取出数据块。不过一旦文件系统继续写入空闲块可能被复用恢复成功率会迅速下降。数据恢复的黄金原则是发现问题后立即卸载或只读挂载避免任何写入。十五、源码视角内核中的 Ext2 与 Ext3如果想从源码层面验证前文内容可以关注内核源码中的几个关键目录和头文件。以较早期的内核为例fs/ext2/Ext2 的核心实现fs/ext3/Ext3 的实现建立在 ext2 语义之上fs/jbd/通用日志块设备层Ext3 日志依赖它include/linux/ext2_fs.hExt2 磁盘结构定义include/linux/ext3_fs.hExt3 特定结构定义。在这些头文件中可以看到与磁盘布局一一对应的结构体。下面摘录一个用于学习的简化版超级块结构体struct ext2_super_block { __le32 s_inodes_count; /* inode 总数 */ __le32 s_blocks_count; /* 块总数 */ __le32 s_r_blocks_count; /* 保留块数 */ __le32 s_free_blocks_count; /* 空闲块数 */ __le32 s_free_inodes_count; /* 空闲 inode 数 */ __le32 s_first_data_block; /* 第一个数据块号 */ __le32 s_log_block_size; /* 块大小对数值 */ __le32 s_blocks_per_group; /* 每组块数 */ __le32 s_inodes_per_group; /* 每组 inode 数 */ __le32 s_mtime; /* 挂载时间 */ __le32 s_wtime; /* 写时间 */ __le16 s_mnt_count; /* 挂载计数 */ __le16 s_max_mnt_count; /* 最大挂载次数 */ __le16 s_magic; /* 魔数 0xEF53 */ __le16 s_state; /* 挂载状态 */ /* 后续还有卷名、UUID、特性标志等字段 */ };简化版 inode 结构体如下struct ext2_inode { __le16 i_mode; /* 文件类型与权限 */ __le16 i_uid; /* 属主 */ __le32 i_size; /* 文件大小 */ __le32 i_atime; /* 访问时间 */ __le32 i_ctime; /* 变更时间 */ __le32 i_mtime; /* 修改时间 */ __le32 i_dtime; /* 删除时间 */ __le16 i_gid; /* 属组 */ __le16 i_links_count; /* 链接计数 */ __le32 i_blocks; /* 占用块数 */ __le32 i_flags; /* 标志位 */ __le32 i_block[15]; /* 数据块指针 */ /* 后续还有 ACL、扩展属性等字段 */ };看到i_block[15]就能立刻对应到 12 个直接块加 3 个间接块。结构体字段的数量、顺序和磁盘字节布局严格对应这是理解磁盘文件系统与内存结构之间关系的关键。在挂载过程中内核调用链大致是读取超级块、校验魔数与特性、构建内存超级块对象、读取块组描述符、初始化 inode 缓存、完成根目录挂载。对 Ext3 而言中间还会多出日志初始化步骤读取日志 inode、检查日志头部、必要时恢复日志。对想实践内核调试的读者建议使用调试输出观察挂载流程或通过 debugfs 手动构造损坏的日志观察内核恢复过程。十六、从 Ext3 到 Ext4承上启下的演进虽然本文的主角是 Ext2 和 Ext3但了解 Ext4 的改进有助于理解 Ext3 的边界在哪里。Ext4 的直接前身是 Ext3它在保留日志机制和兼容性的同时解决了 Ext3 的多个核心局限。extent 树替代间接块用「起始块加长度」的区间描述代替逐块指针大幅减少大文件的寻址元数据和 IO 次数延迟分配数据块在写回时才真正分配延长了块分配决策的观察窗口使分配更连续更大的容量支持采用 48 位块号突破 32 位限制支持更大的文件系统与单个文件纳秒时间戳和更多扩展属性提高时间精度和元数据表达能力日志校验和与快速 fsck进一步提升可靠性和恢复体验在线碎片整理与多块分配改善长期运行后的性能。但即便到了 Ext4 时代Ext2/Ext3 奠定的很多机制仍然清晰可见超级块框架、块组与位图、inode 表、兼容特性标志以及日志思想。学习 Ext2/Ext3本质上是在学习 Ext 系列的根。理解这些基础之后再读 Ext4 的 extent、延迟分配和新特性会顺理成章。十七、设计思想总结与延伸思考如果用几句话总结 Ext2 与 Ext3 留给后来的启示可以归纳为以下几点清晰的磁盘格式是文件系统的基石。Ext2 用结构体直接映射磁盘布局虽然在大规模演进时略显僵硬但换来了极高的可理解性和跨工具互操作性。块组设计兼顾局部性和冗余。把元数据与数据捆绑在块组内同时稀疏备份关键元数据是经典且有效的工程折中。日志是崩溃一致性的通用解药。Ext3 证明了在不推翻旧结构的前提下加入日志就能把恢复时间从小时级降到秒级。兼容性可以成为大规模迁移的桥梁。就地升级、可降级的设计极大降低了用户采用门槛。理解安全边界比追求极致更重要。三种日志模式的差异说明文件系统设计永远要在安全与性能之间妥协真正的数据安全还依赖应用层 fsync 和运维策略。跳出 Ext 系列这些思想同样反映在 XFS 的日志、Btrfs 的写时复制与元数据校验、ZFS 的事务组等现代文件系统中。学习 Ext2/Ext3 不仅能理解历史也能为理解更复杂的存储设计打下基础。十八、全文总结Ext2 是 UNIX/Linux 文件系统设计的一座里程碑。它以超级块、块组、inode、目录项和位图为骨架用 12 个直接块加三级间接块完成数据寻址结构清晰、实现稳健。它的最大短板是缺少日志崩溃后依赖全盘 fsck。Ext3 在 Ext2 的磁盘结构之上叠加了日志层通过日志 inode、事务提交与回放把异常断电后的恢复时间大幅缩短。它提供 journal、ordered、writeback 三种模式在数据安全与性能之间提供选择。因为完全兼容 Ext2 格式它实现了平滑升级与降级成为 Linux 历史上影响最为深远的文件系统之一。理解 Ext2 和 Ext3就是理解一套完整文件系统的骨架、寻址方式、空间管理、目录组织、崩溃恢复与兼容性演进。无论未来是从事内核开发、存储系统设计、运维还是数据恢复这份知识都是扎实的地基。最后建议有兴趣深入研究的读者亲手创建一个小型 Ext2 与 Ext3 分区使用 dumpe2fs、debugfs、tune2fs 逐项查看超级块、块组、inode 和日志结构并在安全环境下模拟断电与恢复过程。只有把理论落到工具的每一个输出上对文件系统的理解才会真正变得牢固。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进