拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Android Ext4文件系统故障排查:从Journal机制到fsck实操

Android Ext4文件系统故障排查:从Journal机制到fsck实操

前阵子帮朋友排查一台老设备的Android系统,现象是重启后一直卡在开机动画,进了Recovery发现/data分区挂载失败。折腾了一整天,最后定位到是Ext4文件系统的journal区域出现了坏块。类似的坑我踩过不止一次,而且现在网上关于Android Ext4排查的资料要么太碎片化,要么直接甩一条fsck.ext4 -y让你跑完看运气。所以这篇想把整个排查思路串起来讲清楚:从底层机制到命令实操,再到几个和Android特有架构纠缠在一起的高频问题,一次讲透。

这篇内容适合这么几类人:做ROM定制和系统移植的开发者、需要处理用户数据恢复的运维人员、做自动化测试长期跑设备的工程师,以及那些设备老是莫名出问题的动手党。如果你只是普通用户,也可以照着里面的思路判断问题到底出在文件系统还是硬件,免得被误导。

1. 为什么Android把身家性命压在Ext4上

1.1 从分区布局说起

一台Android设备的存储里面通常分了很多区:boot、system、vendor、cache、data、recovery等。虽然不同厂商的分区名和个数差异很大,但从文件系统的角度来看,真正使用Ext4的往往是这么几个地方:

  • /system(或出厂后只读的product、vendor),负责系统核心文件。
  • /data,也就是userdata分区,承载所有应用、用户数据库、配置文件,以及/data/media下面的多媒体文件。
  • /cache,OTA升级包、恢复日志的临时存放处。

其中/data是最容易出问题、也最影响体验的。举个身边例子:很多同事经常遇到手机无缘无故提示"存储空间不足,无法启动系统",十有八九是/data分区在经历过异常掉电之后,文件系统标记成了错误状态,或者journal里有pending的事务没有回放完。

为什么Android官方长期默认使用Ext4,而不是开源社区里讨论度很高的F2FS或者XFS?其实原因并不神秘,主要是历史稳定性和工具链成熟度。Ext4是Linux内核里打磨了十几年的老牌文件系统,e2fsprogs工具链(fsck、tune2fs、debugfs、dumpe2fs)功能完备,在异常掉电、坏块、碎片化这些场景下的表现经过了足够多的验证。F2FS在NAND闪存上确实有一些优势,但它在掉电一致性、工具支持、SELinux上下文兼容这些方面走过不少弯路,Android官方也是直到后来才敢逐步在一些设备上启用它。

这给排查带来的直接影响是:你几乎所有的现有Linux经验都能直接落地到Android上。Android的Ext4分区和PC上的Ext4分区在底层是同一套协议,只是挂载参数、分区偏移、SELinux标签不同罢了。

1.2 Ext4几个"平时看不见但排查时最有用"的机制

要排查问题,先得知道这个文件系统在背后干了些啥。我挑几个和故障强相关的关键机制说。

Journal(日志)机制。Ext4默认会划出一定空间存放journal,用于保证元数据的一致性。写操作发生时,先把事务写进journal,再真正落到磁盘上的目标位置。如果中途掉电,下次挂载时内核会强制回放journal里的日志,把文件系统恢复到一致的状态。这也是为什么拔SD卡、断电、强制关机之后,Linux系统启动时有时候会看到"recovering journal"一行字。

理解journal对排查很有帮助,因为大量"莫名其妙"的文件丢失、挂载失败,根源其实在journal回放时出了问题。比如journal区有坏块,内核可能直接拒挂或者挂载后立刻变只读。这种时候跑fsck,有时能修好,有时会越修越糟,关键要会判断。

Extent树(B+树索引)。现代Ext4用extent来管理连续数据块,一个文件在磁盘上通常是若干连续的extent。这种设计大大减少了元数据开销,但也带来了一个排查点:如果extent树结构损坏,文件系统通常表现为"文件大小还在,但读出来的内容是乱的"或者"目录里有文件名但打开报I/O错误"。

