
有一次我在给一台Android测试机做例行存储压力测试设备突然弹了“存储空间不足”我下意识打开文件管理器看剩余空间却发现厂商设置里还显示可用2GB多。更诡异的是应用依然写不进去部分目录一打开白屏过一会儿连剪贴板复制都卡住。最后用adb敲了一轮命令真相才慢慢浮出来——问题压根不在“剩余容量”这个数字上而是Ext4文件系统内部的inode和块分配出了岔子。这类问题在Android设备上不算罕见。多数Android手机的内置存储data分区使用Ext4部分厂家在旗舰机型上把它换成F2FS但定制设备、工控板、车载中控甚至外置SD卡仍大量沿用Ext4。在系统开发、应用测试和运维场景里针对Ext4的问题排查远比“删点文件腾空间”要复杂。这篇文章就把我这些年处理过的一些典型Ext4故障从现象、日志到修复手段系统拆一遍重点讲清楚是哪些小细节让一个文件系统看起来“满了却又没满”以及用什么命令一步步把问题逼到墙角。1. 现场开局空间显示混乱时先用三条命令稳住阵脚1.1 看懂df、mount和/proc/partitions的真实含义遇到存储异常第一反应不能是去翻某个应用的设置页而要先让adb连上设备拿到文件系统层面的原始信息。我一般会依次执行这三条命令adb shell df -hT adb shell mount | grep -E ext4|f2fs adb shell cat /proc/partitionsdf -hT能直接显示分区的文件系统类型、总大小、已用和挂载点加了-T参数是为了确认目标分区到底是Ext4还是别的格式。mount命令则给出挂载选项比如rw、ro、seclabel、errorsremount-ro这些标记。/proc/partitions的意义在于建立分区的物理视图你能看到userdata这个分区在哪个主设备号、从设备号下后续查日志时需要把dm-0、mmcblk0p79这类名字对上号。我那次排查时df -hT的输出大致是Filesystem Type Size Used Avail Use% Mounted on /dev/block/dm-4 ext4 107G 99G 8.0G 93% /data第一眼看很合理93%当然可能报存储紧张。可问题在于接下来的df -i立刻推翻了这个结论。圣1.2 区分“块耗尽”和“索引节点耗尽”这是第一道分水岭df -i检查的是inode使用率也就是Ext4文件系统里用来记录文件元数据的索引节点数。文件也好目录也好在Ext4里都要消耗一个inode。数据块总量没满不代表inode够用这是“空间明明还有文件却一个建不出来”最常见的原因。我拿到的输出是这样的Filesystem Inodes IUsed IFree IUse% Mounted on /dev/block/dm-4 7.4M 7.4M 0 100% /datainode已用完剩余数据块再多也没有意义。这个分区上有大量由测试应用产生的小文件每个文件占一个inode几万个几万个地堆积最后把inode池彻底榨干了。df的“Used”列统计的是数据块不能反映这个情况而系统提示存储空间不足通常权重也偏向于可用块数量于是出现了“系统说满了df说还剩不少”的分裂景象。这个排查视角对整个流程非常关键先分清是块不够还是inode不够再往后走才有意义。这两个指标对应的修复方案完全不同后者不需要删大文件而是要把“文件数量”降下来。2. dmesg和/proc/mounts里的暗号从EXT4-fs error到只读挂载2.1 抓日志哪些报错是关键信息有些情况比inode耗尽更严重设备在运行过程中直接把文件系统挂载成只读应用表现是“所有涉及写目录的功能全部异常”。这时光看df没用要去翻内核日志。我在设备上常用adb shell su 0 dmesg | grep -iE ext4|jbd2|I/O error|remount-ro | tail -n 100日志刷屏时可以连续抓几次并比较确认报错是否为持续产生。典型的关键行长这样[ 1618.736914] EXT4-fs error (device dm-4): ext4_lookup: deleted inode referenced: 262145 [ 1618.737519] Aborting journal on device dm-4. [ 1618.738782] EXT4-fs (dm-4): I/O error while writing superblock [ 1618.739104] EXT4-fs (dm-4): remounting filesystem read-only这条链路过一遍基本上就能还原事故经过某个inode引用异常日志中断文件系统决定中止journal随后将分区切换为只读。至于最前面的根因可能是底层eMMC/UFS读写不稳定、断电导致的元数据不一致或者文件系统bug被硬件错误触发。还有一个细节很多人会忽略/proc/mounts里如果已经显示成ro证明坏局面已经发生要是显示rw但应用写不进去那要再往权限、SELinux和FBE加密那层去找别急着怪文件系统。2.2 errorsremount-ro机制为什么Ext4选择“立刻举手投降”Android的fstab里配置Ext4分区时通常带errorsremount-ro参数。它的意思是一旦Ext4遇上它认为不可恢复的内部错误就会主动把分区从rw降级为ro避免继续写入造成二次破坏。这个机制本身是保护而不是缺陷。没有它文件系统在元数据不一致的情况下继续写后果往往是整棵目录树彻底烂掉。调优者可以改成errorscontinue但这适合只读日志型分区的场景普通data分区不建议动。在遇到这种“突然变只读”的故障时我会把dmesg里EXT4-fs error出现前后的约200行日志全部拉出来保存再从最早一次报错的时间点开始倒推看当时在跑什么操作。很多案例最后能发现是某个不落盘的应用直接把缓存文件做到data目录然后睡眠唤醒时底层flash报错从而引爆了文件系统保护机制。3. 底下的块和inode借助dumpe2fs与debugfs深入文件系统内部3.1 inode耗尽后目录文件数为什么会呈现失控状态我在第一部分提到应用中产生小文件会耗尽inode这里补充一下机制。Ext4格式化的过程中会根据分区大小和块大小预先分配一个固定数量的inode池。如果你格式化时用了默认参数普遍情况下每16KB数据块配一个inode。对于128GB的数据分区块大小通常4KB总块数约3200万个inode数量则大约是800万。听起来不少但一旦有App把几十亿字节拆成一堆几KB的零碎文件inode数量会迅速被吃光。很多下载器、聊天应用和游戏资源包都有这个“碎文件制造”癖好。热词里那条典型的/storage/emulated/0/Android/data/com.tencent.tmgp.sgame/files/pandora/pr...就是某个应用把缓存和插件扔在外置存储应用私有目录下的典型模式。这些目录位于FUSE层之上暴露给App的是/storage/emulated/0/...但物理上落在/data/media而/data正是Ext4分区。所以应用层的文件访问最终都会转换成 Ext4的inode分配。可以用下面这条命令找出哪个目录文件数最夸张adb shell su 0 find /data/media -type f | wc -l adb shell su 0 find /data/media -type d | wc -l我遇到过某台设备仅/data/media/0/Android/data一个路径下就躺着超过120万个文件。这个数量在用户态几乎不可能用“看文件夹大小”的方式察觉到只有查inode覆盖才好发现自己已经处在爆仓边缘。3.2 用dumpe2fs和debugfs核对关键参数对一些可以由我们完全控制的分区比如外接SD卡、定制设备里的独立ext4分区我会直接读取超级块信息su 0 tune2fs -l /dev/block/mmcblk1p1 su 0 dumpe2fs -h /dev/block/mmcblk1p1重点关注这几个字段字段含义排查价值Filesystem features如has_journal、dir_index、extents等确认文件和日志能力开启情况Block count / Free blocks块总量与空闲块数判断块层面是否真的还有空间First block time文件系统创建时间判断是否异常重建或克隆过Reserved block count预留块比例解释“明明还有空间却写满”的隐藏规则Inode count / Free inodesinode总量和空闲量确认inode耗尽问题是否真实存在Reserved block count是个很容易被忽视的坑。Ext4默认预留5%的块给root用户和系统关键操作普通App在用户空间看到的可用空间已经扣掉了这部分但你也常有这种感觉——空间明明有普通用户就是写不进去。如果需要更底层的检查可以用debugfs打开块设备查看具体块位图、目录项、甚至过期inode的内容。命令形式一般是这样su 0 debugfs -R stats /dev/block/bootdevice/by-name/userdata su 0 debugfs -R ls -l /media/0 /dev/block/bootdevice/by-name/userdata-R stats能输出超级块解析结果和dumpe2fs的信息可以互相印证。-R ls则可以直接列目录。要注意的是如果分区当前正以rw状态挂载不要对主data分区直接执行debugfs写入类操作比如rm、clri这些只读操作相对安全强制写入会和正在运行的内核文件系统状态产生竞争。3.3 孤儿文件和延迟分配到底是怎么“偷走”空间的排查过程中我还反复遇到过一种现象文件删了、df -h却显示空间没释放。文件系统不断显示可用空间缩小通过常规rm删除大量文件之后仍然没有反映出实际空余。这时候再深入检查往往是两类情况造成的。一类是孤儿文件orphan file某个进程持有已删除文件的文件句柄文件从目录树上摘掉了但数据块仍被占用直到进程退出或重启才能释放。这也是Android OTA升级前经常提示重启的原因之一。用lsof | grep deleted能在一定程度上发现可疑句柄。另一类是延迟分配delayed allocation导致的未完事务Ext4为了减少碎片会先收集一批写请求再统一分配块。如果写入过程中断电或系统崩溃内核日志区可能残留未提交的块映射直观表现就是读得到文件大小但底层块没对上。这两种情况都不能靠简单清缓存解决保险做法是重启一次再观察df变化如果重启后空间依然没回来那多半需要进入恢复模式跑文件系统修复。4. 实际恢复流程从软修复到硬重建尽量保住数据4.1 先把分区从“只读”里捞出来当dmesg显示Ext4已经把分区remount成只读时最简单有效的做法是完整重启让文件系统以journal回放方式重新挂载。如果只是临时I/O扰动日志重放可以恢复一致性分区会重新以rw状态可用。重启后再次执行adb shell mount | grep /data adb shell df -hT /data确认rw、可用块和inode都恢复正常后再让业务进程继续做写操作。这个步骤看似简单却是整个排查里最关键的动作很多工程师一看到只读就直接想到格式化反而把还有救的设备提前判了死刑。但重启之前务必记得先保存日志。重启会把dmesg缓冲区清空如果没有提前落盘事后就只能查/data/anr、/data/tombstones或厂商的dropbox目录了。4.2 什么时候该跑fsck什么情况下只能格式化如果重启后依然变只读或者日志里反复出现同一inode的EXT4-fs error那就需要进入恢复模式运行文件系统检查。对于Android手机常规操作是关机后进入Recovery模式通过adb调用adb reboot recovery adb shell e2fsck -fy /dev/block/bootdevice/by-name/userdata-f强制检查-y自动回答yes。这里有一个非常反直觉的注意点如果分区能正常挂载并且文件正处于使用中直接在开机状态跑e2fsck是相当冒险的。工具要求分区未挂载或只读挂载它才能安全地修复位图和inode。修复完成后退出Recovery重启再交付给用户。多数单纯由断电、异常重启引起的元数据错乱这一步都能救回来。但如果日志里出现了大量底层块I/O错误比如blk_update_request: I/O error, dev mmcblk0, sector 12345678 Buffer I/O error on device dm-4, logical block 12345说明问题已经超出了文件系统层面很可能是eMMC/UFS闪存颗粒或控制器开始老化。这种硬件坏块类故障e2fsck会把坏区域内的文件标记为损坏并隔离但坏块继续扩散的话过几周又会出现同样症状。在这种情况下我的建议很直接尽快备份关键数据然后整体重刷设备或更换存储介质不要指望格式化能解决物理退化。4.3 万不得已的重建格式化data分区与后续注意事项一旦确认data分区目录结构损坏严重、e2fsck也修不回来那就只能走重建路线。在Recovery模式下执行adb shell mkfs.ext4 -F -b 4096 -m 0 /dev/block/bootdevice/by-name/userdata-m 0表示将预留比例降为0这对一般用户数据分区是合理的能释放出约5%的空间而区分区的超级块如果也需要重建或者遇到vendor分区问题就要按厂商分区表逐项恢复。格式化是个单向阀执行前务必想清楚本机所有用户数据、照片、应用数据都会消失。埋点也好、导出也好能备份的先备份别等到执行完才拍大腿。重建完data分区后还需要检查fstab里声明的文件系统类型是否与格式化结果一致。Android 10及以上设备普遍使用动态分区userdata在超级分区内对应一个逻辑块设备。如果只是删除某个动态分区然后重建没写回正确分区表重启时很可能直接进不了系统。5. 预防优先把碎片化写入、日志预警和文件数控制做到前面5.1 从应用层控制“文件数量风暴”排查完了事后措施比救火重要。inode耗尽这类问题往往不是某一天突然爆发的而是应用层写文件策略一路“裸奔”导致的。作为设备或系统的维护者如果在代码评审阶段就能强制应用把碎文件打包成大文件或者至少限制缓存目录总量很多悲剧都能避免。例如应用要把补丁包解压到/storage/emulated/0/Android/data/包名/files/目录时合理的做法是维护一个zip包而不是解出几千个小文件实在需要解包也要在会话结束时清理临时产物。Android的StorageManager提供了getStorageLowBytes()和getAllocatableBytes()接口进程在写入前先检查可用空间和剩余文件数能极大降低把设备写崩的概率。如果你管理的是开放给外部定制的系统还可以在init脚本里加上对/data分区的定期df -i告警。 平时压根没人去看它等到告警阈值到80%时就开始清理正好能把问题控制在萌芽期。5.2 预留块、预留inode与周期性trim的调优思路针对已知的“小文件大户”设备我在格式化分区时会有意增加inode密度。例如mkfs.ext4 -I 256 -i 4096 -b 4096 /dev/block/xxx-i 4096的含义是每4096字节数据分配一个inode这会让inode总量从默认的每16KB一个提升到每4KB一个。对于大量小文件的场景这是最直接的预防手段。代价是inode表本身会占据更多空间可用于文件的容量会相应减少所以普通分区不要无脑调先统计设备上的平均文件大小再定。另外Android设备需要周期性执行fstrim来回收闪存块。Ext4删除文件时逻辑块已经释放但底层eMMC/UFS并不感知仍需TRIM通知才能真正擦除。很多旧设备长期不执行trimdf的Free空间看着足够写入速度却越来越慢甚至报I/O错误。可以定期触发adb shell fstrim /data或在init.rc里配置包含fstrim的服务。想快速确认碎块是否严重可以看/proc/sys/fs/binfmt这类参数意义不大直观判断标准就是free块很多但写入掉速明显那基本就是底层块管理出了问题。5.3 Ext4和F2FS的选择不应只看跑分讨论预防不可避免会提到要不要把data分区切到F2FS。F2FS是为闪存设计的日志结构文件系统顺序写入、磨损均衡改善在随机写入小文件场景下通常比Ext4更激进这也是很多国内厂商直接使用F2FS当data分区的原因。但从排查视角来看Ext4也有它不可替代的优点工具链非常成熟e2fsck和debugfs几乎在所有环境都能找到损坏后的恢复流程被反复验证过。F2FS出了问题fsck.f2fs也存在但整体生态、文档深度和工业级案例都没法和Ext4相比。因此我个人的原则是像boot、vendor、dtbo这类分区且通常保持为只读的用Ext4无所谓如果data分区面对大量小文件写入并且更新流程可保持版本研测那么F2FS是更从容的选择。但我们调试中的绝大多数定制设备仍在用Ext4这不丢人问题在于要把它的inode、日志和trim管理摸透。6. 沉淀下来的一套快速定位手法最后分享一个我在多台设备之间反复验证过的排查顺序。遇到“Android存储空间显示异常、应用写不进去、或者data分区莫名变只读”的时候直接按这个路径走基本不会漏掉关键证据df -hT /data和df -i /data一起看先排除inode耗尽。mount | grep /data 确认是rw还是ro并记录挂载参数。如果只读立刻保存dmesg里的EXT4-fs error、JBD2、I/O error相关日志然后重启。重启后再看一次df和dmesg确认是临时扰动还是持续恶化。持续恶化则进Recovery跑e2fsck -fy修复后再验证。e2fsck无效或者底层I/O错误反复出现再考虑格式化或更换硬件。排查完成后把坏inode、目录项残留、文件数量分布这些结论回写到运维记录中避免两周后同一个问题重新侦查一遍。这一套流程读起来朴素但每一步都对应着一个“我亲眼见过有人跳过然后多花半天”的经验。比如只记df不记df -i直接错过inode耗尽一看只读就格式化白白丢掉一整个分区还能恢复的数据不看dmesg就乱跑e2fsck甚至把本可正常挂载的设备修出二次损坏。如果你手上也有一台状态奇怪的Android设备不妨先从最不起眼的df -i开始它往往比任何高级分析工具都更快地告诉你真相。