前几天群里有人甩过来一条报错,原话是unable to chmod '/storage/emulated/0/android/data/com.xjs.ehviewer': operation not permitted。按字面理解,就是一个普通权限问题,但真上手去查的时候,发现事情没这么简单:chmod不成功后,紧接着复制文件也失败、日志写不进去、连应用自己生成的目录都在重启后“消失”了。这种事在Android的Ext4文件系统上太典型了,不把从VFS层到SELinux再到分区挂载选项这一条链捋清楚,排查起来基本靠蒙。
这篇文章把我平时处理Android Ext4问题时的思路完整整理了一遍。不管你是做系统开发、应用逆向、自动化测试,还是只是经常折腾手机的老玩家,下面这些场景大概率都遇到过。我会用一个个具体的“现场”来拆解,每一步怎么定位、怎么确认根因、怎么安全处理,尽量把“为什么”也讲透。
1. 认识Android的Ext4:别等炸了才想起它是谁
很多人一听到“Ext4”就想起Linux服务器,总觉得Android上的文件系统顶多是个“移动版”,不值得单独研究。这个想法会吃亏。Android虽然内核是Linux,但它的存储架构和普通发行版差异巨大,很多坑恰恰出在那些“你以为一样其实不一样”的地方。
1.1 分区与文件系统的对应关系
Android设备内部并不是一个大分区一锅炖,而是按功能切成若干分区,常见的有/system、/vendor、/product、/data、/cache、/metadata等。绝大多数分区用Ext4格式,少数分区用EROFS、F2FS或者原始分区。
重点是/data分区,它挂载在/dev/block/by-name/userdata上。这个分区存放了所有应用数据、系统设置、用户配置文件,甚至还包括你的照片、下载文件。它才是日常出现文件系统问题的主角。
/system这类只读分区如果出了问题,通常表现为开不了机、卡logo、OTA失败。而/data分区出问题就“精彩”了,各种权限报错、文件消失、空间消失、app闪退都有可能。
1.2 /sdcard和/storage/emulated/0到底是谁
这块要单独说,因为太多人在这里栽跟头。你在Android里看到的/sdcard、/storage/emulated/0并不是一个独立SD卡分区,它其实是/data/media/0目录通过FUSE(用户可以简单理解成一个“翻译层”)或者sdcardfs挂载出来的虚拟视图。
也就是说,你的照片文件物理上存在Ext4分区里,但从路径上看到的是一套模拟“外部存储”的目录结构。这个设计导致了一个很关键的特性:当你访问/storage/emulated/0/Android/data/...时,路上经过了不止一层的权限检查——Ext4文件系统权限是一层,FUSE挂载的行为限制又是一层,SELinux再加一层。
所以经常遇到的现象就是:明明自己就是root,chmod一个文件却提示operation not permitted,或者明明看到文件存在,cat却读不出来。这不是系统“疯了”,是好几层限制叠在一起了。
1.3 一个App目录报错案例的构成拆解
看一条实际路径:/storage/emulated/0/android/data/com.tencent.tmgp.sgame/files/pandora/pro。
一层层拆开来说:
/storage/emulated/0:模拟外部存储的根,对应/data/media/0android/data/:Android应用外部私有目录的公共父目录com.tencent.tmgp.sgame:应用包名,代表这个目录归哪个应用所有files/:应用自己存放持久化文件的地方pandora/pro:具体业务子目录,一般是游戏资源或缓存
这类目录的权限模型是“谁创建谁拥有”,正常情况下其他应用(包括adb shell)都不能进去读写。Android 11之后尤其严格,即使通过USB连接电脑,也看不到android/data下的任何内容。
理解了路径的“构成”,再看报错就清晰多了。Google搜到的很多英文回答让你“chmod /storage/emulated/0”,但这条路在FUSE层面根本走不通——FUSE挂载对权限有自己的一套行为约束,不是底层Ext4不支持,而是中间这层不允许。后面我会展开讲怎么验证。
2. 现场一:Operation not permitted——权限问题排查
还是从最经典的那个报错说起。unable to chmod这种问题,看似简单,实际涉及四层可能性:容器层、挂载层、SELinux层、应用层。排查思路就是从外到里逐步缩小范围。
2.1 报错复现与第一反应
先复现一下场景。我把一台Android设备通过adb连上电脑,执行:
adb shell $ mkdir -p /storage/emulated/0/android/data/com.xjs.ehviewer $ chmod 755 /storage/emulated/0/android/data/com.xjs.ehviewer chmod: chmod '/storage/emulated/0/android/data/com.xjs.ehviewer': Operation not permitted第一反应不是去和权限“搏斗”,而是先确认三件基础事实:设备有没有root、挂载是不是只读、SELinux是不是Enforcing。
$ id $ mount | grep /data $ getenforce通过这几条命令,先判断是“文件系统不让动”还是“安全策略不让动”。
注意:这里的“Operation not permitted”是典型的Errno EPERM,和“Permission denied”(EACCES)有区别。前者通常意味着操作本身被禁止,后者通常是当前用户权限不够。看到哪个报错,排查方向完全不一样。
2.2 从挂载参数到SELinux逐层排查
当报错落在/storage/emulated/0/android/data/这类路径上时,优先级最高的怀疑对象其实是FUSE或sdcardfs挂载层。可以这样查:
$ mount | grep -E 'emulated|sdcard|fuse'正常情况下你会看到类似这样的输出:
/dev/block/dm-0 on /mnt/user/0/emulated type ext4 ... /data/media on /storage/emulated type sdcardfs ...看到sdcardfs这类字样就基本明白了:在这个挂载层里,chmod行为是受限的,因为sdcardfs在挂载时已经定义好了一套简化的权限视图。它刻意屏蔽了对mode位的修改,为的是让应用之间互相隔离,防止一个应用通过暴力chmod把别的应用目录“打开”。
再往深一层看SELinux:
$ ls -lZ /storage/emulated/0/android/data/com.xjs.ehviewer看安全上下文尾部是不是u:object_r:app_data_file:s0或类似标签。接着查avc日志确认有没有被SELinux拦下来:
$ dmesg | grep avc | tail -20 $ logcat -d | grep avc | tail -20如果日志里出现了avc: denied { setattr } for ... comm="touch",那说明SELinux这一步确实参与了拦截。但要注意,SELinux并不总是主凶,很多时候它只是“背锅”的——上层的sdcardfs/FUSE先拒绝了,根本没走到SELinux那一步。
2.3 用“最小变更”确认根因
排查这类多层问题,我的习惯是先做几个互相对照的实验,把变量一个一个排除。
第一个实验,尝试在/data/local/tmp下建目录并chmod:
$ mkdir -p /data/local/tmp/test_dir $ chmod 777 /data/local/tmp/test_dir如果这个能成功,说明底层Ext4本身是好的,当前用户的权限也够。
第二个实验,在/storage/emulated/0根目录下建一个普通目录并chmod:
$ mkdir -p /storage/emulated/0/test_dir $ chmod 777 /storage/emulated/0/test_dir如果这步也失败,问题基本锁定在上层挂载行为。如果成功,那问题就更进一步出在android/data这个特殊路径上。
第三个实验,在/storage/emulated/0/android/data/com.xjs.ehviewer内部再建子目录并授权:
$ mkdir -p /storage/emulated/0/android/data/com.xjs.ehviewer/test_sub $ chmod 755 /storage/emulated/0/android/data/com.xjs.ehviewer/test_sub在很多设备上,外层目录的权限不可更改,但在“自己拥有”的嵌套子目录里反而可以操作。这个现象很能说明问题:Android在模拟外部存储这一层故意锁定了应用隐私目录的顶层权限,不允许其他主体去改。
2.4 修复方案与慎用清单
针对“Operation not permitted”,如果目标路径是应用私有目录,正确的做法不是强行chmod,而是通过正式的API去访问:
- 应用侧:使用
context.getExternalFilesDir()或FileProvider来共享文件,而不是直接跨应用操作路径。 - 调试侧:在开发调试时使用
adb shell run-as com.xjs.ehviewer进入应用沙箱内操作,这是在非root环境下最安全的变通方案。 - 备份侧:Android 11以上用
adb backup或者在系统设置里允许USB传输,不要指望物理路径可以随意访问。 - 如果设备确实是自己维护的工程机且已经root:可以通过
magisk模块、SELinux放行策略,或者在/data自身分区下操作,而不是在FUSE层折腾。
实际经验:强行chmod应用私有目录,就算成功了,也很容易让应用崩溃。因为应用在启动时会校验目录权限,如果发现owner对不上,往往直接抛出
SecurityException。我之前见过一个案例,有人为了“清理游戏资源包”把整个/sdcard/Android/data目录chmod成777,结果游戏一启动就读写失败,日志目录全乱了。
这章最后留一个实操心得:遇到EPERM,先dmesg再mount,先看“谁不允许”再看“为什么不允许”,这是排查所有文件系统权限问题的通用顺序。
3. 现场二:容量还在,文件却没地方放了
权限问题解决之后,下一类高频事故是存储空间异常。最典型的症状:明明看存储设置里还有好几个G,但应用就是写不进文件,系统提示“存储空间不足”。
3.1 从“复制失败”开始
有一次用户反馈,图库应用生成的缩略图目录/storage/emulated/0/android/data/com.android.gallery3d/files/thumbdb写不进去。查了一下,df -h显示/data分区还有很多剩余,但应用就是报磁盘写入失败。
这个现象非常值得警惕。剩余空间充足但写入失败,优先考虑三种原因:inode耗尽、FBE密钥未解锁、文件系统元数据损坏。其中inode耗尽最常见。
3.2 区分块满与inode耗尽
df -h只展示了块(block)的使用率,但Ext4写入文件时还需要一个inode——你可以把它理解成文件系统的“户口本”。如果inode用光了,哪怕磁盘块还剩一大半,也无法创建新文件和目录。
查看inode使用情况要用:
$ df -i /data输出里如果IUse%接近100%,问题就清楚了。小文件非常多的时候最容易出现inode耗尽。我见过一个真实场景:某个App疯狂创建0字节日志文件,每个文件占一个inode,几百万个文件直接填满了分区。
3.3 实操:安全定位占位文件
定位“谁占满了inode”时,直接du可能不准,因为du默认按数据块统计,对0字节文件不敏感。正确方式是:
$ find /data -xdev -type f -printf '%h\n' | sort | uniq -c | sort -nr | head -30这条命令会按“目录”统计文件数量,数量超大且文件体积很小的目录,基本就是inode杀手。
查到可疑目录后,先确认是不是系统缓存或应用日志,确认无误再清理。千万不要在没确认前就乱删。有一次我把一个看似很大的/data/cache目录直接清掉,结果系统无法正常启动——因为里面藏着OAT缓存文件,删掉后Zygote加载失败,只能重新刷分区。
3.4 真·修复:fsck与debugfs的使用边界
如果怀疑文件系统本身有元数据损坏(比如异常断电后出现),可以在recovery模式下用fsck检查。但这里有一条铁律:不要在已挂载的分区上跑fsck。正确做法是进入recovery,找到对应块设备,先做只读检查:
# 在设备上获取userdata分区真实路径 adb shell ls -l /dev/block/by-name/userdata # 进入recovery或使用adb reboot bootloader后再引导到recovery # 在recovery adb shell中执行(注意是只读模式!) e2fsck -fn /dev/block/by-name/userdata-f表示强制检查,-n表示只读,不会做任何修复。这一条很重要,我第一次操作时就因为习惯性加了-y(自动修复),导致文件系统被改得一团糟。
如果只想查看某个文件是否还能被Ext4恢复,可以用debugfs:
debugfs -R 'ls -l /lost+found' /dev/block/by-name/userdata debugfs -R 'stat /data/media/0/Pictures/photo.jpg' /dev/block/by-name/userdata不过要先说明:debugfs属于“手术刀”级别,适合做信息提取,不适合日常操作。真正常见的inode耗尽问题,重启进recovery清掉几个大缓存目录就解决了,不一定要上fsck。
4. 现场三:文件“消失”、路径对不上、重启后目录变了
如果说权限和空间问题还能靠命令定位,“文件消失”绝对是最玄学的。经常是昨晚还好好的,今早醒来发现目录还在但里面空了,或者换台设备登录同一个账号,路径对不上了。
4.1 三个很常见的“灵异事件”
第一个事件,重启后/storage/emulated/0/Android/data/com.tencent.tmgp.sgame/files/pandora/pro变成了空目录,但应用内部存储里却能正常显示资源。
第二个事件,通过content://com.baidu.searchbox.fileprovider/baiddpath/android/data/com.ba...这个uri能读取文件,但FileProvider的路径映射一旦失效,系统提示“文件不存在或已被删除”。
第三个事件,从旧手机迁移数据后,图片全在但路径全变了,比如原来在/storage/emulated/0/Pictures/,现在跑到/storage/emulated/0/DCIM/或者某个以日期命名的临时目录里。
这三个事件表面不相关,实际上背后有几套机制在同时起作用。
4.2 藏在背后的挂载与加密逻辑
Android从7.0开始普及FBE(File-Based Encryption,基于文件的加密)。/data分区下很多目录分成了ce(Credential Encrypted)和de(Direct Encrypted)两套。锁屏状态下,ce目录往往是不可访问的;一旦解锁完成,挂载层才会把ce目录“映射”出来。
所以“重启后目录空了”这个现象,很多情况根本不是数据丢了,而是你还没把加密密钥喂给系统——设备和app都还在等待解锁状态。
至于路径对不上的问题,责任通常在第4层FileProvider或/sdcard的符号链接。/sdcard并不是真实路径,它是指向/storage/emulated/0的符号链接。如果系统存储迁移(比如从内部存储迁移到SD卡)没有做好原路径索引更新,就会出现“用一个uri打开没问题但按路径找不到文件”的割裂现象。
4.3 一条路从应用层查到内核层
遇到这种“路径对不上”的异常,先确认你是不是在正确的时间点上访问:
# 查看CE目录状态 adb shell ls /data/user/0/ adb shell ls /data/user_de/0/如果/data/user/0/下某应用目录为空,大概率是CE还没解锁。再查挂载映射关系:
adb shell ls -l /sdcard adb shell cat /proc/mounts | grep media adb shell getprop sys.user.0.ce_available把这几条命令跑完,基本能判断“文件消失”是暂时的加密等待还是真的被系统清理了。去年我帮人处理过一次“小米健康日志目录找不到”的问题,路径是/storage/emulated/0/android/data/com.mi.health/files/log/xiaomifit.main.log,现象就是重启后这个路径变空。排查之后确认根本不是丢文件,而是设备解锁后CE挂载比应用启动慢,等解锁完成再去访问,文件原封不动都在。
如果真确定是文件系统层丢失(比如lost+found目录里出现大量编号文件),那就说明是异常断电或分区损坏导致Ext4把孤儿文件挂到了lost+found。这时候不要直接在手机上操作,先拿到原始块设备做镜像备份,再用debugfs去恢复,能救多少是多少。
个人经验:遇到任何“文件消失”,先不要急着恢复,先判断“逻辑消失”和“物理消失”。逻辑消失可以通过解锁、重启、等待、重新mount解决;物理消失才是靠fsck和debugfs能救的。把这两类搞混,是做文件系统恢复时最容易白费力气的地方。
5. 现场四:文件系统拖垮整机——CPU 100%里的I/O陷阱
有一个热搜词是“线上服务器的cpu使用达到100%了,如何排查定位解决”,虽然说的是服务器,但Android设备上也会遇到一模一样的现象:设备卡成PPT、温度飙升、系统监视器显示CPU 100%,最后揪出来的元凶居然不在CPU,而在文件系统。
5.1 文件系统和CPU有什么关系
文件系统I/O和CPU是强绑定的。以Ext4为例,写操作会经过VFS层、文件系统层、块层,最后通知硬件。任何一层卡住,内核都会把进程挂起等待,表现出来就是进程口在I/O wait,而CPU占用被内核线程(比如jbd2、kworker)拉高。
我见过最典型的场景:某游戏App每次启动都在/storage/emulated/0/android/data/com.pubg.imobile/android/obb/找寻最大的资源包,代码写得很“憨”,每次要做全目录扫描加校验。资源包大、目录文件多,一启动就把I/O队列打满。这时系统所有进程都在等I/O,CPU空转也高,用top看就是一片红。
5.2 一份真实的排查顺序
遇到CPU高企,先分清是“真忙”还是“假忙”。用以下命令:
# 看CPU时间片都花在哪 adb shell top -H -n 1 | head -40 # 看内核I/O等待时间占比 adb shell cat /proc/stat | grep procs # 看各分区实时读写 adb shell cat /proc/diskstats如果发现大量线程阻塞在D状态(不可中断睡眠),同时si和wa两项数值居高不下,基本可以断定是I/O瓶颈。
再把范围缩到进程:
adb shell cat /proc/所有进程/stack或者在root设备上直接抓取ftrace:
echo 0 > /sys/kernel/debug/tracing/tracing_on echo 1 > /sys/kernel/debug/tracing/tracing_on cat /sys/kernel/debug/tracing/trace重点看内核函数栈里是不是大量出现ext4_*相关调用。如果是,问题定位到文件系统层基本没跑了。
5.3 针对文件系统的加固手段
定位到是文件系统拖累全局性能后,有几件事可以做:
第一件,查是否有已删除但仍然被进程占用的文件。被删除的文件不释放占用的块,而且每次I/O读写都会反复触发元数据更新:
adb shell lsof | grep deleted有root权限的设备也可以直接遍历/proc/*/fd/,找指向deleted的符号链接。
第二件,查看进程是否在疯狂调用sync或fsync。如果某进程高频调用fsync,jbd2线程会忙个不停。快速判断方法:
adb shell cat /proc/进程号/io看writeback计数是否短时间内暴增。如果确认是应用层问题,只能等应用修复,或者临时用setenforce 0做实验(注意这只是临时手段,不适合长期使用)。
第三件,检查/data分区碎片化程度。碎片过多会让Ext4在随机读场景下表现极差。虽然Ext4不太建议日常做碎片整理,但在空间长期紧张又频繁增删文件的设备上,e4defrag可以当作一次性的“大扫除”手段。工程机可以执行,但生产环境注意先备份和验证。
6. 工具箱:我平时“出警”用的一套命令
写到这里,把常用命令整理成一份速查表,方便照着用。
6.1 分区与挂载检查
| 目的 | 命令 | 说明 |
|---|---|---|
| 查看分区空间 | adb shell df -h | 关注Use%和Avail |
| 查看inode使用 | adb shell df -i /data | IUse%接近100%要警惕 |
| 查看挂载参数 | adb shell mount | 过滤Ext4和emulated相关行 |
| 查看SELinux状态 | adb shell getenforce | Enforcing/Disabled |
| 查看文件上下文 | adb shell ls -lZ <path> | 确认secontext |
| 查看内核日志 | adb shell dmesg | 找ext4、I/O error、avc |
6.2 文件级分析与数据恢复
| 目的 | 命令 | 说明 |
|---|---|---|
| 统计目录文件数 | adb shell find /data -xdev -type f -printf '%h\n' | sort | uniq -c | sort -nr | head | 找inode大户 |
| 按大小排目录 | adb shell du -m -d 3 /data 2>/dev/null | sort -nr | head | 找空间大户 |
| 找已删未释放资源 | adb shell lsof | grep deleted | 定位被占用空间 |
| 只读检查Ext4 | e2fsck -fn /dev/block/by-name/userdata | 不能加-y |
| 查看文件inode信息 | debugfs -R 'stat <path>' /dev/block/by-name/userdata | 数据恢复时使用 |
| 提取恢复文件 | debugfs -R 'dump <path> /tmp/xxx' /dev/block/by-name/userdata | 只读导出文件 |
6.3 跟新人说的三句话
第一句:遇到文件系统问题,先做信息收集,再做判断,最后才动手处理。顺序反了大概率扩大故障面。
第二句:没有root权限的普通设备,不要硬折腾底层Ext4。很多上层报错(比如权限问题、目录消失)都是Android设计的行为,不是文件系统坏了,你把它当成文件系统问题去修,反而会引入真实损伤。
第三句:如果是做开发,调试阶段多用run-as和FileProvider,别老想着直接chmod跨应用目录。这条路在Android 11之后基本被堵死了,越早接受这一点,越少踩坑。
如果非要说一个具体验:新版本Android的权限模型越来越严格,但底层Ext4本身一直很稳定。真正导致问题的,基本都是“应用代码抗不住I/O延迟”“FUSE层行为变了应用没适配”“SELinux策略与第三方应用冲突”这几类。把重心放在“应用如何正确访问文件”上,比研究怎么暴力打穿路径要靠谱得多。
最后分享一点个人体会
我最早处理文件系统问题时也走过弯路——拿到一个“Operation not permitted”,第一反应就是上fsck,结果在那台设备上跑了半天,除了把读头累得够呛,一点问题也没解决。后来才意识到,权限问题是“策略问题”,不是“磁盘问题”。硬盘目录权限不符、SELinux拦截、挂载选项禁止,这三种情况的处理方式完全不同,用错了工具等于对着感冒病人做手术。
另外,养成一个习惯:在recovery模式下修分区之前,先拍照或截图记录dmesg里最后几条和mmc、blk_update_request相关的日志。这些日志是判断是否真的发生硬件I/O异常的唯一硬证据,也是事后复盘的关键。电子设备不会说话,但内核日志会;好好记录和分析日志,是解决一切文件系统问题最简单也最可靠的方式。