延迟分配(Delayed Allocation)。数据写入时会先在内存缓存中攒着,等到内核决定回写时再分配磁盘块。这个机制大幅提升了连续写入性能,但也意味着"程序认为数据写完了"和"数据真正落盘"之间存在时间差。掉电时,如果脏页还没刷下去,数据就会丢失。很多人说"文件写进去丢了,是不是文件系统坏了",其实只是延迟分配和掉电之间博弈的正常结果。

1.3 一个关键认知:文件系统问题的世界和用户看到的世界是两回事

排查久了你会发现,用户描述的现象和文件系统底层报错之间隔着一层很大的鸿沟。

用户说"我的照片打不开了",底层的现象可能是:inode损坏、目录项引用的extent指向了错误的数据块、SELinux上下文丢失导致应用没有权限读取、FUSE服务和底层文件系统之间的挂载状态异常。这四种情况的排查路径是完全不同的。

所以你遇到问题,第一步永远不要急着跑fsck。先把现象翻译成底层可能的原因,再决定要不要让fsck动刀。我见过太多人一上来就fsck -y,结果把一个其实还能抢救的只读挂载问题,直接改成了一堆孤儿文件。后面我会细讲怎么避免这种操作。

2. 先把故障现象分成四大类,防止乱修

我习惯把Android Ext4问题分成四类,这样排查的时候思路清晰很多,也方便直接对号入座。

2.1 开不了机、卡logo,或者不断重启

这是比较吓人的一类。现象是刷完机第一次启动就卡在开机logo,或者设备用着用着突然重启,然后再也起不来。

底层原因一般是两种。一种是挂载时失败导致系统无法继续,最常见的是/data分区无法挂载;另一种是挂载成功,但启动过程中访问某些关键文件时发生I/O错误,导致zygote、system_server反复崩溃。

如果是前者,进Recovery之后手动mount一下userdata分区就能看到具体的错误信息。常见错误有"Structure needs cleaning"(文件系统标记为错误状态,需要fsck)、"Failed to mount /data: Permission denied"(SELinux阻断或分区设备节点权限问题)、"Not a directory"(分区内容完全错乱,通常是选错分区或刷入了不匹配的镜像)。

后者则棘手一些,因为日志被大量开机进程刷屏淹没。这时候建议先关掉自动重启,竞相用adb logcat或者抓/proc/last_kmsg(老内核)观察崩溃点附近有没有ext4相关的I/O错误。

2.2 分区挂载失败或挂载后立刻变只读

内核在挂载ext4时,如果发现文件系统的错误状态标志被置位(比如超额的链路错误计数、日志回放失败),会进入拒绝挂载或只读挂载的模式。这是Ext4的自保护机制,避免在损坏状态下继续写入造成二次破坏。

一条典型的日志长这样:

EXT4-fs (mmcblk0p25): errors on device, fsck required EXT4-fs error (device mmcblk0p25): ext4_lookup: ... Remounting filesystem read-only

看到这种日志,第一个动作是判断"为什么内核认为文件系统错了"。可能是上次掉电前有未完成的事务,可能是硬件坏块开始出现,偶尔也可能是内核升级后挂载参数和旧文件系统不兼容(比如启用了metadata_csum之后老工具链不支持)。

2.3 文件在、目录也正常,但读不出内容或者空间对不上

这类现象最迷惑人。ls能看到文件名,stat能看到大小,但cat或者cp一执行就报Input/output error,或者读到一半内容错乱。

我遇到过一次很典型的案例:同事设备里有一个文件夹,所有文件名和大小看起来都是正确的,但里面十几个MP4没有一个能播放,全部报I/O错误。用debugfs检查后发现,这些文件的inode指向的extent里有几个块被标记为bad block,物理介质读取失败。

另一种"空间对不上"也很常见:df显示还有20GB可用,但拷贝文件时系统提示存储空间不足。这里有两个坎:一是/data分区往往预留了约5%的reserved blocks(给系统关键进程在磁盘满时保留空间),df默认不显示;二是删除文件后空间没有立即释放,如果文件还被进程以"已删除但仍打开"的状态持有,占用的block不会被回收,这在长期运行的设备上很致命。lsof看进程打开的文件描述符,如果发现文件名后面带着(deleted),那就是典型情况。

