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下面的多媒体文件。/cacheOTA升级包、恢复日志的临时存放处。其中/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 deniedSELinux阻断或分区设备节点权限问题、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 pid1234 scontextu:r:untrusted_app:s0 tcontextu: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 devicejournal已中止下次挂载会进入强制回放失败状态。遇到它首先要考虑的是为什么journal中止而不是马上修复。JBD2: I/O errorjournal本身发生了物理读写出错说明硬件坏块的可能性直线上升。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最终要能正常读取依赖的是一条完整的链路FileProvider根据配置把URI映射到真实物理路径比如/storage/emulated/0/Android/data/com.some.app/files/xxx.pdf。目标应用通过ContentResolver.openFileDescriptor()调用系统服务系统服务会经由FUSE/存储服务去访问/data/media/0/Android/data/...下的文件。往下穿透到Ext4层真正从磁盘读取数据块。这三个环节中任何一个出问题都会导致URI打开失败。我在真实项目里就见过这种情况底层Ext4文件系统的SELinux上下文错误导致存储服务解析到路径后内核在目录遍历阶段直接返回EACCESFileProvider完全无辜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默认采用FBEFile-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只刷数据不刷metadatafsync两者都刷在闪存上该用fsync的地方别用fdatasync省那点时间。5.2 从sync角度看待重启、掉电后的数据状态系统层和文件系统层都有各自的sync行为。sync命令会调度一批回写确保所有脏块落盘。Android上还有一个安全性更高的机制——FSBARRIER和barrier1挂载选项。Ext4在journal提交时会下发barrier请求保证之前的数据块先于journal提交落盘避免元数据先到数据后到产生的数据损坏。这也是为什么如果在内核中找到真的掉电损坏往往是设备控制器本身违规牺牲了顺序或者板级NAND控制器做了奇怪的cache优化。做完一次正确排查之后我还建议关注errorsremount-ro这个行为是否还在。Android很多分区挂载时默认带errorsremount-ro它意味着一旦文件系统在运行时遇到错误内核会把分区切换为只读防止进一步损坏。这其实是个保护机制。很多人困惑为什么好好的突然变只读其实那是系统在保护数据。5.3 嵌入式场景的对照NFS和littlefs的启发经常有做嵌入式Linux的朋友来问为什么Android上的文件系统问题这么难搞其实对比一下你们常用的方案就明白了。比如调试阶段很多人用NFS v3挂载根文件系统。NFS下遇到文件系统问题往往是网络丢包、服务器端存储故障、或者NFS lock丢失导致的排查路径基本不用考虑本地磁盘结构和journal这跟Android完全不是一个玩法。而littlefs这种专门为微控制器设计的文件系统用copy-on-writeCoW策略替代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 bs1M statusprogress哪怕明明只是权限问题最后什么都不用修这步操作的成本也远低于一次错误的fsck带来的损失。如果你手里的是老设备不妨提前用tune2fs -l看看挂载计数和错误状态很多问题在你发现之前就已经在日志里躺了好几个月了。
返回列表