ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Android Ext4文件系统问题排查:从VFS到CPU 100%的实战指南

Android Ext4文件系统问题排查:从VFS到CPU 100%的实战指南 如果你在搜索框里敲“Android-Ext4文件系统问题排查”大概会看到一整套熟悉的搜索结果/storage/emulated/0/Android/data/进不去、FileProvider 报错、嵌入式Linux开机卡在 VFS、线上服务器 CPU 100%……这些年来我几乎每个都当过“受害者”。印象最深的一次联调甲方拿来一台工程机说“有些游戏App数据备份不了连了USB能看到目录名字但一点进去就报无权限”。我先看了一眼路径/storage/emulated/0/Android/data/com.xxx.game/files/...心里已经知道大概率不是权限开关没开的事而是Android存储模型在文件系统层面的隔离机制在作祟。后来从 dmesg 里的 SELinux avc 日志验证了猜想。从那以后我把这类问题的排查流程固定成了一整套方法今天借这篇博文完整梳理一遍。本文围绕 Android 上 Ext4 及周边存储机制来展开覆盖用户目录访问异常、FileProvider 分享失败、嵌入式 rootfs 挂载/NFSv3/sync 和 VFS 报错以及服务器“CPU 跑满”时如何从文件系统和 I/O 层面入手定位。适合 Android 应用开发、系统开发、嵌入式 Linux 和运维方向的同学参考我把每一步判断逻辑和实际心得都写在下面方便大家直接对照排查。1. 排查前先把三张“图”装进脑子Ext4、FUSE、VFS怎么分工1.1 分区类型别想当然同一台设备可能同时存在Ext4、F2FS、EROFS“Android-Ext4”这个组合词很多人第一反应是“Android 不是早换 F2FS 了吗”。这话说对了一半。的确近年主流设备把/data分区换成了 F2FS因为 F2FS 对 NAND 随机写的适配更好。但一台手机上/vendor、/odm、/cache、部分平台的/metadata和/persist依然大量使用 Ext4老平台、低端平台、车机方案上/data也经常是 Ext4。也就是说你会同时遇到好几种文件系统“遇到问题先确认它到底是不是 Ext4”这个习惯很重要。在设备上跑这个命令最直观adb shell df -T Filesystem Type 1K-blocks Used Available Use% Mounted on /dev/block/dm-0 ext4 89289264 12098448 77190816 14% /vendor /dev/block/dm-5 f2fs 107374168 40291984 67082184 38% /datadm-0这种名字来自 device mapper也就是 Android 动态分区机制。Android 10 之后 system、vendor、product 被塞进一个叫super的大分区开机再切成多个逻辑卷。所以你在日志里看到的块设备是dm-x而不是mmcblk0pX。真正溯源时用ls -l /dev/block/by-name/去找“哪个名字对应哪个 dm”再用/proc/partitions结合 df 的挂载点判断当前分区属于哪个逻辑卷。不少排查失败就是死盯着/dev/block/mmcblk0p5这种静态分区名结果动的其实是一个早已不存在的分区。顺手把 by-name 列表保存一份后面填 fstab、写 OTA 脚本都用得上。1.2 用户可见的“机体存储”其实经过了FUSE这层代理为什么/storage/emulated/0/Android/data/包名/files的路径看起来在手机上存在用文件管理器却访问不了一个关键原因是这里不是直接挂载的 Ext4 路径而是经过 FUSE用户态文件系统模拟出来的“媒体存储视图”。底层真实路径是/data/media/0。FUSE 进程会拦截open/read/write/stat/chmod/chown等一组系统调用再映射到/data/media/0的对应操作。设计目的是多用户隔离和权限模拟——让 App 以为自己在读 SD 卡但实际由系统托管。弊端是你在 shell 下觉得“我在 root为什么不能 chmod”因为 FUSE 那一层对操作的人员认证和权限位做了二次校验并且 Linux VFS 对某些挂载选项如nosuid,nodev,noexec也会直接按住。另外Android 11 之后连“历史遗留的外部存储权限开关”都不好用了。网上“Android 12 适配”相关问答里经常有人提requestLegacyExternalStorage不行了就是因为 targetSdk 30 时这台“FUSE 代理”的过滤规则变了。所以别再用“上溯兼容老权限”的思路解决问题得按新规则把文件挪到正确目录。1.3 VFS是中间调度层也是报错密集区VFS 是“虚拟文件系统”层你可以把它理解为逻辑上所有文件系统之间的公共接口。open(/sdcard/xx)走到内核后不直接落进 Ext4而是先到 VFS由 VFS 查命名空间、解析 dentry/inode、判断权限再根据挂载点把请求分发给具体文件系统驱动。排查中频繁出现的VFS: Cannot open root device就是在 VFS 这层发生的。它表示 VFS 已经去解析了root启动参数但没有找到对应的块设备或者找到了但驱动不识别其中的文件系统。所以看到这个报错不要直接去重做文件系统先检查三件事设备驱动加载没有、分区号对不对、bootloader 传给内核的root与实际分区是否匹配。只要这三件事有一件错了后面 fsck 重建十遍也无济于事。2. 日常踩坑第一线/storage/emulated/0/Android/data与FileProvider报错2.1 “App里打不开预览下载的文件又找不到”的典型成因“App 里既打不开预览下载的文件系统又找不到”这个句式你在各类论坛里应该经常看到。拿游戏目录举例很多应用会把下载的资源放进/storage/emulated/0/Android/data/com.xxx.game/files/pandora/这种路径。这个路径在 root shell 里是可见的USB 调试时 MTP 也能看到“目录存在”但普通用户去文件管理 App 里打不开连上电脑也不一定拷得出来。原因是/storage/emulated/0/Android/data/包名/属于 Android 定义的“App 专用外部存储”。应用自己可以读写第三方应用默认不能碰。Android 11 开始系统对这个根目录的隔离进一步收紧甚至有厂商在 MTP 协议层做了一层掩藏用户看到的目录是“空”的或者直接报无权限。所以开发者要记住一条铁律凡是用户最终要带走、要分享、要二次编辑的文件放到/sdcard/Download/、/sdcard/DCIM/这类公共媒体目录不要放进 Android/data。游戏资源包、缓存、临时解压文件则可以放但别指望用户能自己备份。2.2 FileProvider的“路径声明”和“URI授权”两步都容易错搜索热词里频繁出现content://com.baidu.searchbox.fileprovider/baiddpath/android/data/...、content://com.tencent.wework.fileprovider/external_path/android/data/...这类以 FileProvider 开头的分享路径。这类问题多半是想通过 FileProvider 直接分享/Android/data/下的文件。但 Android 11 默认不允许第三方跨包读取该目录所以即便 URI 生成了接收方的ContentResolver.openFileDescriptor仍然可能抛SecurityException。正确做法分三步第一步在file_paths.xml里声明合适的根路径paths files-path namefiles path. / cache-path namecache path. / external-files-path nameext_files path. / /paths第二步分享前先判断目标文件位置。文件在getExternalFilesDir(null)下的downloads/目录时用external-files-path在getCacheDir()下时用cache-path。然后FileProvider.getUriForFile(context, getPackageName() .fileprovider, file)生成 URI。第三步手动授权。用Intent.setClipData搭配grantUriPermission或者直接用Intent.FLAG_GRANT_READ_URI_PERMISSION。很多分享失败不是 FileProvider 配置问题而是忘了加授权 flag。如果外部 App 依然打不开看 logcat 里有没有Not allowed to read或Permission Denial有就把文件复制到自身 cache 目录再分享一次。这招在处理“从 Android/data 上传到预览”时最可靠。2.3 unable to chmod报错从SELinux与挂载选项两个方向查unable to chmod /storage/emulated/0/android/data/com.xxx: Operation not permitted这个报错在 root 过的手机上尤其常见。很多人第一反应是反复执行 chmod但真正的原因往往不是权限位不对而是/storage/emulated/0走的是 FUSE 或 sdcardfs这层不接受你随便 chown/chmod 别的应用目录SELinux 对某些标签如u:object_r:app_data_file:s0有明确的写限制。我的排查顺序是ls -ldZ查看目标目录的所有者和 SELinux 标签mount | grep -E sdcardfs|fuse|emulated看挂载选项确认是否有nosuid,nodev,noexec限制dmesg | grep avc | tail -n 20确认有没有被 SELinux 拒绝如果是 shell 操作受限用adb shell run-as com.xxx切换到应用身份再看能否读取或修改。如果确实是为了备份数据可以先把文件 copy 到/sdcard/Download/或者直接走系统自带的“备份到电脑”功能。我在项目里一般不会用chmod -R 777这种操作它表面改对了后续容易把 SELinux 标签弄乱甚至触发系统重新标号问题更大。3. 嵌入式视角rootfs挂载、NFSv3、sync、VFS3.1 根文件系统启动失败时先给“VFS报错”分诊嵌入式 Linux 开发里VFS: Cannot open root device应当是几乎人人都见过的开机报错。我见过有同事一看到这个就挂载重烧镜像结果折腾半天没动静。后来仔细观察发现是 kernel cmdline 里的rootmmcblk0p3与 bootloader 实际烧写分区对不上。建议分诊三步dmesg | grep mmc查看 MMC 探测情况确认物理设备有没有被内核枚举出来cat /proc/partitions看在内存设备上能看到的 主:辅 设备号和大小判断分区是否存在blkid /dev/block/mmcblk0pX看文件系统 UUID、type 是否正常。在启动早期root指向的块设备若未建立就会出现 VFS 报错。这时不要急着重建文件系统先检查rootwait参数是否存在。很多 MMC 卡、U 盘在启动时初始化较慢加一个rootwait就能解决。这个点特别容易被新手忽略。常见现象和对应方向可以这样快速归类启动报错优先怀疑方向VFS: Cannot open root device驱动没加载、分区号不对、rootwait 缺失EXT4-fs: unable to read superblock分区偏移不对、镜像损坏Kernel panic - not syncing: VFSinitramfs 与根文件系统类型不匹配3.2 NFSv3挂载rootfs的参数与权限坑用 NFS 作为开发期根文件系统是嵌入式里很常见的做法修改 rootfs 不用反复烧卡。常见方式是在 uboot 或 extlinux.conf 里传root/dev/nfs nfsroot192.168.1.10:/srv/nfs/rootfs,v3,tcp,rsize8192,wsize8192 ipdhcp rw其中v3代表使用 NFSv3。如果不指定内核可能默认尝试 NFSv4而服务端没有配 v4 就会报Connection refused或Access denied。容易踩的三个坑服务器/etc/exports里的root_squash会把目标板上的 root 用户映射成 nobody权限维持成nfsnobody导致板上的 root 也写不进去。开发期可以写no_root_squash但仅限内网生产镜像千万不要这么写。板子通过 NFS root 引导后网络中断会让上层直接“卡死”表现为所有访问根文件系统的进程进入 D 状态。现场要评估是否加hard,intrNFSv3 支持intr选项或调整tcp/udp。烧到 NFS 的 rootfs 若由 Ext4 组成实际写落在服务器磁盘上所以在板子上 sync 只是把页面缓存发到 NFS 客户端数据最终落盘时间还与服务器侧有关。要验证“重启不丢”最好在服务器端sync一次再确认。3.3 sync、fdatasync、fsync到底谁负责“保命”如果你在嵌入式板卡上执行了 sync掉电后文件还是少了第一反应别骂 Ext4先看挂载参数。Ext4 有 delayed allocation 机制write() 返回不代表数据已进磁盘sync 会强制将脏页写回但若底层块设备本身有 write cache则还需要一次 cache flush。这个链条上任意一层断了断电丢数据的概率都不低。要查当前挂载参数mount | grep -E / | /data dmesg | grep EXT4-fs | tail如果看到dataordered含义是数据在日志提交前要写回数据块整体一致性较好但性能略有损耗如果看到datawriteback断电丢失数据的概率更高。需要可靠性的场合应显式指定-o dataordered并适当调短commit5这类日志提交间隔同时确认块设备支持 cache flush 命令。另外还要认识到sync命令通常可以理解为“把系统里所有脏页刷一下”但它不等于fdatasync(fd)后者只保证某个文件的文件数据落盘不刷无关页。嵌入式场景里要想稳核心数据落盘得在进程里显式调用 fsync/fdatasync而不是“退出前调一次 sync”。4. “CPU 100%”排查链路I/O、等待和块设备4.1 先拆“CPU 100%”的用户态、内核态、等待时间线上服务器的 CPU 跑满是运维排障的经典题但“文件系统问题”常常被遗漏。拿到机器后第一件事不是看火焰图而是用 top 看 CPU 行top -n 1 %Cpu(s): 35.2 us, 12.3 sy, 46.8 wa, ...us高多为业务进程计算、GC、锁竞争sy高内核态耗 CPU可能涉及系统调用频繁、磁盘调度、网络协议栈wa高CPU 在等待 I/O很可能与文件系统、块设备相关。如果wa轻轻松松超过 60%那就要开始查文件系统如果 us 高那就去抓用户态进程栈不要先怀疑磁盘。很多人在 CPU 100% 时直接重启等于把现场证据全丢了非常可惜。4.2 从iostat到进程级定位的完整链路首先查块设备有没有排队iostat -dx 1 5 Device: tps MB_read/s MB_wrtn/s ...重点看avgqu-sz平均队列长度和%util。如果%util接近 100% 而 tps 不算高可能是有大块顺序写或磁盘本身状态差。之后落到进程pidstat -d 1 3 iotop -oP再深入一层看目标进程的 fd 里是不是有删除了但还打开的文件lsof -nP | grep deleted ls -l /proc/pid/fd | grep deleted这种 deleted fd 非常坑文件不再出现在目录里但 inode 还被进程占着磁盘空间不会释放。很多时候“文件系统找不到”其实是这类空间泄漏造成写满假象应用创建新文件时报No space left on device用户感知就是各种异常。4.3 遇到Ext4报错后的救援姿势如果 dmesg 已经出现EXT4-fs error (device dm-0)不要反复重启建议先确认完整报错然后尽快做只读挂载或直接断电保护数据。常见步骤先用mount -o remount,ro /data强制只读若可能备份数据rsync、dd到安全位置e2fsck -fy /dev/block/...修复前先把该分区卸载修复后检查挂载参数把errorspanic改回errorscontinue或errorsremount-ro。我个人经验最忌讳的是看到EXT4-fs error就慌立刻在挂载状态下跑 e2fsck 或者 mkfs。那样轻则修南辕北辙重则把还在用的 inode 也搞坏。现场数据恢复永远优先于修复速度。5. 我日常用的“采集五件套”与几条不碰红线5.1 Android设备现场先固定证据再做操作Android 设备出问题尤其像“文件系统无法写入”“解锁后数据消失”我最先抽五个命令的结果存证adb shell date 00_time.txt adb shell dmesg 01_dmesg.txt adb shell logcat -d -b all 02_logcat.txt adb shell cat /proc/mounts 03_mounts.txt adb shell df -T 04_df.txt adb shell ls -lZ /storage/emulated/0/Android/data/ 05_selinux.txtdmesg负责找文件系统层和 SELinux 拒绝logcat找 App 层异常/proc/mounts看当前挂载选项df -T看是否空间或 inode 满ls -lZ看目录 SELinux 标签。这五样一组合多数权限类问题都能定位到具体层而不是凭空猜“是不是机器坏了”。服务器端也一样先留存dmesg、iostat、sar、系统日志的时间窗口数据再考虑动文件系统。没有现场证据的修复经常是修了这次下回又复发。5.2 删库跑路边缘的几道红线不要对已挂载分区直接 fsck。除非你非常明确该文件系统已处于只读或异常状态否则先 umount不要用 chmod -R 777 来“解锁”Android/data 目录。它改动所有权和标签更容易让应用崩溃不要在 NFS 客户端上反复执行rm -rf清理 rootfs而不同时关注服务端。NFS 服务端磁盘满时执行清理可能长时间无响应不要在线上直接 mkfs。如果需要重建先dd一份镜像到其他盘保存现场再动手不要忽略dumpe2fs -h里的Filesystem state状态。clean 不等于数据完好需要时先e2fsck -n只读检测。5.3 把排查顺序刻进肌肉先看、再动、最后修我把整个流程总结成“先看、再动、最后修”先看收集 dmesg、mount、df、iostat、SELinux 日志明确问题层次再动用只读方式验证能 remount ro 就先 ro能备份就先备份最后修确认根因后才执行 fsck、chmod、清理或恢复。这个顺序在 Android-Ext4 文件系统问题、嵌入式 rootfs、服务器高 CPU 场景都适用是我踩过不少坑后沉淀下来的。问题往往不复杂复杂的是在没有证据的情况下动手然后花几倍时间收尾。至少对我来说能稳定复现的现场比任何猜测都更有说服力这也是排障这件事最值得认真对待的地方。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进