2.4 权限类报错:SELinux上下文和目录权限

这类问题文件系统本身可能没坏,只是它的xattr(扩展属性)出错了。Ext4支持存储security.selinux这个扩展属性,Android上每个文件都有对应的SELinux上下文标签。如果标签丢失、被错误修改、或者恢复出厂时刷镜像没带正确的SELinux context,那么即使文件的Unix权限是rw-r--r--,应用仍然可能无法访问。

典型的报错往往不是来自ext4本身,而是来自SELinux拒绝日志:

avc: denied { read } for pid=1234 scontext=u:r:untrusted_app:s0 tcontext=u:object_r:userdata_file:s0

排查这种问题时,ls -Z查看安全上下文是否丢失或错挂是第一步。如果用TWRP挂载后跑过一些古老的chmod -R脚本,或者某些工具没有遵循preserve_context参数,就很容易把SELinux标签搞坏。这点重启后往往表现为"某个应用老是崩"或者"文件管理器能看到文件但打不开"。

3. 一套可以照抄的排查命令链

当设备真的出了问题,下面这套流程我已经用过很多次,稳得很。前提是你有 adb 和任意能进 Recovery 的方法,最好是 TWRP 这类有文件管理能力的第三方Recovery,或者直接由bootloader进fastboot。

3.1 第一步:停止一切写入,收集现场

这句是整篇文章里最重要的一句:发现文件系统故障后,第一时间停止向故障分区写入任何数据。否则你正在覆盖的可能正是以后你用来恢复现场的关键数据。

收集现场要做三件事:

  • 拿到当前挂载状态:mount | grep -E "ext4|f2fs",确认哪些分区是ro、哪些是rw,哪个分区已经掉出来了。
  • 抓内核日志,尤其带ext4关键字的:dmesg | grep -iE "ext4|jbd2|block"。
  • 如果设备能进系统,记下当前df -h和df -i的状态,看是否出现异常大的已用inode数或异常低的可用block数。

这一步收集的信息往往就能决定后续方向。比如日志里直接出现journal has been aborted,说明问题大概率在journal或硬件;如果出现No space left on device但df显示还有空间,那要考虑块分配位图和实际block状态之间的偏差,可能需要e2fsck介入。

3.2 第二步:从日志中定位具体报错

在Recovery下,dmesg一样能用。注意检视这样几个关键字符串:

  • EXT4-fs error:内核对文件系统内部结构不一致的现场报告,后面通常会跟着具体的函数名,比如ext4_lookup、ext4_mark_inode_dirty、ext4_find_entry。这些函数名本身就是初步诊断线索,比如频繁出现ext4_lookup相关的错误,多半是目录项损坏,而ext4_mark_inode_dirty出错则多半和inode位图或journal状态有关。
  • Aborting journal on device:journal已中止,下次挂载会进入强制回放失败状态。遇到它,首先要考虑的是为什么journal中止,而不是马上修复。
  • JBD2: I/O error:journal本身发生了物理读写出错,说明硬件坏块的可能性直线上升。
  • EXT4-fs (device): mounted filesystem with ordered data mode:表示挂载成功,可以暂时排除挂载层面的问题。

日志里看到的错误,配合分区设备路径可以精准定位到是哪个分区。Android 10以后基本都用分区名软链,比如/dev/block/bootdevice/by-name/userdata,所以在日志里看到mmcblk0p58这种,用它对照ls -l /dev/block/bootdevice/by-name/就能翻译成 human friendly 的名称,方便后续操作。

3.3 第三步:fsck的三种姿势

主要看情况选择,不要无脑-y:

只读检查(推荐先做):

e2fsck -fn /dev/block/bootdevice/by-name/userdata

