
文件系统这个坑我断续填了六篇。作为常年跟Linux服务器和嵌入式设备打交道的人我越来越觉得“文件系统”是那种看着不起眼、但一出事就让人抓狂的东西目录打不开、机器重启后文件损坏、磁盘满了却删不出空间十有八九都是对底层机制缺乏认知。市面上讲ext4的文章不少但多数停留在“它有日志”“它支持大文件”这种概念层读完了还是不会排查问题、不会解释现象。这篇我把ext4从磁盘布局到一次读写的完整链路拆开讲尽量用大白话和实际操作结合适合做运维、搞嵌入式或者准备Linux面试的朋友。你能在这篇里搞清楚几个事ext4为什么是Linux默认文件系统、它如何在磁盘上组织数据、崩溃后靠什么恢复、以及你执行sync或遇到目录项异常时系统在底层到底做了什么。1. 为什么我们还在用ext4经典设计的生命力1.1 从ext2到ext4一条清晰的演进路线ext4不是凭空冒出来的。它的父辈是ext2和ext3血缘关系无比紧密。ext2诞生于1993年没有日志功能掉电后必须从头完整扫描文件系统来修复一致性对大容量磁盘来说可能要几十分钟甚至几小时。ext3在2001年加入了一个关键特性——日志journal它把元数据的变更先记录到一块固定区域掉电后只需要重放日志就能很快恢复一致性故障恢复时间大幅缩短。但ext3仍然沿用了ext2的块映射方式单个文件最大只能到2TB文件系统总量也有上限面对2000年代后期动辄几十TB的存储需求开始吃力。ext4在2008年作为内核2.6.28的稳定特性发布最重要的变化是用extent树取代了传统的块映射表并且把最大文件系统容量提升到了1EB单文件最大16TB。它保留了ext3的日志机制同时引入了延迟分配、多块分配等性能优化手段。这里有一个很多人忽略的点ext4文件系统可以被挂载为ext3使用反过来ext3也能挂载为ext4关闭新特性后这种兼容性让ext4在很长一段时间里都是最稳妥的升级路径。即便今天有xfs、btrfs这些“更现代”的选择很多发行版安装时默认分区依然是ext4原因就是稳、简单、资料多。1.2 ext4解决的核心痛点容量、速度、可靠性把ext4解决的问题拆开看核心其实有三个。第一是容量。ext4支持的文件系统最大1EB单文件最大16TB这对当前绝大多数场景来说已经够用了。xfs在单个大文件和高并发上有优势btrfs支持快照和压缩但ext4胜过它们的点在于结构相对简单出问题后的修复手段和文档都非常成熟。第二是性能。ext4在顺序写、随机写上都有不错的发挥尤其是延迟分配机制能把小块写入积攒起来一次性分配给连续磁盘块减少文件碎片。我用同一个SATA SSD对比过ext4和xfs的普通文件拷贝耗时差距基本在1%以内日常使用感知不到差别。第三是可靠性。ext4的日志默认工作在ordered模式意思是元数据先写日志等数据块真正刷到磁盘后再提交元数据。这种设计保证了掉电后不会出现“数据块存在但元数据还指向旧块”的错乱。再加上ext4的fsck工具非常成熟很多老运维都靠它救过命。1.3 与xfs、btrfs这些“后浪”的定位差异我经常被问现在都流行btrfs你还在讲ext4是不是过时了我的看法是工具选型没有银弹。xfs在处理超大文件、并行IO上更猛适合大容量存储服务器btrfs提供子卷、快照、校验和功能丰富但也因为复杂偶尔会有一些让人头大的兼容性或者性能问题。ext4更像是“皮实”的路线你用最普通的方式格式化它就能老老实实跑很多年。在嵌入式领域ext4更是根文件系统的常客后面我会专门展开这块。2. 磁盘上的物理解剖超级块、块组和inode2.1 扇区、块和块组磁盘空间的三级结构磁盘最底层的读写单位是扇区传统上是512字节现在常见4K扇区。文件系统操作时不会一个扇区一个扇区地处理而是把多个连续扇区组成一个“块”blockext4默认块大小是4KB。如果每次都单独分配一个4KB块一个100GB的分区会产生几千万个块管理起来太散所以ext4把整块存储空间划分成若干个“块组”block group每个块组包含连续的块例如默认情况下一个块组有128MB32768个块。块组内部各自管理自己的空闲块和inode用位图记录哪些块/ inode已经使用。这样的好处是局部性好文件尽量在同一个块组里分配减少磁盘寻道。作者组flex_bg机制还允许把多个块组绑成一个“弹性块组”让新文件在更大范围内聚合分配进一步降低碎片。理解了这个结构你就能明白为什么响应的备份和恢复都跟块组有关。2.2 超级块文件系统的“大脑中枢”超级块是ext4最关键的元数据里面存着文件系统的大小、块数、块大小、inode总数、空闲块/inode统计、校验和、挂载次数和最后一次挂载时间等信息。如果超级块损坏内核根本没法安全地挂载这个文件系统。为了防止坏块把超级块也带走ext4在每个块组都保留了超级块的备份通常隔几个块组放一份主超级块损坏时可以用备份恢复。实际恢复时经常用到这个命令# 假设主超级块损坏备份超级块在块组1偏移32768块 e2fsck -b 32768 /dev/sdb1这个参数就是告诉e2fsck使用备份超级块来检查并修复文件系统。我处理过一台机器因为磁盘坏道导致主超级块损坏靠这个方式救回来了绝大部分数据。所以我对“备份超级块”的价值深有体会关键时候真的能救命。2.3 inode文件的身份证与户口本文件系统里文件名只是给人看的标签真正定位文件数据是靠inode节点号。inode里存放了文件的所有元数据权限、所有者、时间戳、文件大小、数据所在块的位置通过extent树、以及各种扩展属性。每个块组也有自己独立的inode表用inode位图来记录哪些inode已被使用。注意目录也要占用inode目录的本质是一个特殊文件里面存放着目录项。inode的数量在格式化时固定所以会出现一种经典问题磁盘剩余空间很多但系统提示无法创建文件因为inode用完了。解决办法要么提前规划inode密度要么在外层换文件系统。这也是面试时经常拿来考察基础的题目。2.4 目录项怎么从路径名一路找下去目录是一个映射表每一项叫目录项dentry也就是dirent它把一个名字和某个inode号关联起来。比如你访问/home/user/a.txt文件系统会先从根目录/的inode通常是2号中找到home目录的inode接着读取home的目录项找到user目录的inode再读取user的目录项找到a.txt的inode最后通过inode找到数据块。整个路径查找就是一个逐级索引的过程。为了加速查找ext4支持目录索引使用哈希树把大量目录项组织成树形而不是线性扫描所以在包含几十万文件的目录中操作也不会卡得像ext3那样严重。但目录项如果损坏文件名对应不到inode就会出现“文件明明在却打不开”的现象这种情况往往需要通过fsck或者debugfs来处理。3. 核心机制拆解extent、日志与延迟分配3.1 extent树告别一环套一环的指针地狱ext2/ext3时代记录文件数据块用的是块指针数组每个指针指向文件的一个块。文件大了就需要多层间接指针就像老式图书馆的索引卡片先查一级卡片再翻二级卡片再找到书架。这种设计在大文件时会产生严重的碎片和额外的IO开销。ext4引入了extent机制一个extent代表一段连续的磁盘块比如文件占用了从块号100到200的连续101个块只需要记录一个起始块号和长度一个extent就描述完了。ext4用一棵树来组织这些extent文件不大时extent头直接放在inode里文件大就通过树的中间节点扩展。这样内存和磁盘IO都省很多这也是ext4处理大文件比ext3快得多的重要原因。3.2 journal日志掉电后的快速恢复路径ext4日志区是一个固定大小的环形空间默认128MB可通过tune2fs调整。关键操作分三步先是把要执行的元数据变更写入日志然后真正修改元数据最后等所有数据都安全落地后再把日志条目标记为提交。如果中途掉电挂载时内核会扫描日志把未提交的部分丢弃已提交但未完成的元数据变更重放一遍就能恢复到一致状态。ext4有三种日志模式模式行为数据安全性能journal数据和元数据都写日志最高最差ordered先保证数据块落盘再提交元数据日志高默认中writeback不保证数据块先落盘只记录元数据较低最快大多数人用默认的ordered就够了。只有在追求极限写性能、且能接受一定数据丢失的场景比如缓存节点才会考虑writeback。这里要强调journal保护的是文件系统一致性不等于把应用层数据完整保存进程的缓冲区间数据如果不落到page cache里文件系统也无能为力。3.3 延迟分配多块分配把碎片扼杀在摇篮里ext4有一个被低估但很关键的特性延迟分配delayed allocation。当进程调用write系统调用写入数据时内核并不会立刻为每次写入分配磁盘块而是先看一下数据是否最终会被写得更“有连续性”。它会尽量等到真正要刷盘时把所有收集到的写入一起安排成连续块一次性分配。这就是“延迟”的含义。配合延迟分配的是多块分配mballoc在一次分配中尽量一次满足多个块的需求而不是一次分配一个块、反复与位图交互。这两个机制叠加的效果是对同一个文件的多次小写最终在磁盘上形成连续区域后续读性能也更好。不过延迟分配也导致一个坑写完后立刻断电数据还没来得及分配实际块就丢了。所以你如果要确保数据落盘必须在业务代码里调用fsync或fdatasync而不能只依赖内核的回写时机。3.4 快速提交与持续改进ext4没有躺平很多人以为ext4发展到2008年就定型了其实内核一直在给ext4加新特性。比如fast_commit快速提交特性它把日志提交的延迟降到极低让注重IOPS的数据库类应用也能享受一致性保障。还有casefold不区分大小写的目录、encryption文件级加密、project quota目录级配额。这些特性说明ext4能够在不改变核心结构的情况下吸收新技术这也是它能长期占据默认位置的原因。4. 一次文件读写请求的全链路从VFS到磁盘4.1 VFS层统一一切文件系统的总入口你调用open/read/write这些系统调用先进入VFS。VFS是“虚拟文件系统”层它定义了一组标准接口每个文件系统都实现这些接口。VFS维护着三个重要缓存dentry cache目录项缓存、inode cache索引节点缓存、还有一个是mount树。路径查找会优先在dentry缓存里命中避免每个路径都重新遍历磁盘目录项。这也是为什么访问过的目录再次访问会快很多。VFS层还有一个重要工作是维护打开文件表每个fd对应一个struct file它指向inode和当前读写位置。删除文件时只要还有进程保留着打开它的fd磁盘上的inode和块就不会释放直到所有fd关闭。所以生产中常见的“删了文件但磁盘空间没变”就是某个进程一直在占用已删除的文件。4.2 page cache与writeback写数据到底缓存在哪文件读写到page cache这一层。读文件时内核把磁盘块读入内存页下次再读同一块直接命中内存。写文件时进程把数据拷贝到page cache里的脏页然后返回真正的写盘动作由内核的writeback机制异步完成。sync命令的作用就是强制让脏页尽快刷盘返回时确保数据已经写到持久存储。用过数据库的人都知道自己业务里调fsync比依赖系统sync更可控因为数据库要求的是事务日志先落盘。ext4在VFS和块层之间的角色是当writeback准备写一个inode的脏页时ext4会把对应的逻辑文件区间映射到磁盘块上这个映射过程中会创建extent、分配块然后生成bio请求发给块设备层。简单来说VFS决定“什么时候写”ext4决定“写到哪”。4.3 ext4的地址空间操作与bio提交每个文件在内存里都有一个“地址空间”管理着这个文件的page cache。ext4实现了writepages回调函数它主要做三件事把page cache中的页按文件逻辑偏移整理调用extent模块找到或创建对应的物理块生成一个或多个bio结构体提交给块设备层。块设备层再通过IO调度器把请求排队最终执行真正的磁盘访问。其中有意思的是ext4的“块预留”逻辑。在延迟分配阶段ext4已经算好需要多少个块但不立即分配。真正到writepages时如果发现有其他进程抢占了这些预留块它就得重新安排可能造成碎片。这解释了为什么高并发写入场景下碎片反而多一些也说明不是所有机制在任何时候都有益处。4.4 根文件系统的挂载路径从initramfs到ext4很多嵌入式Linux工程师会被“根文件系统挂载”这个概念绕晕。启动时内核首先把initramfs一个小的内存文件系统加载进内存然后执行里面的init程序。init程序负责加载块设备驱动、组装设备节点然后以只读方式挂载真正的根文件系统再通过/sbin/init做后续切换。如果根文件系统是ext4内核就需要能识别ext4的内置/模块驱动。嵌入式场景里有人为了调试方便用NFS作为根文件系统root/dev/nfs nfsroot服务器IP:/路径这样内核启动后通过网络挂载根文件系统主机上改代码设备端马上生效。但NFS根依赖网络生产环境很少用。绝大多数量产设备会选择ext4作为根文件系统原因无外乎SATA/eMMC/NAND Flash都可以格式化为ext4支持权限管理日志能应对意外掉电且驱动在内核里长期稳定维持。相比之下littlefs这类专门为Flash设计的文件系统则更多出现在无MMU或极小资源场景中咱们下一节聊。5. 另一个视角嵌入式中到底选ext4还是littlefs5.1 littlefs的设计目标与适用边界littlefs是ARM开源的一个面向嵌入式Flash的文件系统它针对的是MCU级的极小资源环境特点在于内嵌掉电保护不需要日志也能在Sudden Power Loss后保持一致。它通过一种类似“Copy-on-Write”的两遍提交机制和负载均衡的元数据存储实现掉电安全和擦写均衡。内存占用只有几KB支持磨损均衡非常适合NOR Flash、小容量SPI NAND。但littlefs不是万能的。它没有复杂的权限体系、没有内存映射IO设计目标也不是为超大文件和高吞吐服务。如果你在只有几百KB到几MB的存储上跑裸机或RTOSlittlefs是合理的。但如果你跑的是嵌入式Linux内核本来就提供了块设备层再用littlefs就有点“杀鸡用牛刀”。5.2 ext4在嵌入式Linux里的实际代价与优化手段嵌入式Linux设备通常有几十MB到几十GB的存储很多采用ext4作为根文件系统。要知道ext4的日志特性虽然保证了掉电一致性但也会产生额外的闪存写入。Flash的擦写次数有限如果不加处理日志区反复写可能加速坏块。因此很多产品的做法是挂载ext4时使用commit600这种参数延长日志提交周期减少写频率rootfs分区只读挂载比如mount -o ro把运行时可写数据放到单独的data分区对临时文件目录使用tmpfs不进ext4。这些做法都是实际产品里的常规操作。反过来如果你用的是一块高端eMMC且带硬件FTL闪存转换层ext4的日志损耗相对可控因为它本身会做均衡。5.3 选型对比没有最好只有更合适维度ext4littlefs运行环境Linux内核裸机/RTOS/Linux均可最小内存开销MB级别KB级别掉电保护日志fsck内建复制提交机制大文件支持优秀弱目标小型文件磨损均衡依赖下层eMMC/FTL自身做磨损均衡权限与多用户完整POSIX不支持q适用场景嵌入式Linux根文件系统/数据分区物联网传感器、小型NAND/NOR看到这里你应该明白讨论“ext4好还是littlefs好”没有意义关键是你系统里有没有运行Linux对POSIX语义要求多高以及闪存是裸的还是带FTL的。我个人的经验单板跑Linux、用到进程管理和动态库老老实实选ext4要是做一个带网络上传功能的小设备且Flash裸容量很小littlefs会更省心。6. 实战排查与面试高频问题6.1 常见故障现象只读、空间丢失和目录打不开第一类文件系统变为只读。通常是因为内核探测到ext4出错后为了防止更严重损坏主动把分区重新挂载为只读。这时候第一件事不是重启而是去dmesg看具体报错。如果只坏了一个无关紧要的目录可以用fsck修如果是硬件坏块导致超级块区域损坏就需要备份块。第二类标题里那种“文件系统找不到就卡住”往往是目录项损坏或者inode无法读取。应急思路是先用debugfs查看目录项尝试把文件导出来再做修复。第三类磁盘空间明明满了但是du统计出来占用却很少。基本可以判断是已删除的文件还被进程打开。用lsof | grep deleted找到对应进程重启进程空间就释放了。6.2 常用的“体检”命令与其等出问题不如日常做体检。我一般这样查# 查看文件系统特性、块组信息 dumpe2fs -h /dev/sda1 # 查看上电次数、上次挂载时间、错误行为 tune2fs -l /dev/sda1 # 强制检测但以只读方式只打印不修复 fsck -n /dev/sda1 # 查看是否启用了fast_commit等特性 debugfs -R feature /dev/sda1注意fsck不要对已挂载可写的分区执行除非是只读检测。否则会造成二次伤害。6.3 面试里关于ext4的经典追问结合Linux面试题里经常出现的方向我总结几个重点为什么文件删除了空间没释放因为还有进程持有fdinode未回收。你回答时可以引申延迟分配和page cache说明必须等所有fd关闭且脏页刷盘后块才释放。ext4日志三种模式的区别ordered模式因为数据先落盘能保证在崩溃后不会出现数据块与元数据不一致writeback快但要接受数据块可能晚于元数据更新崩溃时可能上报错。inode和目录项的关系文件名在目录项里文件属性在inode里目录项是文件名的映射。ext4和ext3最大的区别是什么extent与延迟分配再加上大容量支持。sync和fsync的区别sync是让内核中的脏页都尽量刷盘fsync是同步指定文件的fd及其数据到存储设备。fsync返回前一般已经通知块层真正写盘sync则只负责队列调度不一定等全部完成。这些问题看起来简单实际面试官会追问到底层行为所以建议把上面的机制和链路都亲手过一遍。7. 一点实战体会理解ext4之后很多坑会自动消失做文件系统排查这些年我最大的体会是如果你只背命令遇到千奇百怪的现象还是会慌但如果你理解磁盘布局和IO链路很多问题是可以直接从现象反推根因的。比如看到Input/output error你能想到坏块或超级块问题看到No space left on device但df还显示有空间你能想到inode或预留块看到日志里连续报“ext4_find_entry: deleted inode referenced”你能定位到目录项或inode不一致。我一直建议团队里的新人在不重要的虚拟机里亲手执行一遍mkfs.ext4、dumpe2fs、debugfs用dd制造几个坏块场景再跑fsck这个过程中的直觉比读十遍文档都管用。ext4没那么神秘它就是一个把“inode索引块分配日志提交”组合得当的系统。把这套核心逻辑装在脑子里以后再往上研究xfs、btrfs甚至自己设计一个文件系统都会轻松很多。