ARTICLE DETAIL

资讯详情

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

Android存储问题排查:从Ext4原理到FUSE、I/O卡顿实战

Android存储问题排查:从Ext4原理到FUSE、I/O卡顿实战 在做Android系统或应用开发时遇到文件相关的问题几乎是一种“必然”。凡是涉及存储、IO、权限的疑难杂症最后往往都会撞到同一个词——Ext4文件系统。这篇文章是本人近两年排查Android设备存储问题的一份实战沉淀从分区布局、权限报错、I/O卡顿到掉电损坏把真正踩过的坑和有效记录、定位、修复的思路完整梳理出来。内容会覆盖从adb shell到dmesg、从e2fsck到FBE、从VFS层到磁盘块的层面适合正在被“磁盘明明没满却写不进文件”或者“文件突然不见了”这类问题折磨的Android系统工程师、嵌入式开发者也适合对存储原理感兴趣的应用层选手。1. 问题从哪里起Android的Ext4分区布局与两个“存储”概念先纠正一个常见误区。很多同行在讨论Android存储问题时把“存储空间”混为一谈。实际上一台Android手机里至少有完全不同的两套“存储”逻辑第一套是物理分区层面的Ext4系统分区比如/system、/data、/cache、/vendor这些挂载点。它们由内核的块设备驱动直接管理跑在真正的磁盘块上。这里的一切由VFS虚拟文件系统与具体文件系统驱动协同工作。/data分区通常是Ext4或者F2FS存放所有应用数据。第二套是你通过文件管理器看到的“内部存储”例如/storage/emulated/0。这套路径其实并不是独立的文件系统而是由FUSE(filesystem in userspace)守护进程把/data/media目录桥接出来的一个“用户视图”。sdcardfs或者现代Android的fuse实现负责这个转换。所以你在/storage/emulated/0/Android/data/下看到的东西本质上还是落在/data/media这个Ext4目录树下面。搞不清这两层排查问题的时候就会走弯路。比如一个典型的报错unable to chmod /storage/emulated/0/android/data/com.xxxx: operation not permitted这个错误在应用侧和系统侧看到的原因完全不同。应用侧通常是Android的分区存储权限机制Scoped Storage在起作用而不是文件系统权限。你在/data/media层面对目录的权限设置因为FUSE绕了一层可能没问题但FUSE上层对外呈现的权限位和实际VFS层的不一致chmod就会被打回。这类问题本质上是“上层策略限制”跟磁盘块层面没有关系。正因为我长期和这两层存储打交道下面要讲的排查思路才足够收敛。遇到问题先问自己一句这个现象是发生在真实Ext4分区上还是发生在FUSE桥接层定位方向完全不同。2. “磁盘没满却写不进”的真实原因block耗尽、inode耗尽与保留块这类问题最常见也最容易误判。现象是写文件时返回ENOSPCNo space left on device但是用df -h一看磁盘还剩下几个GB甚至几十个GB。这时候多数人的第一反应是“系统统计错了”但Ext4自身坑很多至少要从三个层面去查。2.1 是inode耗尽不是block耗尽df -h只看数据块的使用率而df -i看inode使用率。小文件特别多的场景下inode可能先被耗尽。Android设备上大量小文件缓存、游戏资源分包、WebView缓存目录往往都有几十万个文件inode很容易被打满。这是排查中优先级非常高的检查项。举个例子项目里遇到一台测试机明明/data还有3GB空间但所有应用都报告无法写入。输入adb shell df -i /data看到Inodes这一列的IUsed已经到达100%。定位到问题是某个应用在循环生成无意义的临时文件每个文件还要setfsize导致文件系统把元数据刷得飞快。清理后恢复。诊断命令adb shell df -h /data adb shell df -i /data adb shell tune2fs -l /dev/block/mmcblk0p44 | grep -E Free blocks|Free inodes|Reserved block count顺带一说Ext4默认保留5%的块给root用户这个保留块主要用于防止日志写满导致系统崩溃。对Android这种不追求5%空间的系统编译时可以把保留比例调成0-m 0建文件系统或tune2fs -m 0。不然那块“看不见”的保留空间也会算进ENOSPC。2.2 碎片与损坏的预处理保留块耗尽reserved block count用完之后即便df -h显示还有剩余普通用户进程也可能触发ENOSPC。注意Android上很多系统服务是以root身份运行的所以root不一定立刻崩但普通应用写入会先崩。我之前排查过一个诡异案例df -h显示/data还剩1.5GB任何应用一写文件就报ENOSPC。tune2fs -l一看Reserved block count是0这看起来没问题啊但继续往下翻发现Free blocks其实只剩几千块而df计算的是“包括保留块在内”的总剩余。为什么df和tune2fs看到的数字不一致因为df统计的是文件系统可见的块而很多小文件占用的间接块和extent树也会吃掉不算进常规统计的资源。所以真正釜底抽薪的办法是看tune2fs -l的原始数据别只信df。2.3 目录项计数与extent树的隐性占用还有一种更隐蔽的情况目录项(dentry)数量多到一定程度后Ext4的目录索引Htree会极度膨胀。此时du -sh显示的总占用可能不大但是文件系统内部的extent映射树已经碎片化严重任何一次写入都要做大量元数据操作。表现出来就是写文件速度暴跌、IO等待飙升、甚至有时etx4 journal空间也被挤占。这个场景用dumpe2fs -h或debugfs -R stats /dev/block/bootdevice/by-name/userdata能看到超级块信息。如果Free blocks和Free inodes都正常写入还是慢就要考虑碎片整理。ext4没有像老式ext2那种在线defrag工具唯一的在线整理手段是e4defrag不过Android的构建通常不带这个工具。解决碎片问题的最可靠手段仍然是定期离线整理或者在系统空闲时执行fstrim其实是TRIM操作对SSD回收块。下面会讲到为什么TRIM对Ext4在闪存上的表现很重要。3. 一个典型坑位/storage/emulated/0/Android/data/... 不可访问这个目录路径在热词里反复出现了好几周期几乎成了Android存储疑难杂症的代名词/storage/emulated/0/android/data/com.tencent.tmgp.sgame/files/pandora/pro /storage/emulated/0/android/data/com.omarea.gesture/cache/... /storage/emulated/0/android/data/com.android.gallery3d/files/thumbdb/photo...有人问为什么有时候用adb shell直接访问不了这些路径有时候明明adb能看到但应用逻辑找不到文件还有时候干脆ls报Permission denied。这种感觉像是“文件系统坏了”但很多时候根本就是设计如此、不是故障。3.1 FUSE 层与文件权限的“两张脸”Android从4.4开始用FUSE来做/storage/emulated/0的用户空间视图。系统在/data/media/0下真实存放文件同时保存了一个“挂载命名空间”下的/storage/emulated/0目录树。FUSE守护进程会检查调用者是不是有权限访问对应目录这个检查基于Android的GID机制每个应用会被分配到不同的GID比如sdcard_rw、media_rw、应用自身的GIDFUSE根据这些GID决定放行还是拒绝。所以出现opendir failed: Permission denied先别急着怀疑文件系统损坏先确认你有没有正确使用Android的存储访问模式。ADB shell里默认用户是shellshell这个GID对FUSE挂载点默认有读权限但写入往往受限。通过应用访问时应用必须申请READ_EXTERNAL_STORAGE或WRITE_EXTERNAL_STORAGE这类运行时权限而且新的Android版本引入了分区存储Scoped Storage路径访问限制更严格。想通过run-as调试某个应用的数据目录你会拿到的是/data/user/0/packagename的视角不是/storage/emulated/0/Android/data/packagename。经验是能通过FileManager正常看到的文件就代表FUSE层没问题应用内打不开多半是应用自己没处理分区存储的URI而不是文件系统挂了。3.2 “文件找不到”到底是谁的锅另外一个高频现象文件明明刚写进去应用却打不开或找不到。排查顺序如下用adb shell content query --uri content://...检查MediaStore索引是否已经更新。如果新文件不是通过MediaStore插入而是直接用File API写入索引可能延迟更新。某些场景照片、视频要用MediaScannerConnection.scanFile()主动通知。用adb shell run-as packageName ls -l /data/user/0/packageName/files/检查应用私有目录是否有文件。如果你的应用路径写的是context.getExternalFilesDir()它的实际路径可能是/storage/emulated/0/Android/data/packageName/files但如果你没有正确申请权限在Android 11以上连这个路径的访问也是受限的。检查是否应用崩溃后没来得及flush后面在第5章展开说。这里先记住Ext4的sync和fsync策略决定了“断电前写入了”和“断电后文件一定存在”是两回事。3.3 FileProvider的路径冲突热词里还有content://com.baidu.searchbox.fileprovider/baiddpath/android/data/com...这种内容URI。FileProvider会给不同应用暴露不同文件路径如果你host端的file_paths.xml配置不严谨就会出现能通过FileProvider拿到URI但实际打开文件时文件系统返回ENOENT的情况。具体排查手段adb shell dumpsys package packageName | grep -A 5 FileProvider或者直接在host应用的AndroidManifest里看FileProvider的meta-data配置确认external-path、external-files-path这些标签映射的路径。如果路径配置里的目录不存在FileProvider也能返回URI但contentResolver.openFileDescriptor会失败这跟“文件系统坏了”完全是两码事。4. I/O卡顿与system_server卡死的真凶journal、sync与TRIM线上移动设备反映“操作卡顿”“相册加载慢”很多都不是CPU问题而是I/O问题。再往下挖Ex4老系统的性能瓶颈往往集中在几个点日志提交锁journal、写放大、TRIM缺失导致的SSD垃圾回收。4.1 Ext4的journal到底是个什么东西Ext4日志journal相当于是“草稿本”。写文件时先往journal里记录元数据操作“我要把块A的内容改成B”然后才真正写数据。这样就算系统在写的过程中掉电恢复时也能根据journal回滚或重放避免metadata不一致。但这个“草稿本”有开销。每当commit一次就要等待I/O把日志块落盘。高频小文件写入时journal的提交延迟直接影响吞吐量和IOPS。Android上通常用ordered模式这是性能与安全的一个折衷选择数据和元数据必须按特定顺序落盘保证数据块先于元数据块。代价就是如果你写很小的文件可能因为等fsync而卡顿。如果遇到I/O明显慢可以用adb shell cat /proc/mounts | grep /data确认挂载参数。比如/dev/block/mmcblk0p44 /data ext4 rw,seclabel,relatime,dataordered,noresvport 0 0其中dataordered是常规选项。nodelalloc、datawriteback这些选项在性能上会有所提升但牺牲的是部分崩溃一致性不建议随意改。4.2 sync与fsync的心理预期Android应用的SharedPreferences在提交时用apply()是异步的commit()是同步的。异步就可能导致数据丢失。一个经典挂掉案例应用apply()写了一个配置用户马上强制断电配置最后丢失了。原因是apply()只是把更新放进了内存队列甚至没有主动调用fsync文件描述符。同样的道理可以解释“文件显示已下载成功但应用打不开”。如果你用的是第三方下载库但它没有在写入完成后调用fsync(2)或者MediaStore没有及时更新索引就会出问题。恢复经验是对关键数据文件、数据库务必在提交后显示调用FileDescriptor.sync()或FileChannel.force()。多花几毫秒换来的是稳定。4.3 TRIM对Ext4闪存表现的影响Android设备用的是eMMC/UFS闪存不是机械盘。但Ext4并没有主动告诉SSD“哪些块可以回收”它依赖TRIM命令。如果系统丢失了TRIM调度闪存内部的垃圾回收会把写放大推到很高的区间。Android从4.0时代就开始用/system/bin/fstrim做定时TRIM每日或每周空闲时执行。但如果你关屏策略太激进、storage模块卡死fstrim长期不执行长期运行后随机写性能会剧降。排查方法adb shell dumpsys mount | grep -i trim adb shell sm fstrim # 手动触发一次如果fstrim执行非常慢几十分钟说明SSD的饥饿块已经很多了擦写回收在后台大量进行此时必然伴随掉帧和IO卡顿。不是SSD坏了只是长期缺TRIM的必然结果。5. 掉电/异常重启后的文件消失ext4日志机制与孤儿文件机制做Android嵌入式调试时经常干一件事就是直接扣电池、断电源。很多工程师第一次遇到“重启后某个文件不见了”都怀疑是文件系统损坏但其实是Ext4自己处理异常掉电的方式。5.1 为什么文件在断电后丢失不一定代表文件系统坏了在Ext4 ordered模式下数据与元数据写盘存在先后。如果系统在写数据块后还没有提交对应的元数据事务那么掉电之后这个文件是不可见的。从用户视角看就是“文件没了”但在文件系统层面这属于正常的崩溃语义没落盘的数据本身就不该被期待恢复。真正的损坏是什么是元数据本身产生了不一致比如目录项指向了不存在的块、主索引被破坏、孤儿文件链断裂。这种情况下文件系统需要e2fsck修复。5.2 挂载失败或只读挂载是严重信号Android设备root后可以手动执行adb reboot # 或者直接adb shell mount -o remount,rw /如果开机后/data分区挂载失败会降级到/data临时文件系统或进入修复模式。此时日志里往往有EXT4-fs (mmcblk0p44): recovery complete EXT4-fs (mmcblk0p44): mounted filesystem with ordered data mode. Opts: errorsremount-roerrorsremount-ro很关键如果文件系统在运行时发生了错误会自动切换为只读防止进一步破坏。出现这种情况立即备份数据然后离线执行e2fsck。5.3 实操e2fsck在Android上怎么用Android的/system/bin/e2fsck是BusyBox或AOSP自带的简化版本。执行之前务必先卸载分区不要在挂载状态下修复adb root adb shell # 卸载 /data umount /data # 修复 e2fsck -fy /dev/block/mmcblk0p44注意-y表示自动回答yes-f表示强制检查即使文件系统标记干净。这个组合在设备已经降级到只读或卡在启动阶段时很有效。要是你还是害怕破坏用-n先做只读检查看输出再决定。5.4 “文件系统坏了吗”的第一瞬间判断遇到重启后文件丢失、应用数据损坏第一件事不是跑e2fsck而是看dmesg。因为e2fsck需要足够离线时间而排查也可能发现只是应用缓存没落盘。adb shell dmesg | grep -i ext4 adb shell logcat -d -b system | grep -i vold\|mountdmesg里面有EXT4-fs error、orphan file、remount-ro这类字样才真正需要进入修复流程。如果没有这些大概率是应用没做fsync导致的修代码比修文件系统更值得。6. 完整的排查工具箱从命令到思路的一站式梳理最后这一部分我把自己日常用的排查命令和操作顺序整理成一套流水线。这套流程我在多个项目里都用过覆盖了从应用层到底层的大部分存储问题。6.1 状态速查三板斧先花30秒看三个输出可以迅速缩小问题范围1. df -hT /data # 查块使用率与文件系统类型 2. df -i /data # 查inode使用率 3. dmesg | grep -i ext4 # 查有没有文件系统错误如果df显示正常、dmesg干净问题大概率在应用层策略或FUSE层。如果df异常立刻进入下一节。6.2 深挖一层超级块和块组信息用tune2fs -l看超级块tune2fs -l /dev/block/bootdevice/by-name/userdata输出重点看这几项Filesystem state: clean如果不是clean说明需要fsckFree blocks、Free inodes跟df做交叉验证Reserved block countFilesystem created判断这个分区多久没重建过了注意Android 9以上很多出厂data分区用的是F2FS不是Ext4。如果这里查到是F2FS所有Ext4用的大招都要换成fsck.f2fs。很多同事不知道这一点浪费了大量时间。6.3 块级检查与坏块闪存坏块在eMMC/UFS上虽然不多但不是没有。如果发现持续某个逻辑块读写延迟超高或者e2fsck反复报IO错误就该怀疑物理块了。查看EMMC/UFS健康状态的命令因平台而异cat /sys/class/mmc_host/mmc0/mmc0:0001/health # 部分平台 cat /sys/block/mmcblk0/device/life_time # UFS的寿命估算6.4 从系统角度排查I/O等待如果表现是“UI卡顿、postBootMessage迟迟不来”用top -H -p或perf确认是不是有线程在大量等待D状态不可中断睡眠然后看是哪个文件触发的adb shell echo 1 /proc/sys/vm/block_dump adb logcat -v time | grep --line-buffered -E READ|WRITEblock_dump会打印出每次块I/O的方向和设备帮助定位哪些目录产生了大量读写。6.5 分区层级与Android独有的 context check最后提醒Android的/data分区启用file-based encryption(FBE)之后同样一块文件系统不同用户下的加密context不同。如果应用无权访问另一个用户的/data/user/1/xxx即使VFS权限全放行也读不出来。热词里的/storage/emulated/0/android/data/...和content://路径冲突多数都绕不开FBE的user_id隔离。dumpsys user可以查看用户空间ls -lZ /data/user/0/xxx可以看security context。如果context不对e2fsck也不会帮你解决因为这是文件系统之上的一层抽象。7. 写在最后一个压箱底的建议排查Android文件系统问题最大的陷阱不是你不够熟悉Ext4而是你把所有问题都当成Ext4问题。先分清VFS层、FUSE层、应用层、加密层四个层面的边界再决定要不要深入block层。这一条原则能帮你省下大量无效操作。实际处理中我见过太多人在df和e2fsck之间反复折腾最后发现不过是应用忘了fsync。相反真正掉电丢数据的案子又常常因为急于用e2fsck -p自动修复反而弄丢了原本可以手工抢救的孤儿文件。根据个人经验动手修复前先dd一份分区镜像到外部存储永远是性价比最高的保险。多花几分钟备份可能省下后面几天的恢复工作。希望这套经验对需要跟Android存储死磕的同行们有点帮助。
返回列表