-f强制检查,-n表示只读,所有问题一律回答No。这一步完全不写入任何数据,适合先给文件系统做一次"体检",看它到底损坏到什么程度。如果在这里看到大量Fix?的提示,先不要急着修复,把输出内容拍下来或者保存到U盘/OTG上,后续可以判断哪些是严重问题。

自动修复(谨慎选择):

e2fsck -fp /dev/block/bootdevice/by-name/userdata

-p表示自动修复safe问题,-f强制检查。这个组合相对保守,修复的都是一些明确安全的问题,比如孤儿inode清理、块位图校准。如果你不确定要不要执行,可以先-fn检查后在提示列表中评估修复项的类型。

全面修复(基本靠运气,但有时候只能这么干):

e2fsck -fy /dev/block/bootdevice/by-name/userdata

-y对所有问题都回答Yes,风险在于可能会丢掉大量文件,尤其是当某个目录所在的块组严重损坏时。无论如何,跑这种命令之前请先做镜像备份(后面细说)。

这里有个细节可能很多人不知道:fsck不能和已挂载的文件系统同时运行。文件系统在挂载状态下,内核会持续进行脏页回写、journal更新,fsck读到的是不断变化的状态,结果必然不可靠。所以在Recovery下,只要分区没自动挂载上最好;如果TWRP已经帮你挂载了/data,需要先umount /data再执行fsck。

3.4 第四步:检查和修复配套属性

fsck修正的是文件系统结构层面的东西,但Android系统要正常引导,光有结构还不够。还需要检查另外两样东西。

一是挂载计数和错误计数。用tune2fs -l查看:

tune2fs -l /dev/block/bootdevice/by-name/userdata

重点看这几个字段:Errors behavior(错误处理策略,比如Continue/Read-only)、Mount count(累计挂载次数)、Maximum mount count(达到多少次后触发强制自检)、Free blocks、Free inodes。知道了Maximum mount count之后,你就能理解为什么很多设备在长时间不重启后会偶发"慢启动"——那是文件系统在自检,系统整体体验会变卡。

二是SELinux上下文。在Recovery环境里,挂载后执行:

ls -Z /data

正常情况下的输出应该类似:

u:object_r:data_root_file:s0 . u:object_r:system_data_file:s0 system u:object_r:userdata_file:s0 data u:object_r:media_rw_data_file:s0 media

如果发现标签丢失(输出?或者全都变成u:object_r:tmpfs:s0之类的不正常标签),就需要在上层做restorecon操作。这通常需要把分区挂载为rw,在Recovery里或者通过adb执行restorecon -R /data(前提是文件系统挂载到/data且SELinux处于permissive或enforcing但策略允许)。不过要注意,恢复出厂设置时的mkfs.ext4 -O quota也会影响标签初始化,所以如果一个分区第一次启动就全部标错,考虑是不是刷入了没有正确处理SELinux的镜像。

4. 四个真实高频案例的完整链条

这里挑四个我实际遇到过、而且网上提问量非常大的场景,把从现象到根因的完整链路捋一遍。尤其是前两个,它们表面上是App层的问题,但根子扎在底层存储状态上。

4.1 案例一:应用FileProvider返回的URI一打开就报"无法访问"

开发Android应用的人应该都被content://URI坑过。比如某个应用通过FileProvider分享文件,生成的URI看起来是:

content://com.some.app.fileprovider/external_root/Android/data/com.some.app/files/xxx.pdf

但分享给微信、邮件、或者另一个应用之后,打开时直接报FileNotFoundException或者"文件不存在"。很多人排查时只盯着file_paths.xml的路径配置,其实底层链条远不止这一环。

这个URI最终要能正常读取,依赖的是一条完整的链路:

  1. FileProvider根据配置,把URI映射到真实物理路径(比如/storage/emulated/0/Android/data/com.some.app/files/xxx.pdf)。
  2. 目标应用通过ContentResolver.openFileDescriptor()调用系统服务,系统服务会经由FUSE/存储服务去访问/data/media/0/Android/data/...下的文件。
  3. 往下穿透到Ext4层,真正从磁盘读取数据块。

