ARTICLE DETAIL

资讯详情

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

Android存储权限报错排查:从Operation not permitted到Ext4修复

Android存储权限报错排查:从Operation not permitted到Ext4修复 做Android系统或应用开发的人大概率都遇到过这样一幕某个应用突然打不开预览里面下载好的文件也找不到进adb想手动改一下权限结果系统甩给你一句unable to chmod /storage/emulated/0/android/data/xxx: Operation not permitted。这个报错太经典了很多人第一反应是App出了问题或者权限不够于是去翻权限配置、改manifest折腾半天毫无进展。我排查过不少这类问题最后基本都会回到同一个根源Android设备上的Ext4文件系统以及它和上层的/storage/emulated/0目录之间那层复杂的关系。这篇文章就把我实际排查过程中总结出的一套链路完整分享出来——从表面报错一路挖到Ext4分区的挂载状态、SELinux标签、日志回放最后才是真正的修复步骤。文章的内容和命令都是我实际验证过的适合做Android系统开发、应用开发以及日常折腾设备的工程师参考。1. 先把目录关系理清楚/data分区和你看到的/storage/emulated/0到底怎么映射很多人挂在第一步是因为根本不了解自己看到的那条路径到底对应底层哪个存储区域。/storage/emulated/0看起来像是个独立的SD卡目录其实它根本不是Ext4直接挂载出来的而是基于/data分区之上的一个抽象层。在Android设备上执行一次挂载信息查询就能看得很清楚adb shell mount | grep -E /data | /storage/emulated # 输出大致是 # /dev/block/dm-0 /data ext4 rw,seclabel,relatime,dataordered 0 0 # /dev/fuse /storage/emulated/0 fuse rw,nosuid,nodev,relatime,user_id0,group_id0,default_permissions 0 0第一行才是Ext4真正挂载的地方——/data分区。第二行是FUSE文件系统挂载的/storage/emulated/0。也就是说你在/storage/emulated/0/android/data/com.tencent.tmgp.sgame/files/pandora/下面看到的文件物理上实际存放在/data/media/0/Android/data/com.tencent.tmgp.sgame/files/pandora/这个路径里。FUSE层负责把/data/media/0模拟成用户看到的/storage/emulated/0同时做权限检查和文件过滤。设计这一层的主要目的是多用户隔离。Android为了让一台设备能跑多个用户、多个工作档案又不想让每个用户都单独分一个真分区就在/data/media这个目录之上做了一层按用户ID划分的视图映射。用户0看到/storage/emulated/0用户10工作档案看到/storage/emulated/10底层的物理文件其实都在同一个Ext4文件系统里。这就是为什么/data/media目录在文件系统层面常常能看到u0、u10这类子目录的原因。确认物理目录和虚拟目录的对应关系可以直接用ls对比adb shell ls -l /data/media/0/Android/data/com.tencent.tmgp.sgame/files/pandora/ adb shell ls -l /storage/emulated/0/Android/data/com.tencent.tmgp.sgame/files/pandora/正常情况下你会看到一样的文件列表。如果有差异那基本可以断定是FUSE层缓存或者权限判断出了问题。这套映射关系对排查问题至关重要因为很多操作被拒绝并不是Ext4本身的问题而是FUSE层根据调用者的身份做了拦截。后面你会看到同样的路径App自己访问没问题你用adb shell去访问就被拒绝根源就在这层。2. Operation not permitted背后的完整排查链路最常见的报错就是这种形式adb shell chmod 777 /storage/emulated/0/android/data/com.pubg.imobile/android/obb/ # unable to chmod /storage/emulated/0/android/data/com.pubg.imobile/android/obb: Operation not permitted如果你遇到的是Permission denied那是chmod权限不足但Operation not permitted通常意味着操作被SELinux策略或者FUSE层的安全判断拒绝。这两者差的不是一点半点排查方向完全不同。我建议按下面这条链路一步步来不要跳步。2.1 第一步确认FUSE层是否普通操作根本够不着Android 11之后/storage/emulated/0彻底改用FUSE实现FUSE守护进程会根据调用进程的UID和目录归属做严格匹配。第三方应用的私有目录Android/data/包名/只能由该应用本身或者系统级的mediaprovider访问。你用adb shell的身份去访问UID是shell2000不是那个应用自己的UIDFUSE层直接拒绝。这一步不需要任何高级工具就能确认adb shell id # uid2000(shell) gid2000(shell) groups... adb shell ls -lZ /storage/emulated/0/Android/data/看看目标目录的属主是谁再看看你自己的uid基本就明白怎么回事了。所以这条路径的权限问题第一层根因往往是你的身份不对而不是Ext4文件系统有问题。2.2 第二步查dmesg看SELinux的AVC拒绝记录如果换到root身份或者system身份还是报Operation not permitted那么问题大概率已经下沉到SELinux这一层。观察内核日志是最好的办法adb shell dmesg | grep -i avc: denied | tail -50典型输出类似avc: denied { chmod } for pid1234 commsh nameobb devdm-0 ino234567 scontextu:r:su:s0 tcontextu:object_r:app_data_file:s0 tclassdir permissive0这段日志已经把原因说得很清楚你的进程上下文是su目标目录的上下文是app_data_file策略不允许su对这个类别的目录执行chmod。不是Ext4不让你改是内核安全模块不让你改。遇到这类问题有几个处理方向如果是测试环境可以临时进入SELinux permissive模式验证setenforce 0确认问题确实是SELinux而非其他机制但改完记得恢复。如果是自己的系统固件需要修改SELinux的te策略文件为对应域增加allow规则。如果是用户设备的售后退换问题直接告诉用户这是系统安全机制正常拦截App数据不能靠root强行改权限。2.3 第三步区分文件系统“只读”和“只读挂载”还有一个常被忽略的原因Ext4文件系统在检测到内部错误后会自动把分区重新挂载为只读防止数据继续损坏。这种情况的排查方式是查看当前挂载状态adb shell mount | grep /data # 如果看到 ro 而不是 rw # /dev/block/dm-0 /data ext4 ro,seclabel,relatime,dataordered 0 0同时查看内核相关错误adb shell dmesg | grep -i ext4 | tail -50如果dmesg中出现EXT4-fs error (device dm-0)、Remounting filesystem read-only这类信息那就是Ext4自己把自己锁死了。这时候你执行任何写操作都会返回Operation not permitted或者Read-only file system看起来就像是权限问题其实文件系统已经进入保护模式。遇到这种情况的处理是先做数据备份在只读状态下能读多少算多少。重启进入恢复模式。在recovery下对/data分区执行文件系统检查和修复具体流程放下一节。修复完成后再正常开机确认挂载恢复为rw。2.4 一个综合案例分析我之前排查过一个挺典型的故障有个设备里存放日常记录图片的应用突然所有历史图片都打不开应用内显示文件丢失但用文件管理器又能看到文件存在。adb进去查看目录能列出但任何写入操作都失败提示Operation not permitted。按照上面的链路排查后问题出在两层叠加第一层该应用的私有目录权限信息在FUSE层缓存中出现了错乱App通过自己uid访问时FUSE返回了不存在的空目录视图所以应用找不到文件。第二层底层/data分区的Ext4文件系统部分元数据损坏导致FUSE守护进程读取目录内容时反复失败继而拒绝了所有上层写入。重启清FUSE缓存之后第一层问题消失但底层损坏依旧存在这就需要进入文件系统修复流程了。像这种上层症状和下层根因不一致的情况在Ext4问题上非常常见排查时一定要有往下多挖一层的意识。3. Ext4真正的故障现场从日志报错到e2fsck完整修复如果你在内核日志里看到类似下面的信息那就不是权限问题了而是Ext4文件系统本身出故障了EXT4-fs error (device dm-0): ext4_find_entry:1456: inode #9846614: comm kworker/u8:2: reading directory lblock 0 EXT4-fs error (device dm-0): ext4_ext_map_blocks:482: inode #3767: block 261238: comm sqlite: invalid extent Aborting journal on device dm-0. EXT4-fs (dm-0): Remounting filesystem read-only在解释怎么修复之前我觉得有必要先说清楚Ext4里面几个关键机制不然你光会跑命令不懂为什么会坏、为什么修完就好。3.1 inode、目录块和日志机制这三件事要理解可以把Ext4文件系统想象成一个图书馆inode就是图书索引卡上面记录着这本书文件在哪个书架数据块、作者是谁属主、权限如何、最近修改时间。目录块就是图书馆里的分类导引台告诉你这个分类下有哪些书、索引卡放在哪里。journal日志区则像是图书管理员的流水账本每次改书架前先把这次操作的步骤记在账本上执行完再划线标记完成。日志机制journaling的意义在于崩溃恢复。当系统突然断电、内核panic、或者刷机中断时下次开机时Ext4会回放journal把没做完的收尾工作补完保证文件系统的一致性。这就是为什么异常断电后文件系统往往还能挂载的原因。但问题也恰恰出在journal身上如果journal写入了坏块或者journal所在的元数据区因为闪存老化损坏了挂载时回放日志本身就会失败。这时内核常常直接放弃挂载或者以只读方式挂载。理解了这套机制你再看fsck的动作就明白了fsck做的事情其实就是把索引卡inode和书架数据块逐一核对把没对上账的记录修整或清理再把journal重新初始化。它是在对图书馆做全馆盘点而不是靠一层日志找平。3.2 修复的标准流程我自己操作时一般按这五步走每一步都不要跳第一步确认分区设备和挂载状态adb shell df -T /data adb shell mount | grep /data adb shell ls -l /dev/block/bootdevice/by-name/ 2/dev/null | grep -i userdata # 或者 adb shell find /dev/block -type l -name *userdata* 2/dev/null记录下userdata分区对应的设备节点比如/dev/block/mmcblk0p46或/dev/block/dm-0。后面修复要用到。第二步在recovery模式下操作而不是开机状态绝对不要在系统正在运行、分区处于挂载状态时执行e2fsck。Ext4运行时会持续写入两边同时操作会造成二次损坏。正确做法是重启进recoveryadb reboot recovery或者自己按组合键进recovery。进入后很多recovery支持adb命令可以先挂载验证状态adb shell mount | grep /data如果recovery里面没自动挂载就不要挂载直接用设备节点检查。第三步执行检查先看前次修复记录再做只读检查adb shell e2fsck -n /dev/block/bootdevice/by-name/userdata-n表示只检查不做修改。这一步能看文件系统损坏到什么程度比如superblock last mount time is in the future、inodes that were part of a corrupted orphan linked list这些提示。如果提示的是一堆自动修复后的残留问题说明之前可能已经有人跑过一次不完整的fsck。第四步执行修复确认一遍设备节点然后执行adb shell e2fsck -fy /dev/block/bootdevice/by-name/userdata这里两个参数含义-f强制检查即使文件系统标记为clean的也查一遍。因为设备上文件系统经历了异常卸载标记可能不准强制检查更稳妥。-y所有问题自动回答yes。修复过程中它可能会问clear inode X? (y/n)生产环境没人守在旁边直接自动确认。如果是超级块superblock损坏e2fsck会提示无法读取这时候可以指定备份超级块adb shell e2fsck -fy -b 32768 /dev/block/bootdevice/by-name/userdata32768是Ext4最常见的块组间隔参数也就是每隔32768个块有一个备份superblock。如果这个位置也不对可以尝试-b 8193或者其他位置或者看输出提示里给了哪个备份块可用。第五步修复完成后不要立刻重启先再看一眼adb shell e2fsck -n /dev/block/bootdevice/by-name/userdata确认输出是clean再正常重启。修复后第一次开机可能比较慢因为Ext4要在挂载时做journal回放并重建部分目录结构这是正常现象。3.3 修复过程中的特别提醒整个流程里有几个很容易踩的坑加密设备的误操作Android 7之后全盘加密普遍Android 10之后文件级加密FBE成为主流。/data分区里的用户文件在应用层看来是密文对e2fsck来说它们只是普通的块数据所以e2fsck不需要解密密钥可以直接修。但注意如果加密元数据和多媒体数据存在Ext4层的extent映射损坏修复后个别文件会出现在/lostfound目录里而且可能是无法打开的空壳。这个是Ext4统一行为不是恢复软件能救回来的。recovery里的adb权限有些官方recovery的adb是受限的e2fsck可能不在里面或者需要手动放行/system/bin和/sbin。如果recovery里没有e2fsck你可以从固件包里提取一个对应架构的e2fsck和libext2fs.so推送到/sbin再执行但务必匹配同一套版本避免工具版本不兼容。备份永远放在第一位e2fsck -fy可以修复元数据但修复本身也可能把文件归入lostfound甚至标记为删除。如果设备还能以只读方式挂载优先在只读状态下把重要数据通过adb pull备份出来再执行修复。这一步看似浪费时间实际遇到数据损坏时能救命。4. 高IO和CPU 100%的排查别急着怀疑应用先看看jbd2和回写参数Ext4的问题不只是文件损坏和权限拦截还有一个很常见但特别容易误判的场景——设备突然变得很卡CPU负载飙到100%IO wait特别高。很多人一看CPU跑满就去查是哪个进程吃CPU结果用top一看排前面的全是kworker、jbd2这种内核线程瞬间就懵了。先说结论如果是jbd2线程导致CPU高那么根因往往在Ext4的日志提交频率和上层文件写入行为上而不是某个应用真的在疯狂消耗计算资源。4.1 jbd2是什么为什么它能把CPU吃满jbd2是Ext4的日志内核线程全称是Journaling Block Device。它的工作是把文件系统的元数据变更记录到journal区这个动作叫做日志提交journal commit。正常情况下jbd2偶尔占用一点CPU没什么存在感。但当上层有大量小文件、频繁的fsync()/fdatasync()调用时jbd2会被迫高频提交日志每次提交都需要持锁做校验、写缓存、刷新块设备CPU消耗立刻上去了。典型的管理员视角操作# 看CPU负载 adb shell top -H -n 1 | grep -E jbd2|kworker输出里可能看到1 0.0% 123 0.0% 0 S 123 0 0 0 0 jbd2/dm-0-8jbd2的线程名是jbd2/dm-0-8这种格式后面的dm-0-8表示它的设备号和分区号。如果它出现在高位说明文件系统日志提交非常频繁。4.2 真正要查的是谁在触发高频fsyncjbd2只是受害者真正的行凶者是上层应用。排查链路应该是这样的第一步确认系统IO状态。Android上没有服务器那种iostat但可以读/proc/diskstatsadb shell cat /proc/diskstats # 关注字段read completed、write completed、time spent writing从前后两次采样的差值可以算出写入速率。如果写入速率很高但应用层看起来没那么大流量基本可以断定是小文件频繁落盘。第二步用系统调用跟踪锁定进程。Android上可以用strace或者系统自带的debuggerdadb shell strace -p pid -f -e tracefsync,fdatasync,write 21 | head -100我曾经遇到过一个健康类应用它每隔几百毫秒往私有目录写一条日志每次写入前都调用fdatasync强制落盘存储立刻被打满jbd2 CPU直接飙到50%以上。这就是典型的应用行为不当文件系统参数默认为服务器设计导致的性能问题。第三步检查系统的脏页回写参数。Android的kernel是有dirty_ratio和dirty_background_ratio的默认值和服务器接近但移动端的设计目标是更省电、更少磨损而不是极限吞吐adb shell cat /proc/sys/vm/dirty_ratio adb shell cat /proc/sys/vm/dirty_background_ratio adb shell cat /proc/sys/vm/dirty_writeback_centisecs adb shell cat /proc/sys/vm/dirty_expire_centisecs如果应用在疯狂fsync这些参数再合理也救不回来因为fsync本身就要求立刻回写无法靠后台聚合延迟来完成。对付这种场景手段有几种应用侧优化把频繁的小日志改为批量写入降低fsync频率。这是最根本的解法。文件系统挂载参数调整在fstab里把userdata的挂载参数改成datawriteback而不是dataordered可以降低一部分元数据日志开销但要接受崩溃时文件内容可能和元数据不同步的代价我们生产环境不太建议。内核线程绑核在压力大的设备上把jbd2绑定到某个频率高一点的CPU核心避免它和其他重要线程争抢。这个是系统工程师的调优手法应用开发用不上。4.3 提取和定位高IO问题的实际案例我之前查过一个后台跑着跑着整个界面卡死的案例。当时的top里出现jbd2占用40%、kworker/u8占用60%IO等待时间达到50%以上。用strace跟踪后发现是应用在做崩溃日志收集每打印一条崩溃堆栈就调用一次fsync崩溃期间循环写了几百次小文件每次一两KB。几百次fsync下来Ext4的journal区被反复刷写CPU自然撑不住。这种问题用参数调优可以缓解但治本得靠应用层搜集日志可以合并写、延迟批量落盘或者放到后台空闲时段统一清理。在Android这种存储性能不高的设备上尤其忌讳无节制的fsync调用这也是很多应用在真机上出现存储卡顿的一号嫌疑犯。5. 一些日常维护手段先预防后救火处理过几次Ext4的故障之后我养成了一个习惯新设备或者出厂固件到手先做一轮文件系统的摸底体检把容易出问题的雷提前排掉。5.1 常规体检三件套# 1. 查看挂载状态和文件系统类型 adb shell mount | grep -E /data | /cache | /system # 2. 查看文件系统剩余空间 adb shell df -h /data # 3. 查看日志里有没有ext4错误或SELinux拒绝 adb shell dmesg | grep -iE ext4|avc: denied | tail -50这三条命令基本够日常用了。如果第一条里面出现ro第二条显示剩余空间低于10%或者第三条有连续多条error就要准备介入处理了。5.2 防止损坏的三个习惯保持充足剩余空间是很多人忽略的。Ext4在分区剩余空间极低时分配inode和目录块的成本急剧上升频繁分配失败会引发内核报错极端情况下会触发文件系统自动降级。Android设备建议至少预留15%的空间别把128GB塞到只剩1GB才去清理。异常断电重新开机后要观察一段时间再下结论。我自己有过一次教训设备异常断电后重启看起来一切正常但应用一打开就闪退最后dmesg发现是/data分区元数据损坏但尚未触发只读保护应用读取目录失败才崩溃。早年遇到这种问题我习惯直接重置设备后来学会了先做一次只读fsck检查再决定是否重置。大版本OTA升级前清理乱七八糟的临时文件。OTA升级过程中/data分区大量写操作如果空间不足或者应用残留大量无效小文件升级中断的概率会显著提升而OTA中断是Ext4出现orphan文件块、lostfound文件的重要诱因。养成升级前清缓存、删无用安装包的习惯对保护文件系统很有效。5.3 如果修复完没多久又复发要考虑硬件层面如果e2fsck修复完用了一两个星期又出现类似的Ext4错误这就不能归咎于软件了。大概率是闪存eMMC/UFS的块老化或坏块管理出问题。Ext4会把这些坏块加入忽略列表但如果是闪存控制器损坏新的坏块会不断出现文件系统反复挂掉。这种情况的处理先用mmc相关工具或者厂商诊断工具测一下闪存健康状况。检查有没有I/O error在dmesg中频繁出现。如果确认是硬件问题别浪费时间反复修直接跟用户/工厂沟通换机或换存储芯片。5.4 说点实在的经验最后说一个我自己踩过多次坑才得出的经验不要直接在/storage/emulated/0这个FUSE层里做大量的文件移动、重命名、权限修改操作。这一层的设计本质上是给上层App访问用的不是给系统工程师折腾底层文件用的。真正的操作应该在/data/media/0这个物理路径上进行或者用Google官方的adb shell content命令配合FileProvider去操作。直接对着FUSE层做chmod和rename常常会触发SELinux拒绝和FUSE层的路径解析冲突让你误以为是Ext4出问题了实际上只是你选错了操作层。再补充一个小技巧如果你要确认一个文件在Ext4层的真实inode状态绕开FUSE直接访问物理路径会准得多adb shell stat /data/media/0/Android/data/package/files/somefile.txtstat输出里的Device、Inode、Access字段都是从Ext4层直接读出来的比从FUSE层看到的更真实。如果stat正常但上层App打不开那病根在FUSE或者其他上层服务如果stat本身就报错那才是真到了Ext4这一层。这个判断在混淆各种Operation not permitted报错时极其管用。
返回列表