
说起 Android 上的 Ext4 文件系统问题排查我一年里至少会遇到几十次类似求助手机里某个游戏的日志目录/storage/emulated/0/Android/data/...路径明明还在点进去就报“无法访问”某银行 App 下载的文件App 里能打开去系统文件管理器里死活找不到开发调试时 adb 里执行 chmod直接给我弹一个Operation not permitted。这些现象表面看都像“文件系统坏了”但真正动手查下来十有八九是 Android 存储分层、权限模型和挂载方式之间的错位。这篇文章我想按三个视角来拆普通用户遇到“文件看不到”该怎么查开发者遇到“Permission Denied”该怎么想以及真正进入内核层后ext4 本身的故障要怎么定位。无论你是手机用户、App 开发还是嵌入式 Linux 从业者这套排查思路应该都能用得上。先说明白一点大多数问题根本不需要格式化分区更不需要重装系统你需要的是先搞懂这一层层的“门”到底开在哪。1. 先想清楚Android 的 Ext4 在哪一层1.1 分区布局里ext4 真正的“主场”是 /data先别急着进 recovery 跑 fsck我建议先在 shell 里看一眼分区挂载表。Android 设备本质上是一个 Linux 系统常见的 system、product、vendor、cache、userdata 分区里系统分区很多用只读 ext4 或 erofs而日常数据所在的 userdata 分区通常才是真正的可写 ext4 主场部分新机会用 f2fs。用一条命令就能看adb shell cat /proc/mounts | grep -E ext4|f2fs|fuse输出一般长这样/dev/block/dm-3 / ext4 ro,seclabel,relatime 0 0 /dev/block/dm-8 /data ext4 rw,seclabel,noatime,discard 0 0 /dev/fuse /storage/emulated/0 fuse rw,nosuid,nodev,noexec,noatime,user_id1023,group_id1023 0 0看到/dev/block/dm-8 /data ext4 rw...这样的行说明 userdata 分区按 ext4 挂载。如果手机是 f2fs你会在/data那行看到 f2fs。标题里写 Ext4实际排查思路是通用的但 ext4 的日志、保留空间、fsck 行为有它自己的脾气后面会专门说。很多用户跑来问“我的 ext4 分区是不是坏了”我第一反应是反问你到底在/data上做过什么操作如果只是文件管理器里看/sdcard那根本没碰到 ext4 那层至少别先甩锅给文件系统。1.2 你看到的“sdcard”其实不是一块独立磁盘这是 Android 存储上最容易产生误解的地方。手机上“文件管理”里显示的/storage/emulated/0或者老代码里的/sdcard并不是一个独立分区。在多数现代 Android 设备上这部分内容实际存放在 userdata 分区的/data/media/0目录对外通过 FUSE用户态文件系统挂载成一个“假的外置存储”。为什么要绕这一层因为 Android 要给你一种“类似 SD 卡路径”的体验但又不想让 App 直接碰到底层 ext4 的 inode 权限。FUSE 挂载后的目录owner、permissions 都是模拟出来的。你看到/storage/emulated/0/Android/data/xxx是drwxrwx--x可底层/data/media/0/Android/data/xxx的真实属主可能属于某个 app uid两者不是一回事。因此很多人会做一件徒劳的事在文件管理器里给/storage/emulated/0/...下的文件改权限改了半点下一个进程打开还是拒绝。不是你操作不对是这一层根本不该由你手动改——它由系统存储框架统一管理。1.3 一次 Permission Denied 背后至少有两三层拦路命令行里看到Permission denied第一反应不该是“文件系统坏了”。在 Android 上请求从 App 发出去会依次经过 SELinux、FUSE 层的 UID 检查最后才落到 ext4 的 inode 权限上。每一层都可能拒绝你。我的快速判断套路是这样报错特征优先怀疑对象下一步动作Permission deniedSELinux 或 FUSE 权限模拟logcat查avc: denied或看挂载参数Read-only file systemext4 被强制只读或磁盘错误查dmesg里的EXT4-fs errorNo space left on deviceblock 或 inode 耗尽df -h与df -i对比No such file or directory路径映射或 FileProvider 配置核对真实路径和 XML 映射这层“先归因、再动手”的思路后面几章会反复用到。先别急着格式化你的数据往往没坏只是门走错了。2. 用户视角文件“找不到、打不开、删不掉”排查2.1 文件“消失”先验证它是真没了还是你看不到典型例子某个银行 App 里下载了一笔交易的 PDFApp 内能预览去系统文件管理器里却找不到。这种问题核心在 MediaStore 索引。文件落地到/sdcard/Download/后系统媒体库如果没有及时扫描文件管理器的“最近文件”列表里自然不出现但文件实体是存在的。验证方法很直接在电脑上开 adb 看一眼底层目录adb shell ls -l /sdcard/Download/如果能看到文件说明只是媒体库没刷新。我遇到比较顽固的情况是某个目录里放了.nomedia文件那么整个目录都会被媒体库忽略。这个文件不会删除你的照片但足以让所有 App 的“图片选择器”“文档选择器”里看不到内容。处理方式是删掉.nomedia后重启手机或者在开发者选项里触发媒体库重新扫描。如果 adb 也看不到文件那才轮到怀疑文件系统层。可以继续用find在/sdcard下搜文件名缩小范围。命名越长、隐藏目录越深越容易被“找不到”但极少真的是分区崩了。2.2 Android/data 进不去是系统限制不是文件坏了这两年问得最多的一句话“我的/storage/emulated/0/Android/data/某游戏/files/...目录打不开了是不是分区坏了” 不是。Android 11 开始强化了分区存储普通文件管理器和非系统 App 默认访问不了Android/data目录。你用系统自带的文件管理器在少数机型上能看到但里面多数子目录打不开报Operation not permitted这是设计如此。这个目录里装的是应用私有扩展文件典型的是游戏资源、日志、缓存。想读取里面的东西正规渠道有几种一是应用自己提供文件分享功能二是通过 USB 连接电脑在 MTP 模式下部分手机允许浏览Android/data三是开发者用 adb 以应用身份执行run-as仅限 debug 应用。我处理过很多用户卡在“一定要用文件管理器点进去看”的执念里其实换个方式导出就完事了。文件系统本身好得很别总往“分区损坏”上想。2.3 空间明明清了df 还显示满inode 和保留空间还有一种情况手机提示存储空间不足删了一堆视频设置里显示剩了十几个 G但系统继续报“无法写入”。这种大多不是错觉而是要查两层东西文件系统 inode 和已删除但未释放的文件。ext4 上No space left on device不一定代表磁盘 block 满了inode 耗尽也会这样。小文件特别多的时候比如某个日志目录疯狂生成文件block 没用完inode 先用完。在 adb 里看adb shell df -h /data adb shell df -i /datadf -i里IUse%如果接近 100%说明 inode 耗尽。常见“凶手”就是日志目录比如/data/media/0/Android/data/com.mi.health/files/log/这类路径下堆了大量碎文件。清掉一些旧 loginode 释放后空间就会恢复。另一个隐蔽因素某些进程仍持有已删除文件的文件描述符。文件被删除后只要进程不关闭句柄占用的空间就不会真正释放。可以执行ls -l /proc/*/fd/* 2/dev/null | grep deleted找出这类“僵尸文件”。普通用户遇到这种问题最简单的办法是重启相关应用让进程释放句柄。2.4 给用户侧的三板斧排查命令考虑到不是每个人都会 adb我一般建议按顺序做三件事先用系统文件管理器的“显示隐藏文件”开关看目标目录是否被.nomedia遮蔽换 MTP 模式连电脑直接看 USB 挂载出来的目录绕过手机文件管理器的限制还不行就用 adb 执行ls、find、df -i确认到底是“不存在”还是“无权限”。如果你连电脑也没有至少记住一个判断文件能否被某个 App 打开、能否被分享出来比“文件管理器里能不能看到”更重要。前者通畅说明数据还在只是索引和权限没对上不用焦虑。3. 开发者视角Permission Denied 和文件系统权限模型3.1 想读应用私有目录run-as 才是正道开发时经常要查应用自己的文件有没有写对位置。应用私有文件在/data/data/包名/下普通 adb shell 用户没权限但如果你开发的应用是 debuggable 的可以使用run-as以该应用 uid 执行命令adb shell run-as com.example.app ls -lZ /data/data/com.example.app/files/加了-Z能同时看到 SELinux 标签我强烈建议每次排查都带这个参数。如果run-as都提示Package xxx is not debuggable说明你拿到的是 release 包想访问私有目录只能靠 root 或让 App 自己导出日志别再和权限模型硬刚了。很多人喜欢adb root后直接ls /data/data那是另一条路。但 root 权限在 SELinux enforcing 状态下也不是万能的某些带untrusted_app标签的文件即使 uid 是 root访问时也会被拒绝。看到Permission denied先想想是不是 SELinux 在拦截。3.2 为什么 chmod 会报 Operation not permitted开发阶段特别常见的一个报错“unable to chmod /storage/emulated/0/Android/data/xxx: operation not permitted”。我看到这条的第一反应就是你是不是在外部存储上用 shell 去改别的 App 目录的权限外部存储的 FUSE 模型里每个文件在“虚拟视图”中属于某个 app uid。你当前 shell 的 uid 通常是shell或media_rw并不是这个文件逻辑上的 ownerFUSE 层就直接丢一个EPERM给你。这不是 ext4 的问题是 FUSE 的权限模拟规则。开启 SELinux 的设备上可能同时还存在avc: denied { setattr }的记录两个原因叠在一起表现就是一行模模糊糊的Operation not permitted。正确的做法是不要手动 chmod而是让文件按照 Android 的存储 API 去创建和管理。保存媒体文件用MediaStore选择文件用 SAFStorage Access Framework里的ActivityResultContracts.OpenDocument()跨应用传文件用 FileProvider。外部存储本来就是“按 App 隔离”的非要手工改权限改变不了这个设计。3.3 FileProvider 路径映射容易栽在 Android/data 上Android 7.0 之后应用间传一个file:///URI 会直接崩FileUriExposedException所以大家普遍转投 FileProvider。FileProvider 配置里的paths决定了你可以向外暴露哪些目录。网上能看到很多类似content://com.xxx.fileprovider/...的 URI不是所有都能访问成功。我的一个真实经历某个日志分享功能把文件写在应用私有目录files/下但file_paths.xml里只配置了cache-path。App 自己读没问题其他 App 打开分享出来的 URI 就报FileNotFoundException。原因很简单FileProvider 要按照 paths 配置把 URI 的虚拟路径映射回真实路径你只声明了 cache它就不知道该把files翻译成什么。另一个常见坑是映射到Android/data下的文件。Android 11 之后即使你是 target 较高的应用外部 App 想读取/sdcard/Android/data/别的包名/依然受限这时候 FileProvider 也救不了你因为受限制的是底层存取路径不是 URI 格式。解决方案是把共享文件放进files-path或cache-path避免把“共享文件”放在Android/data那个隔离区。3.4 跨应用共享文件别再用 Android/data 当传话筒以前很多人开发时图省事两个 App 约定一个目录/sdcard/Android/data/com.a/某缓存目录/A 写B 读。Android 10 之前勉强能跑Android 11 之后直接失灵B 连目录列表都拿不到。这套路已经过时了别再基于它做设计。正确做法按场景选两个自己开发的 App 之间共享小数据自定义 ContentProvider或通过ContentResolver调用系统的MediaStore落一个文件再通过 URI 授权访问分享给外部 AppFileProvider 加Intent.FLAG_GRANT_READ_URI_PERMISSION并把文件放在私有目录或媒体库需要长期保存、让用户自己管理走 SAF让用户选目录你只管读写返回的content://URI。核心原则一句话不要试图绕过 Android 的权限模型它只会越来越严。文件系统往往没坏是开发思路要跟着系统版本走。4. 内核层Ext4 本身出问题时的排查手段4.1 开机卡动画或反复重启先把 dmesg 捞出来用户层和开发者层的锅排完之后终于轮到 ext4 本身。如果设备开机卡在 Logo、反复重启、或者桌面起来了但大量 App 报“只读文件系统”这时才真正进入内核级排查。Android 的文件系统异常一般会在内核日志里留下痕迹。没有串口日志时常见方式是进 Recovery 后用 adbadb shell cat /proc/last_kmsg或者直接抓完整 dmesgadb shell dmesg | grep -i -E ext4|f2fs|jbd2|I/O error常见的信息大概这几类EXT4-fs error (device dm-8): ext4_find_entry: ...目录项读取异常文件系统检测到不一致JBD2: Spotted dirty metadata buffer日志层发现有数据不符合预期Remounting filesystem read-only内核判断错误严重主动把分区切成只读防止继续写坏。这里有个重点当你看到Read-only file system时盘不一定物理坏了但内核已经决定自我保护。这时候你直接往里面写文件一定失败可别再用“权限”去解释更不要再试图mount -o rw,remount强改。第一步永远是确认真相错误发生在哪个分区、是否反复出现、最近有没有异常断电。4.2 fsck 的操作纪律先备份再离线执行如果你的手机能进 Recovery界面里往往有“清除数据”类操作但那个是双清不是 fsck。要真正修 ext4需要在分区未挂载状态下跑e2fsck。adb 进 recovery 后如果 userdata 是 ext4可以尝试adb shell e2fsck -fy /dev/block/by-name/userdata但这里我要非常慎重地说不要在任何分区处于挂载状态时执行 fsck。Android 正常开机时/data是挂载着的直接跑 fsck 会撞上 ext4 日志重放逻辑轻则无效重则把没落盘的数据搞乱。正确的前提是 bootloader 或 Recovery 阶段分区未挂载比如先重启进 Recovery再执行只读检查e2fsck -n看看问题有多大。我自己的纪律是三步走能导出数据就先把关键分区内容备份出来尤其用户照片、聊天记录先e2fsck -n只读检查看它报告多少个 inconsistent、多少个 orphan确认可以修复时才e2fsck -fy同时做好数据损失的心理预期。-y表示对所有问题自动回答 yes省事但危险。fsck 不是万能药它能重建空闲空间链表、清理错误位图但已经覆盖掉的目录项找不回来。4.3 ext4 日志与高 CPU / IO 飚高的排查思路很多人会搜“线上服务器的 CPU 使用达到 100% 了如何排查、定位和解决该问题”。这套思路同样适用于 Android 和嵌入式 Linux。只要性能问题集中在文件系统层最终大概率会碰到jbd2进程。ext4 使用日志记录元数据操作事务提交要由 jbd2 线程完成。设备 IO 很慢、磁盘队列堆积的时候进程等待 IO 的时间占比上升CPU 使用率难看反而是次要的。排查顺序我给一个固定套路top -H -p 主进程PID # 看具体线程名和运行状态 pidstat -d 1 # 看每秒块设备读写 iostat -x 1 # 看设备 util/await dmesg | tail -n 500 # 看是否有 IO 错误导致重试看到名字类似jbd2/dm-8-3的线程在 D 状态高居不下说明 ext4 在等底层 IO 完成。这时候你不是去“杀进程”或“清缓存”而要判断是不是存储介质写入异常是不是文件系统快满了导致分配路径变慢调优方向可以是挂载参数nobarrier有风险别盲上、迁移到 f2fs、换用更好的 UFS/SSD。这个排查链路和线上服务器 CPU 100% 的处置几乎是同一条方法论先定位线程再分析它卡在哪个内核函数最后才动配置。4.4 根文件系统挂载失败从 Android 串到嵌入式 Linux做嵌入式 Linux 开发的朋友经常遇到“嵌入式 Linux 根文件系统挂载使用 NFS v3”的场景。开发阶段为了快速调试板子在 uboot 里指定root/dev/nfs nfsrootserver_ip:/path,vers3,tcp希望内核通过 NFS 挂根文件系统。如果起不来dmesg 里通常会看到VFS: Unable to mount root fs via NFS。问题往往不在 ext4而在内核配置和 Server 配置内核要开启CONFIG_ROOT_NFS、CONFIG_NFS_V3等光有网络驱动不够Server 端的/etc/exports要允许对应网段访问而且一般得带no_root_squash和rw否则 root 用户在板子上写入时会被压缩成匿名用户权限板子启动日志里先看到IP-Config: Complete再有 NFS 挂载尝试如果没有拿到 IP说明网络配置先失败了。如果是真正的 ext4 根文件系统起不来则检查 cmdline 里的root/dev/mmcblk0p2是否匹配实际分区以及有没有给rootwait。必要时用init/bin/sh进 shell 手动挂载看具体报错。这个小技巧能帮你把“内核 panic”和“文件系统真的坏了”快速区分开。顺带提一句littlefs。很多单片机项目用 PlatformIOboard_build.filesystem littlefs配置的是小容量 Flash 上的文件系统。它没有 ext4 那么重的日志和块管理主打掉电保护和磨损均衡。和 ext4 没有谁更好只看场景。如果你发现 littlefs 挂载后文件丢失优先检查分区起始地址、擦除块大小以及是否在写入过程中断电而不是照搬 Android 的 e2fsck 思路。5. 问题排查速查表与工具箱5.1 症状、原因、对策对照表症状常见原因处理思路文件管理器看不到刚下载的文件MediaStore 未索引或.nomedia遮蔽底层ls验证实体删.nomedia后重启/storage/emulated/0/Android/data打不开分区存储限制/FUSE 权限用 MTP、run-as或备份工具导出chmod 返回Operation not permitted非 owner FUSE/SELinux 拦截改用 MediaStore、SAF、FileProvider系统提示Read-only file systemext4 错误后被强制只读查 dmesg进 Recovery 离线 fsckNo space left on deviceblock 或 inode 满df -h/df -i清理日志开机卡 Logo 反复重启分区损坏或 fsck 卡死抓 last_kmsgRecovery 中e2fsck -n根文件系统挂载失败cmdline、内核模块、NFS exports查 dmesg核对启动参数jbd2 线程高 CPU / IO 队列堆积底层存储变慢或文件系统压力大pidstat/iostat/perf定位后再调优5.2 最常用的命令工具箱# 看挂载参数和文件系统类型 adb shell cat /proc/mounts | grep -E ext4|f2fs|fuse # 看空间和 inode adb shell df -h /data adb shell df -i /data # 看目录真实大小 adb shell du -sh /sdcard/Android/data/com.example/ # 看 SELinux 标签 adb shell ls -lZ /sdcard/ # 看内核日志 adb shell dmesg | grep -i -E ext4|jbd2|I/O error # 看应用私有目录debug 应用 adb shell run-as com.example.app ls -lZ files/ # 文件系统只读检查 e2fsck -n /dev/block/by-name/userdata # 定位高 CPU/IO 线程 top -H -p pid pidstat -d 1 iostat -x 1这些命令我平时写在一个脚本里出问题先全跑一遍输出结合时间点交叉看能省很多时间。5.3 实测中最容易踩的三个坑第一个坑分区还挂载着就敢跑e2fsck。哪怕是 Recovery 里也要先确认分区没被自动挂载。直接跑修复运气好报个 “Cant read superblock”运气差能把日志回放搞乱数据更麻烦。第二个坑以为/sdcard就是一块 ext4 分区直接在 FUSE 层改权限、改属主。改了半天没效果又开始怀疑文件系统损坏。实际上你只是在“虚拟视图”上做的无用功真正的权限控制早被 FUSE 接管了。第三个坑忽略 SELinux。老 Android 上Permission denied可能是 ext4 权限位新 Android 上很多是 avc denial。先执行adb shell logcat -b events | grep avc或者抓dmesg | grep avc:看完再决定下一步。没有这个习惯你会在权限模型里绕很久。我自己的排查习惯是先在 shell 里敲两条命令cat /proc/mounts看挂载状态df -hT /data看文件系统类型和剩余空间。确认这两个信息之后再顺着报错链路一层层往上走。实际做完一圈你会认同我这句话90% 的“文件系统坏了”都是权限模型、挂载参数和媒体索引之间的错位真正需要 e2fsck 出场的场景少之又少。每次真要跑 fsck 前我会把最坏情况想清楚备份做没做哪些数据丢不起把预期管理好再动手修复才是稳妥的排查方式。最后分享一个小技巧遇到相关报错先adb shell dumpsys mount看系统自己记录的挂载状态再dmesg | grep -i ext4看内核有没有抱怨这两步比任何可视化工具都靠谱。