这三个环节中,任何一个出问题都会导致URI打开失败。我在真实项目里就见过这种情况:底层Ext4文件系统的SELinux上下文错误,导致存储服务解析到路径后,内核在目录遍历阶段直接返回EACCES,FileProvider完全无辜,file_paths.xml配置当然也检查一万遍没毛病。

排查建议:先用adb shell ls -lZ /storage/emulated/0/Android/data/被分享的应用包名/files/目标文件看物理文件是否存在,SELinux上下文是否正确;再抓系统服务日志,看有没有AVC denied信息。确认底层没问题之后,才回头检查FileProvider的xml配置。其实有不少"分享打不开"的最终根因是/data/media目录上了 FUSE 后出现了挂载状态异常,重启或重新挂载存储即可恢复。

4.2 案例二:/storage/emulated/0/Android/data目录进不去

Android 11之后,系统出于隐私保护对/storage/emulated/0/Android/data目录做了特殊限制,第三方文件管理器基本进不去。这个限制本身是上层策略,但如果你做过系统开发或者有root权限,会发现一个有意思的现象:即便你有root,有时候依然进不去这个目录。

为什么?因为两层因素叠在一起。第一层是VFS层的路径访问控制(Android 11之后在ExternalStorageProvider里做了拦截);第二层是底层挂载选项和SELinux策略。如果你在挂载Ext4分区时没有带上正确的上下文或者挂载选项,系统会认为这个挂载点不具备访问这些目录所需的安全属性,即使你以shell身份运行,也会被SELinux挡住。

对于做系统集成的人,调整这类问题时的正确路径是:先确认挂载本身正常(mount | grep sdcardfs/fuse),再确认SELinux策略是否给了对应domain访问权限,最后才是去抠/data分区的底层权限。千万不要上来就chmod -R 777这种大扫除,这在Ext4上会连带破坏SELinux标签,后续问题更多。

4.3 案例三:分区传文件到一半报空间不足

"明明显示还有空间,为什么拷贝不了"——这个问题在PC上也很常见,但Android上有一个特殊根源:延迟分配与掉电的组合。

假设你的程序已经write()了一大堆数据,并且调用了close(),但数据还停留在页缓存里。此时如果你突然拔掉SD卡或者设备异常重启,延迟分配的块可能根本就没分配到inode上。系统重启后,stat文件、df空间都正常,但文件实际内容只有开头一部分,甚至整个文件为空。碰到这种,先查写数据到落盘全过程中进程有没有做fsync,而不是急着去动文件系统。

另一种空间不足的根源是预留块。Ext4默认预留5%的块给root进程用,Android上部分分区预留比例可能更高。df -h看到的"已用空间"是不含预留块的,但当你以普通应用身份写入时,无法使用那部分空间。所以有时候"空间满了"其实并没有满,只是非特权进程可用空间不够了。排查时可以看tune2fs -l里的Reserved block count,心里就有数了。

4.4 案例四:刷完第三方包后/data格式化失败

刷机不畅导致/data格式化失败,也是高频故障。现象大多是TWRP里选择Wipe data之后恢复系统,重启却依然出现"加密失败"或者"无法解密"。

这个问题的根子大概率不在Ext4本身,而在于加密和文件系统标志。现代Android默认采用FBE(File-Based Encryption),/data分区是加密的。Recovery在格式化时,如果只做了mkfs.ext4却没有写回正确的加密metadata信息,系统启动后会认为分区从未初始化或者加密状态异常。此时你在Recovery里看到的OpenSSL/DM-Crypt相关报错,根源往往是ext4的metadata_csum、encrypt等feature和Recovery的make_ext4fs版本不匹配。

这个案例给我们的经验是:在Android上操作Ext4,永远要确认手里的工具链版本和内核的feature列表是对齐的。你在PC的X86环境下用e2fsprogs 1.42格式化出来的分区,装到Android 14设备上可能是完全不能挂载的。

5. 往里再走一步:VFS、sync和掉电一致性

