资讯详情

深入理解 ext4 文件系统:从 extent 到日志模式的底层原理

📅 2026/10/10 22:38:03 | 华诺云谱 👁 阅读
深入理解 ext4 文件系统:从 extent 到日志模式的底层原理
这是文件系统系列的第 6 篇。前面几篇我们把 VFS、挂载、块设备层这些外围机制大致过了一遍今天终于要正面硬刚 Linux 上最常见、也最能打的文件系统ext4。很多人每天都在mkfs.ext4、mount、df -h但真被问到底层原理时能讲清楚 extent 和 ext3 间接块区别、延迟分配为什么能提速、journal 到底在保护什么的人其实不多。这篇我用“运维 内核源码视角”的方式把 ext4 的工作原理完整捋一遍。不管你是写应用、搞嵌入式、调内核还是准备面试这篇都能给你一套能落地的认知框架。1. 先认清文件系统的位置VFS 与挂载机制1.1 一次 write 调用数据到底经过了谁很多初学者有个误区觉得write()就是把数据“写进硬盘”。严格说write()只是把数据从用户态缓冲区拷贝到内核的 page cache 里真正的落盘动作发生在后台的 writeback 机制中。流程大致是ssize_t write(int fd, const void *buf, size_t count);这个系统调用进来后VFS 会根据文件描述符找到对应的struct file再通过file-f_op-write_iter进入具体文件系统。对 ext4 来说调用链会走ext4_file_write_iter最终把数据写到页缓存中并标记为脏页。脏页什么时候回写由内核的 writeback 线程根据水位和超时机制决定不是应用写一次就立刻落盘。这套异步模型是理解 ext4 一切行为的地基。比如你问“为什么我cp大文件然后马上断电文件大小是对的但内容却是旧数据”答案就在这里大小变更属于元数据可能已经被 journal 保护数据内容是否落盘取决于 writeback 的时间点。这也是为什么数据库这类应用必须自己fsync否则操作系统不会替你保证持久性。1.2 挂载流程ext4 是怎么“接管”一个分区的执行mount -t ext4 /dev/sdb1 /mnt时发生的事情远比想象中多。VFS 首先分配一个空的super_block对象然后调 ext4 的ext4_fill_super去读取磁盘上的超级块校验魔数、特性标志、UUID 等关键信息再把块大小、inode 数量、日志信息载入内存。整个过程在 dmesg 里可以看到类似EXT4-fs (sdb1): mounted filesystem with ordered data mode. Opts: (null)这条日志里的ordered data mode是日志模式后面专门讲。挂载成功后VFS 通过 root inode 找到/目录后续所有路径查询都从它开始。启动阶段也一样只不过顺序更拧巴内核先挂载一个内存中的 rootfs加载 initramfs再由 initramfs 里的脚本把真正的根文件系统挂载到/root最后switch_root切换过去。嵌入式开发里很多人喜欢在调试阶段用 NFS 挂载根文件系统方便改动后立即生效生产环境再换回 ext4。这种开发模式本身没问题但你要清楚NFS 挂载的语义和本地 ext4 不一样比如sync的作用范围、锁的语义、掉电恢复能力都不能直接套用本地文件系统的经验。2. 物理布局块组、位图与 flex_bg2.1 整个磁盘的“街区规划”ext4 会把分区划分成很多个“块组”每个块组负责管理一部分连续的数据块。这样做的好处很朴素元数据离数据近文件在组内创建时inode 和数据块往往都在同一个组磁盘寻道时间短。每个块组内部大致是这样的结构块 0引导扇区可用于存放引导程序普通数据分区通常为空超级块和组描述符记录整个文件系统的全局信息以及每组的基础元数据块位图用一位bit表示一个块是否被占用inode 位图用一位表示一个 inode 是否被占用inode 表存放该组所有 inode 的实际结构数据块区真正保存文件内容、目录项、扩展属性等数据的地方超级块不是只存一份而是在部分块组里有备份。原因很直白如果主超级块损坏文件系统就直接废了。备份超级块的存放位置有固定规律后续排查章节我们会用到这里先知道有这个设计。块位图和 inode 位图相当于“房产登记册”创建文件时先在 inode 位图里找空闲 inode再在块位图里找空闲数据块。两本册子都必须在修改数据前更新否则断电后会出现“文件占用了别人数据块”的严重问题。所以大家在日志里常看到的“bitmap mismatch”等报错本质就是这两本册子和实际占用对不上了。2.2 flex_bg把块组“捆”起来管理ext3 时代每个块组单独存放位图和 inode 表创建大量文件时要来回读取不同组里的元数据磁头到处乱跑。ext4 引入 flex_bg 特性把相邻若干个块组通常是 16 个合并成一个“弹性块组”这些组的块位图、inode 位图、inode 表被集中放在这个弹性块组起始附近的一块连续区域里。这样做的好处非常大inode 表连续元数据读取有更好的局部性后续分配时也能更从容地找到连续数据块。实践里用 ext4 格式化一个分区后tune2fs -l里如果看到flex_bg特性说明已经启用了。这也是 ext4 和 ext3 在面对“上百万小文件”这类场景时性能差别明显的重要来源之一。2.3 块大小选择4K 不是唯一答案mkfs.ext4默认块大小是 4096 字节但可以用-b 1024、-b 2048调整。块越小小文件浪费的空间越少但单 block 能管理的位图范围有限大分区会需要更多块组元数据总量变大。块越大大文件连续读写的吞吐越高但小文件最后一截按块对齐浪费也越明显。举个例子一个文件只有 100 字节用 4K 块意味着还是要占一个完整的 4096 字节块剩余空间直接浪费。如果是几百万个小文件浪费加起来非常可观。反过来一个 2GB 的视频文件用 1K 块会让数据块数量翻倍日志和 extent 数量都会增加读写效率反而不如 4K。我的建议是没有特殊需求就保持 4K它和 CPU 页缓存、磁盘扇区之间配合得最好嵌入式场景如果 flash 空间极紧并且文件普遍很小可以考虑 1K 或 2K但要接受元数据复杂度上升。3. 核心寻址inode、extent 与内联数据3.1 inode 到底存了什么inode 是文件系统的核心结构你可以把它理解成“档案袋”。里面不存文件名而是存权限、属主、大小、时间戳、数据块地址等信息。执行stat可以看到这些信息$ stat app.log File: app.log Size: 1024 Blocks: 8 IO Block: 4096 regular file Device: fd01h/64769d Inode: 265842 Links: 1 Access: (0644/-rw-r--r--) Uid: ( 1000/ user) Gid: ( 1000/ user) Access: 2024-05-20 10:00:00.000000000 0800 Modify: 2024-05-20 10:05:00.000000000 0800 Change: 2024-05-20 10:05:00.000000000 0800 Birth: 2024-05-20 09:59:00.000000000 0800注意这里的Links它表示硬链接数。每次创建一个硬链接Links就加一删除一个硬链接就减一。只有当链接数降到 0inode 才会被真正释放。所以mv文件名时 inode 号不变因为只改了目录项cp是创建新 inode所以 inode 号一定会变。这个细节在排查“为什么磁盘空间满了但找不到大文件”时特别重要后面会再提。老读者可能会问那符号链接呢符号链接是一种特殊文件它保存的是目标路径字符串。如果目标路径很短比如 60 字节以内这个字符串可以直接存在 inode 的数据块指针区域里不需要额外分配一个数据块。这是 ext4 里一个容易忽略的省空间设计。3.2 extent 树从“指针数组”到“区间描述”ext3 时代用间接块寻址inode 里放 12 个直接块指针不够就指向一个间接块间接块里再存更多指针还不够就是双重间接、三重间接。这种方案有几个缺点大文件寻址层级多读完一个间接块才能拿到下一批指针文件碎片化时指针数量爆炸写日志时修改这些间接块本身的频率也高。ext4 改用 extent 树。所谓 extent就是“一段连续的物理块区间”。一个 extent 用三个关键字段描述起始逻辑块、长度、起始物理块。比如一个文件从逻辑块 1000 开始连续占据 32768 个块在 inode 里只需要一条 extent 记录而不是 32768 个指针。extent 树的结构可以简单理解成这样inode 的 i_block 区域保存 extent 树根 ├── 如果只有少量 extent树根直接就是叶子 └── 如果 extent 很多树根变成索引节点 ├── 指向下一级索引 └── 最终指向叶子 extent每个叶子 extents 能描述的最大范围是 32768 个块乘以 4K 块大小就是 128MB。所以一个 2GB 文件理论上至少需要 16 条 extent 记录。实际上文件连续程度好的话extent 数量极少这也是 ext4 顺序读写性能出色的原因之一。用一个实际命令看最直观$ filefrag -v large.bin Filesystem type is: ef53 File size of large.bin is 1073741824 (262144 blocks of 4096 bytes) ext: logical_offset: physical_offset: length: expected: 0: 0.. 32767: 345678.. 378445: 32768: 1: 32768.. 65535: 378446.. 411213: 32768:如果输出的 extent 数量很少且长度都接近 32768说明文件非常连续如果出现一堆长度为 8、16 的小 extent说明文件碎片化严重。这个命令我在所有排查性能问题的场合都会先跑一遍。3.3 小文件藏哪里内联数据ext4 还有一个被很多人忽略的功能inline_data。这个特性允许小文件内容直接放进 inode 结构里最多能存大约一百多字节硬件上不额外分配数据块。对小文件极多的目录比如/etc下面一堆几十字节的配置文件能省下大量块。嵌入式系统上这个特性很受欢迎因为 flash 的块本来就珍贵。要确认有没有开启可以看 mount options 或tune2fs -l里的 features。老系统上如果没开启用tune2fs -O inline_data可以加但要注意部分旧内核和旧 e2fsprogs 不识别该特性存在兼容风险。生产环境别乱加先在测试机上验证。4. 日志子系统崩溃恢复的看门人4.1 为什么不能只靠“写完再插拔”磁盘写入不是原子的。一个 4K 的块在硬件层面可能被拆成多次扇区写入即使一个扇区也可能因为掉电出现“半个扇区”的情况。文件系统要执行一个涉及多个块修改的操作比如创建文件必须同时更新 inode 表、块位图、目录项。如果这些更新只做了一半就断电盘上就会留下不一致状态inode 占用了某块但块位图说那块是空的。解决思路就是把“要做的多步修改”先记到一个日志区域。真正修改磁盘之前先把“如何修改”写进日志等日志安全落盘再去真正改数据。崩溃后重新挂载时文件系统根据日志重放或丢弃这些修改保证元数据一致。这就是 JBD2 日志系统干的事。4.2 三种日志模式与安全性分级ext4 沿用了 ext3 的三种日志模式区别在于“数据块”是否也进日志模式行为安全性典型场景datajournal数据先写进日志再写正式位置双写开销大最安全即使数据块损坏也能从日志恢复对数据安全极其苛刻的环境dataordered日志只记元数据但强制数据块先于元数据落盘高既保证一致性性能也可接受ext4 默认模式datawriteback日志只记元数据数据落盘顺序不定较低可能崩出“内容错乱但结构正常”的文件追求性能且能接受数据风险默认是ordered这也是绝大多数发行版的初始值。需要提醒的是不要把ordered理解为“文件内容一定最新”。它保证的是如果一个文件的元数据被持久化那么它对应的数据块至少在元数据落盘之前已经开始写了。但用户进程在write()返回后、fsync()之前崩溃文件内容仍可能丢失或部分丢失。4.3 故障恢复重放与孤儿清理挂载时如果发现日志非空ext4 会进入 recovery 流程JBD2 会把已提交但未完全应用的事务重新执行一遍。dmesg 里会看到类似输出EXT4-fs (sdb1): recovery complete EXT4-fs (sdb1): mounted filesystem with ordered data mode还有一种情况是“孤儿 inode 清理”。文件被删除时如果删除操作没有完成inode 会先被挂到一个孤儿链上等崩溃恢复后统一清理。这也是为什么异常断电后磁盘上偶尔会看到lostfound里多出一些文件——那就是恢复时找不到合法目录项的孤立文件。我干运维时最怕看到有人为了“提升性能”把barrier0加上。barrier 是保证 IO 请求顺序的手段关掉后日志可能先于数据落盘出现“日志说改完了但实际数据没改”的反转问题。实验室环境无所谓生产库千万别这么干。4.4 fast commit下一代优化点近几年内核合入了 fast commit 特性目标是显著降低fsync的延迟。原理很简单完整事务要覆盖大量元数据太慢fast commit 只记录极小的提交信息配合主日志使用让大部分fsync只落盘一小段快速日志。这个特性对数据库这类频繁fsync的应用很有价值。实际用不用得看特点和内核版本新内核配合新 e2fsprogs 才有意义。我记得这个特性在 5.10 之后逐步完善如果你在做高并发写入的测试可以关注一下。5. 块分配策略延迟分配、mballoc 与预分配5.1 延迟分配先攒着再一次性给够ext4 性能提升最关键的一点我认为是延迟分配delayed allocation。它的核心思想是写文件时不急着分配物理块先把数据放在 page cache 里等 writeback 真正要落盘时一次性把所有脏页的所有块分配出来。这个方法的好处是文件系统在分配时能看清“这次要写多少块”于是可以一次性申请一大段连续空间而不是 application 每写 4K 就去找一个块。后者正是 ext3 碎片化的主要原因之一。从语义上讲用户看到文件大小在增长但底层块还没分配直到sync或后台回写物理块才真正被占用。这也是为什么断电前后df显示的空间使用情况往往和文件名、大小对不上。5.2 mballoc多块分配器是怎么找空闲区的mballocmulti-block allocator是 ext4 的块分配核心。它会尽量在同一个块组里凑出连续空间如果当前组不行就换下一组。它还维护了一些预分配组专门给顺序写场景使用减少碎片。工具层面有个小技巧如果知道文件将来会很大可以先用fallocate预分配空间比如虚拟机磁盘镜像fallocate -l 20G vm.img这样 ext4 会提前把 20G 范围内的块分配好并记录成“未初始化” extent。读取该区域时文件系统保证返回全零所以是安全的。对数据库和超大日志文件预分配能显著降低运行期间空间分配和碎片整理的负担。5.3 碎片怎么量化怎么拆判断文件是否碎片化除了前面说的filefrag -v还可以看文件大小是不是和实际占用的 logical block 数对得上。如果filefrag输出的 extent 数量远超预期考虑整理或重写。简单粗暴的办法是cp file file.tmp mv file.tmp filecp 会产生新的连续分配mv 后新旧 inode 交换碎片就顺了一遍。不过 VFS 的 page cache 可能让这个操作的热数据仍然在缓存里如果想强制看到磁盘上的真实布局先sync再执行。6. 目录是怎么组织的线性目录与 HTree6.1 目录在磁盘上也是一种文件很多人以为目录就是“一层一层的文件夹”但磁盘上目录就是一个特殊文件里面存的是目录项。每个目录项记录了三样关键东西inode 号、文件名、文件类型。VFS 里的dentry缓存是内存概念磁盘上并没有所谓的“目录树节点”。当你访问/etc/hosts时内核先读根目录 inode找到etc这个目录项拿到etc目录的 inode再读etc目录文件找到hosts目录项最后定位到hosts文件的 inode。每个斜杠就是一次目录文件查找。6.2 小目录线性扫大目录用 HTree如果目录项很少直接在目录文件里从头到尾线性扫描就够快。但一个目录里放了几万、几十万个文件线性扫描就是灾难。ext4 在目录达到一定规模通常几块之后会启用 HTree也就是对文件名做哈希建成一棵树形索引。这样查找一个文件只需要走哈希树而不是翻遍整个目录。我用一个在嵌入式设备上踩过的例子说明问题。当时设备上有个缓存目录运行几个月后积累了近 30 万个文件。旧内核和文件系统下ls那个目录要等几十秒换成 ext4 并启用目录索引后基本是秒开。日常使用中如果你发现大目录操作明显变慢可以用e2fsck -f重新校验和重建目录索引。不过要注意HTree 索引和哈希值相关cp、find这些命令遍历目录时得到的顺序不保证和创建时间一致别在脚本里依赖顺序。7. 日常运维常用命令、挂载选项与避坑经验7.1 mkfs 和 tune2fs 的关键参数格式化命令里我常用的几个参数是mkfs.ext4 -L mydata -m 1 -E lazy_itable_init1 /dev/sdb1-L指定卷标-m 1把保留块比例从默认的 5% 降成 1%大容量分区上能省出不少空间-E lazy_itable_init1跳过初始化 inode 表的耗时操作格式化能快很多但在第一次大量写入时会略微变慢。另外-O可以明确开启或关闭特性比如mkfs.ext4 -O ^has_journal /dev/sdb1这样可以关闭日志但要明确这是为了什么。日志一致性是 ext4 掉电安全的核心非必要不要关。格式化完成后tune2fs -l是查看文件系统状态的瑞士军刀$ tune2fs -l /dev/sdb1 Filesystem features: has_journal ext_attr resize_inode dir_index filetype extent 64bit flex_bg ... Block count: 26214400 Reserved block count: 262144 Free blocks: 25067520 Inode count: 6553600 Free inodes: 6523100这里面每个字段都有实际价值Reserved block count是保留块Inode count和Free inodes用来排查“空间还有但 inode 用完了”的问题Filesystem features一眼看出 ext4 开了哪些能力。扩容也是老生常谈的话题。LVM 环境下lvextend之后直接resize2fs就能在线扩大文件系统不需要卸载分区。我遇到过一次在线扩容失败原因是临时分区没开flex_bgresize 时找不到足够连续空间来移动元数据。所以早期分区规划时别乱关flex_bg。7.2 挂载选项是时候做个取舍表了挂载选项对性能和安全性的影响很大下面是我实践中整理过的一张速查表挂载选项作用注意点defaults读写、suid、dev 等基础选项总得有个起点noatime/relatime减少 atime 更新频率默认就是 relatime没必要再激进关掉 atimecommitNN日志提交周期默认 5 秒调大能减少小写放大但掉电丢数据窗口变大dataordered元数据日志 数据有序落盘默认安全性和性能平衡barrier1保证关键 IO 顺序默认开启不要为了数字好看而关discard/nodiscard块释放时要不要发 TRIMSSD 上建议nodiscard 定期fstrim避免每次删除都触发 TRIMinline_data允许小文件内容塞进 inode看特性支持情况嵌入式场景里我经常给 eMMC 用noatime,commit60减少不必要的元数据写入延长 flash 寿命。但同样要考虑崩溃后丢数据的窗口如果设备会随机断电commit5还是更稳。7.3 嵌入式与特殊场景ext4 不是唯一解热搜词里有人提到 PlatformIO 使用 littlefs 之类的问题这让我想多说一句选型。ext4 设计目标是通用块设备适合 eMMC、SD 卡、SSD 和机械硬盘。但在小容量 NOR Flash、以及掉电频繁的 IoT 设备上littlefs 这类专为 flash 设计的文件系统往往更合适因为它有磨损均衡、掉电保护、O(1) 的目录操作元数据占用也很小。开发时用 NFS 挂载根文件系统方便调试正式量产则老老实实用 ext4 或 littlefs别混着来。8. 常见故障与排查实录从报错到根因8.1 “空间满了”但 df 看起来没事这是运维最经典的坑应用报“No space left on device”但df -h显示还有几个 G。原因大概率是 inode 用完了。文件系统里能创建文件的数量由Inode count决定不是由可写字节数决定。执行下面两条命令对照df -h df -i如果df -i的IUse%是 100%那就去删点小文件或者用find把海量小文件归类处理。还有一种更隐蔽的情况某个进程删了文件但还持有打开的文件句柄空间其实已经被释放但df一直显示占用。用lsof L1能列出这类已删除但未被释放的文件定位后重启进程即可。8.2 目录索引损坏ext4_find_entry 报错如果 dmesg 里出现类似EXT4-fs error: ext4_find_entry: 目录索引损坏的信息通常是目录的 HTree 索引和实际目录项对不上了。这种情况先别慌也别强行继续写入。正确做法是卸载分区或重启后执行e2fsck -f /dev/sdb1它会扫描目录结构重建索引并修复不一致。我见过有人为图省事直接加-y一路 yes结果把一些可恢复的目录项当成垃圾删掉。第一次修复别急着-y先不加参数跑一遍看它准备干什么心里有数再加。还有修复前如果分区很重要先dd做镜像风险控制永远是第一步。8.3 超级块损坏备用超快的救命用法主超级块损坏时直接mount会失败报错信息会说“wrong fs type, bad superblock”。ext4 的备份超级块有固定位置比如在块组 1、3、5、7 以及 3 的幂次组里常见备份位置是 32768、98304、163840 等。不知道具体位置时可以用mke2fs -n /dev/sdb1-n只打印参数不真正格式化也能输出备份超级块位置。然后用指定的备份块修复fsck.ext4 -b 32768 -y /dev/sdb1修复完成后立刻备份新的超级块信息。这套操作我救回过一次客户分区所以特别有印象。顺带一提看到 “wrong fs type” 先确认是不是真的 ext4有时候是分区表类型弄错或者把加密卷当普通设备挂载没必要一上来就 fsck。8.4 sync 和 fsync 的取舍热搜词里有人刷sync这里我多说一句。sync是把整个系统的脏页、元数据都刷一遍代价很大fsync(fd)只针对某个文件代价小得多。高频写场景下正确姿势是只在关键节点fsync比如数据库提交、配置文件替换后的 rename、嵌入式固件升级时的关键步骤。rename 之后还要fsync目录本身否则目录项没有持久化重启后可能找不到新文件名的文件。这个细节很多人踩过。另外ext4 的ordered模式只保证“元数据日志有序”并不是“数据零丢失”。如果业务对持久性有硬要求光靠文件系统默认选项不够还得靠应用层 fsync 策略、写放大控制和电源掉电检测方案配合。最后分享一个我自己的体会ext4 越透明你越感觉不到它的存在说明这套设计确实成熟。但真到排查问题那天上面这些知识点没有一个是多余的。尤其是“延迟分配”和“日志模式”这两个概念能帮你解释掉一半以上的 weird 现象。记住一个总原则一切性能优化都必须先想清楚掉电后的后果再决定要不要做。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