有一次我在给一台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、errors=remount-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 errors=remount-ro机制:为什么Ext4选择“立刻举手投降”
Android的fstab里配置Ext4分区时,通常带errors=remount-ro参数。它的意思是,一旦Ext4遇上它认为不可恢复的内部错误,就会主动把分区从rw降级为ro,避免继续写入造成二次破坏。
这个机制本身是保护而不是缺陷。没有它,文件系统在元数据不一致的情况下继续写,后果往往是整棵目录树彻底烂掉。调优者可以改成errors=continue,但这适合只读日志型分区的场景,普通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 inodes | inode总量和空闲量 | 确认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通知才能真正擦除。很多旧设备长期不执行trim,df的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开始,它往往比任何高级分析工具都更快地告诉你真相。