5.1 为什么"数据写进去了"和"数据已经在磁盘上"是两回事

Linux的I/O路径经过VFS、页缓存、块层、驱动到存储介质。write()返回成功,只代表数据进入了内核的页缓存,不代表已经写到磁盘。脏页回写由内核线程(pdflush/writeback)决定时机,通常是有脏页比例超限、显式sync、或者系统空闲时才会触发。

Android对这块的教训不少。很多系统应用在做关键数据写入时没有调用fsync或者fdatasync,结果设备一旦异常重启,数据库、配置文件就回到了旧状态。我记得有个做测绘的App,现场采集数据全丢了,最后的根因就是它在电量低时做了大文件写入,写完后直接休眠,没有落盘。

建立这个认知对排查有非常大的帮助:当你发现"文件内容不对"或者"文件消失了",先别假定Ext4损坏,先看这个文件在写入的时候有没有执行过fsync。fdatasync只刷数据不刷metadata,fsync两者都刷,在闪存上,该用fsync的地方别用fdatasync省那点时间。

5.2 从sync角度看待重启、掉电后的数据状态

系统层和文件系统层都有各自的sync行为。sync命令会调度一批回写,确保所有脏块落盘。Android上还有一个"安全性更高"的机制——FSBARRIER和barrier=1挂载选项。Ext4在journal提交时会下发barrier请求,保证之前的数据块先于journal提交落盘,避免"元数据先到,数据后到"产生的数据损坏。这也是为什么如果在内核中找到真的掉电损坏,往往是设备控制器本身违规牺牲了顺序,或者板级NAND控制器做了奇怪的cache优化。

做完一次正确排查之后,我还建议关注errors=remount-ro这个行为是否还在。Android很多分区挂载时默认带errors=remount-ro,它意味着一旦文件系统在运行时遇到错误,内核会把分区切换为只读,防止进一步损坏。这其实是个保护机制。很多人困惑"为什么好好的突然变只读",其实那是系统在保护数据。

5.3 嵌入式场景的对照:NFS和littlefs的启发

经常有做嵌入式Linux的朋友来问:为什么Android上的文件系统问题这么难搞?其实对比一下你们常用的方案就明白了。

比如调试阶段很多人用NFS v3挂载根文件系统。NFS下遇到文件系统问题,往往是网络丢包、服务器端存储故障、或者NFS lock丢失导致的,排查路径基本不用考虑本地磁盘结构和journal,这跟Android完全不是一个玩法。

而littlefs这种专门为微控制器设计的文件系统,用copy-on-write(CoW)策略替代journal,把掉电一致性做在了一套简洁得多的元数据管理机制里。它的优势是代码量小、掉电安全、且对底层介质要求低,缺点则是不能支持Android那么复杂的权限、多用户、加密、quota需求。

对比下来就知道,Android选择Ext4并且忍受它的复杂度,本质上是权衡了容量、兼容性、工具成熟度与安全策略之后的结果。这也提示我们:排查Android Ext4问题时,尽量借用Linux服务器领域积累的成熟经验,比如e2fsck -n做无破坏体检、debugfs做文件级恢复、dumpe2fs检查超级块备份,这些都是移动时代里被很多人忽略但极其好用的工具。

提到debugfs,我多说一句:如果某个分区挂载失败,而你手头没有备份工具,可以用debugfs的cat命令直接读取inode对应的数据块内容到外部文件,这种操作在TWRP有限的工具环境下可以救命。

我在实际处理这类问题时会保留一个习惯:任何重要设备在处理文件系统故障前,都先做一份整个分区的镜像备份。命令简单到只有一行:

dd if=/dev/block/bootdevice/by-name/userdata of=/external_sd/userdata.img bs=1M status=progress

哪怕明明只是权限问题最后什么都不用修,这步操作的成本也远低于一次错误的fsck带来的损失。如果你手里的是老设备,不妨提前用tune2fs -l看看挂载计数和错误状态,很多问题在你发现之前就已经在日志里躺了好几个月了。

返回列表