先从我实际遇到的一个案例说起:一台Android测试机上,/data分区明明还剩好几个G的可用空间,可应用就是装不进去、日志写不出来,df看着一切正常,但操作时一直被拒绝。这种问题我碰过不少次,大多数时候问题本身并不神秘,只是很多人排查文件系统问题时,一上来就盯着 ext4 这四个字母,却忽略了一个关键事实——在Android里,"文件系统问题"往往并不是存储引擎坏了,而是挂载策略、权限模型和存储分层这三层里某一层出了问题。
这篇内容源自我和团队做Android设备维护、嵌入式系统调试时积累的排查经验,结合最近在一些技术社区里被反复问到的场景,把常见的Ext4文件系统问题按类型拆开讲清楚。适合做Android系统开发、应用层适配、嵌入式Linux移植的工程师,也适合做设备运维、遇到线上存储异常时需要快速定位的人。整篇更像是一份实战笔记,把排查思路、关键命令、根因分析都放进去,没有废话,基本可以直接照着操作。
1. Android的文件系统长什么样:先搞清楚你在哪一层挂的
很多人排查存储问题第一步就看df,看到ext4就觉得内核文件系统层出了问题,这个惯性思路害了不少人。想要高效定位问题,首先得把Android的存储架构在脑子里建起来——从你敲命令的位置到最终落盘的数据,中间隔了好几层,每一层都有自己的规则和"脾气"。
1.1 从内核到应用的数据通路
真正的ext4文件系统其实只存在于两个地方:一个是/data分区所在的用户数据块设备,另一个是/system、/vendor、/product这些只读分区。往内核层看,VFS(Virtual File System,虚拟文件系统)是统一入口,它把各种文件系统挂到系统的目录树上,ext4只是VFS之下的一种具体实现。你在/data里创建、读取、删除文件,实际走的是 VFS → ext4 → 块设备驱动 这条链路。
那/sdcard、/storage/emulated/0呢?这里就有一个容易误会的点。用户可见的/storage/emulated/0/Android/data、/storage/emulated/0/DCIM这些路径,其实不是直接挂在ext4块设备上的,而是由一个叫FUSE(Filesystem in Userspace)或旧版本上的sdcardfs层实现的。FUSE 这一层把/data/media目录(在真正的ext4上)暴露给应用,并且由ExternalStorageProvider这类系统服务来管理和限制访问规则。
这就是为什么你经常能看到这样的情况:应用通过FileAPI 读写/storage/emulated/0/Pictures/xxx.jpg明明没问题,但走原生路径访问/data/media/0/Pictures/xxx.jpg却碰到Permission denied。不是ext4拒绝了你,是FUSE层的权限校验把你拦住了。
1.2 挂在用户空间的分区存储层
在Android 10及以上版本,系统还引入了分区存储(Scoped Storage)的概念。虽然数据最终还落在那块ext4文件系统上,但应用能看到的"视图"被限制到自己的私有目录(/sdcard/Android/data/<package>或者内部存储里的/data/data/<package>)和特定的媒体集合目录(Pictures、Movies、Download)。
这层设计的原因很简单:谷歌希望隔离应用之间的数据访问,防止一个App扫走整个用户目录。但对做开发调试的人来说,这层"视图"经常让排查工作变得异常痛苦——明明/data空间够、文件系统本身也没坏,App却访问不到本身应该能访问的东西。
1.3 /data 和 /sdcard 不是一回事
做问题定位前,建议你先分清/data与/sdcard这两个路径各自的挂载点、格式和特点,它们的管理方式是截然不同的:
| 路径 | 实际挂载节点 | 文件系统 | 主要用途 | 常见坑 |
|---|---|---|---|---|
| /data | /dev/block/by-name/userdata | ext4 | 应用私有数据、系统核心数据 | inode耗尽、加密、权限错乱 |
| /data/media | 同一ext4设备,media子目录 | ext4 | FUSE的底层存储池 | 常被误认为"独立文件系统" |
| /storage/emulated/0 | FUSE/sdcardfs挂载点 | 无(虚拟) | 应用可见的用户空间 | 权限策略复杂,容易Permission denied |
| /system、/vendor | 各分区 | ext4/erofs | 系统只读程序 | 只读属性限制,无法直接修改 |
有了这张图,后面所有问题的排查思路就清晰了:先搞清楚当前卡点是出在用户态权限层、挂载层,还是底层ext4本身,然后再对症下药。
2. 最常踩的坑:/storage/emulated/0/Android/data 操作失败与权限之谜
行业里有一类问题出现的频率高得惊人,就是操作/storage/emulated/0/Android/data/下的目录时报错,最经典的就是:
adb shell $ chmod 777 /storage/emulated/0/Android/data/com.example.app chmod: '/storage/emulated/0/Android/data/com.example.app': Operation not permitted类似的还有ls能列出目录但cd进不去、某些文件管理器打开/Android/data后一片空白、写文件时提示EACCES或Operation not permitted。这些问题表面上像ext4权限位问题,实际上三者叠加。排查过一次之后,你就知道真正限制你的并不是文件系统本身。
2.1 根因拆解:分区存储、SELinux、FUSE的三层封锁
第一层是分区存储限制。Android 11开始,系统强制应用无法直接列举或随意访问其他应用的/Android/data目录,这是framework层加的限制。你从adb shell里操作也会受这个限制,因为shell本身也是一个"应用",同样被scoped storage的策略约束。第二层是SELinux策略。你执行chmod、chown这类操作时,VFS层会检查进程的安全上下文有没有对应的权限。shell域(u:r:shell:s0)对某些/storage下的节点做写操作或改属主操作,很容易被avc: denied拦下来。判断方法非常简单:在logcat或dmesg里搜avc,如果有类似avc: denied { setattr } for pid=... comm="chmod"的记录,那就是SELinux的事。第三层是FUSE挂载选项。FUSE这一层在Android里启动时,通常会根据传递给守护进程的参数来决定允许哪些操作透传到底层文件系统。有时候连root都会被卡住,是因为FUSE层直接拒绝了这个操作,根本没到ext4那一层。
打个比方:ext4相当于一个写字楼的保险柜,FUSE是前台接待,SELinux是门卫。你想进保险柜改东西,先得过门卫、再通过前台登记,前台还规定了"你只能进你自己的办公室"。很多人抱怨文件系统"坏了",其实只是被门卫和前台拦住了而已。
2.2 完整排查链路:一次从报错到底层原因的追踪
这里是我在实机上完整走一遍的排查过程,你可以照着来。目标设备是Android 13的一台测试机,操作是尝试修改/storage/emulated/0/Android/data下某个第三方应用目录的权限:
第一步,先确认路径是不是真的存在:
adb shell ls -ld /storage/emulated/0/Android/data/com.example.app如果能列出来但ls里边的子项为空,那大概率不是ext4问题,而是FUSE层给你返回了一个"空视图"。
第二步,写一个最小文件测试:
adb shell touch /storage/emulated/0/Android/data/com.example.app/test.txt同样报Operation not permitted,基本可以排除"路径不存在"这类低级错误,问题聚焦到权限层。
第三步,抓内核日志和logcat:
adb shell dmesg | grep avc adb logcat -d | grep -i storage如果 dmesg 里有avc: denied,方向就明确了——SELinux拦截。如果 logcat 里有Permission denied且来源是ExternalStorageProvider,那就是分区存储限制。
第四步,尝试从另一个上下文访问——用root或换成支持MANAGE_EXTERNAL_STORAGE权限的调试应用。以root身份执行相同操作,如果能成功,就更进一步证明:ext4层和块设备完全健康,纯粹是上层策略问题。
2.3 实际可行的绕过思路(仅限调试场景)
明白了原因,就得知道怎么在调试中"破局"。针对常见场景,我整理了一套稳定可行的做法:
- 场景A:仅需读取App沙盒文件。Android 11及以上,用adb连接设备后在开发者选项打开"文件"权限(有的厂商叫"USB调试(安全设置)"),或者用支持SAF的普通文件管理器授权访问。简单可靠。
- 场景B:需要在shell下操作但手头没有root。可以考虑用
adb shell appops set --uid <uid> MANAGE_EXTERNAL_STORAGE allow来给特定应用授予访问权限,注意需要对应权限才能执行成功。 - 场景C:有root的调试设备。root后先执行
setenforce 0临时关闭SELinux(仅调试设备,生产机千万别这么干),再操作/data和/sdcard路径,通常就能畅通。操作完记得setenforce 1恢复。 - 场景D:要读写
/data/data/<package>这类内部存储目录。这不是/storage/emulated/0,而是挂在真正的ext4/data分区上,同样受SELinux和DAC权限控制,root后配合chmod和chown常规处理即可。
核心思路就是:先判断"不允许"来自哪里,再决定用哪个层级的手段去解决。我见过不少人在没有root的机器上跟SELinux硬肝、跟分区存储较劲,毫无意义——你需要的不是改ext4,而是获得一个更高权限的上下文。
3. 根文件系统挂载与只读问题:从NFS v3到remount rw的实战排查
接下来是嵌入式开发、系统移植里几乎天天要面对的一类问题。做嵌入式Linux、Android系统裁剪、用户态调试时,根文件系统挂载不上、只读挂载修改不了、或者挂载后sync写不进去,是很典型的现象。我在板子上用NFS v3挂载根文件系统时踩过的坑尤其多,值得单独拿出来说说。
3.1 为什么开发阶段用NFS v3挂根文件系统
嵌入式Linux调试阶段,把根文件系统放在开发主机上通过NFS共享给目标板,可以说是个经典方案。好处很明显:改完代码不必烧写Flash,直接在宿主机上同步文件,目标板上重启或重挂就生效,调试迭代速度极快。Android生态里,AOSP源码编译产物也能通过NFS挂载,配合adb root、adb remount来调试系统镜像。
NFS v3和v4相比,挂载参数上有个要特别注意的差异——v3对文件锁和权限模型的支持比较基础,出问题往往是参数配置问题。常见的是挂在目标板上后提示nfs: server X not responding, still trying,这种就是网络层面连通或重传次数设置不合理;还有的是挂载成功后读写全部Read-only file system,这就是服务端导出选项或客户端挂载选项把读写禁了。
举个例子,我在一块RK平台的板子上,宿主机/srv/nfs/rootfs作为根目录,exports文件这样写:
/srv/nfs/rootfs 192.168.1.0/24(rw,sync,no_root_squash,no_subtree_check)目标板上执行:
mount -t nfs -o nolock,rw,vers=3 192.168.1.100:/srv/nfs/rootfs /mnt/rootfsnolock这个参数经验上建议加上,因为v3默认会尝试调用rpc.statd做文件锁,目标板环境里这套服务经常没启动,加上nolock能直接省掉很多莫名其妙的Input/output error。确保rw同时出现在服务器端的导出选项和客户端的挂载选项里,有一边漏了就只读。
3.2 目标板上remount rw失败的排查链路
开发阶段更新系统分区,最常用的命令是:
adb root adb remount或者shell里手动执行:
mount -o rw,remount /system但实际中remount经常失败。请记住一个核心机制:内核不允许对一个文件系统执行与初始化挂载选项冲突的重新挂载,尤其涉及只读转可写时,块设备本身不能被占用写缓存或处于不安全状态。这句话展开就是三个常见原因:
第一个原因是分区本身有verity保护。在Android 10及以上版本很多系统分区用dm-verity把块设备设成了只读校验模式,单纯remount改不了底层的dm目标,即使VFS层允许也会被拒绝。对应解法是关闭AVB/verity验证,这需要 unlock bootloader 并刷入 userdebug/eng 版本,或者用adb disable-verity。第二个原因是块设备正忙。如果某个进程持续占用着该系统分区上打开的文件,remount时内核无法安全切换读写状态,会报Device or resource busy。排查方法是lsof +f -- /system或fuser -m /system找出占用进程。第三个原因是陈旧挂载状态。文件系统以只读挂载时,如果某些元数据写缓存没有完全落盘,直接remount也会失败,这种可以先sync再试。
顺带一提:sync后 remount 仍然失败的,考虑是不是ext4的日志(journal)区出了问题。我在处理一个长时间异常断电的板子时,卡在remount上怎么都过不去,最后fsck.ext4 -fy /dev/block/xxx修复完日志问题立刻就好了。一定要先备份,fsck在生产机上的风险你得自己权衡。
3.3 Fastboot刷机时的分区只读问题
还有一个高频场景在fastboot模式:想fastboot erase cache或fastboot flash boot时报FAILED (remote: not allowed in locked state)。这也是"文件系统问题"的一种近亲——不是文件系统坏了,而是bootloader处于locked状态,禁止对分区做写操作。解决办法是:
fastboot oem unlock # 部分厂商是 fastboot flashing unlock解锁会清空数据,这也是很多人"刷机后数据全没了"的原因。这类问题的本质是设备安全策略,不是ext4故障,但排查时如果不知道这条,很容易往文件系统损坏方向瞎想。
4. 存储空间"假满"、文件损坏和诡异的CPU 100%:底层ext4的排查方法
终于到真正的ext4底层问题。这类问题通常有比较明显的特征:df显示空间足够但写文件报No space left on device;文件读到一半报I/O错误;设备莫名其妙卡顿、CPU被内核线程吃满。每一类都有清晰的排查路径。
4.1 空间显示充足却"No space left on device"
新手最容易懵的就是这个场景:df -h /data还剩3G,touch test却提示No space left on device。这里要普及一个知识点:ext4不是只按块大小存文件,它还要用inode记录文件的元数据(权限、属主、时间戳、数据块指针等)。inode数量在格式化时就固定了,不会随着文件增大而增加。如果分区里塞满了大量碎片小文件(比如某些App的缓存、日志文件),可能会出现"数据块还剩一堆,但inode全耗光了"的尴尬情况。
排查命令:
df -i /data重点关注IUsed%和IFree两列。如果IUsed%接近100%,那没跑了。解决思路是找到“小文件大户”清理:
# 统计/data下各目录的inode占用 for d in /data/*; do echo "$d: $(find $d -type f 2>/dev/null | wc -l)"; done | sort -t: -k2 -rn | head或者在/data下用du --inodes -d 3快速定位。清掉大量临时小文件后,inode释放,问题迎刃而解。这个坑在系统长期运行的设备和高频写入日志的Android盒子上特别常见。那还有没有可能是inode之外的问题?有,比如磁盘配额(quota)超限、F2FS与ext4混用等,但inode耗尽是最容易忽略也最典型的。
4.2 文件系统损坏:kye、dmesg和fsck的配合
异常断电、强制下电、硬件链路不稳,都会导致ext4出现不一致。轻则个别文件损坏,重则整个分区无法挂载。这类问题我在嵌入式设备上遇到得最多,尤其是那些供电不稳、喜欢直接拔电源的项目。
核心排查工具就是dmesg和fsck。内核在挂载时检测到ext4日志异常,通常会打这样的日志:
EXT4-fs error (device mmcblk0p25): ext4_find_entry: ...或者:
EXT4-fs (mmcblk0p25): warning: mounting fs with errors, running e2fsck is recommended看到这类日志,立刻停掉对该分区的写操作,然后进入 recovery 模式或把盘挂到主机上做离线检查:
umount /dev/block/mmcblk0p25 fsck.ext4 -fy /dev/block/mmcblk0p25关于-f和-y多说一句:-f是强制检查,哪怕文件系统标记为clean也执行完整扫描;-y是对所有修复询问自动回答yes。在无人值守的设备上-y很实用,但也要知道它可能在某些极端情况下丢弃部分受损数据。重要设备上,最好先做块级镜像再修复。
另外要养成一个习惯:任何Android/嵌入式文件系统排查都不要只依赖一套工具。e2fsck用于检查ext4;debugfs可以在文件系统离线时查看和修改底层结构,比如找回被删除的文件(前提是块还没被覆盖);blkid查看块设备UUID和文件系统类型,排查挂载错分区的问题。这几样配合起来,基本能应对90%的底层损坏问题。
4.3 线上CPU 100%时的文件系统排查视角
关于"线上服务器的CPU使用达到100%了,如何排查、定位和解决"这个话题,虽然听起来更偏通用运维,但在Android设备上也会出现类似问题——系统卡顿到几乎不可用,top看到kworker或jbd2占满CPU。这里有一个重要的排查方向:jbd2是ext4的日志提交线程,它持续占满CPU往往意味着文件系统在做大量的元数据提交或反复重放日志,底层存储性能也极差或出现坏块。
排查链路可以这样走:先top -H看CPU占用最高的线程,如果看到jbd2/mmcblk0p25-8这种名字,就锁定是文件系统日志线程在作怪。接着用dmesg | tail看有没有大量的I/O错误,再用iostat -x 1(如果有)看%util、await的异常情况,把问题细化到是块设备性能瓶颈、坏块重试,还是日志区反复报错。如果指向坏块/存储寿命问题,大概率得换硬件或调整文件系统的挂载参数(例如commit=600降低日志提交频率,能显著减少存储写入负载)。
值得强调的是,CPU 100%只是一个症状入口,文件系统只是可能导致CPU跑满的源头之一,也存在内存回收风暴、驱动异常等其他可能。这里想提醒的就是:排查时不要只看进程列表,要把内核线程和I/O栈一起看进去,才不会在应用层绕圈子。
5. 日常自救清单:把常见问题在5分钟内分个类
为了让上面这些经验在日常工作中更可用,我习惯把存储/文件系统问题先在脑子里快速分个类。你可以把它当成一张自查表,每次报障都先过一遍,能省下大量无头苍蝇式的排查时间。
| 症状 | 第一怀疑对象 | 首要排查命令 | 常见解法 |
|---|---|---|---|
| 能df能看到分区但无法访问 | 挂载点未挂载或挂载参数不对 | mount | grep /data | 重新挂载,检查fstab参数 |
| 写文件Permission denied | SELinux或DAC权限 | dmesg | grep avc | 调整SELinux策略或chmod |
| Android/data目录内容为空 | 分区存储限制 | 换root / 授权App | 授予MANAGE_EXTERNAL_STORAGE |
| 空间够但写不了 | inode耗尽 | df -i /data | 清理小文件 |
| 掉电后文件打不开 | 文件系统不一致 | dmesg | grep EXT4-fs | fsck离线修复 |
| remount失败 | verity或设备被占用 | lsof +f -- /system | 关闭verity或kill占用进程 |
| CPU被jbd2占满 | 日志提交异常/坏块 | top -H+dmesg | 换盘或调整挂载参数 |
这套自查表里隐藏的一个核心方法论是:先定位层级(用户态/挂载层/内核块设备层),再选中具体工具,最后才决定操作。做文件系统排查这么多年,我发现绝大多数"疑难杂症"最后都是某一个环节的小问题,只是大部分人习惯在错误的层级里反复试探,才把事情越搞越复杂。
最后分享一个我个人的操作习惯:无论是问题排查还是日常维护,把原始命令输出存成日志文件。用adb shell dmesg > dmesg_$(date +%Y%m%d_%H%M%S).log这类方式保存现场,比事后回忆可靠得多。很多诡异问题,第一现场的信息稍纵即逝,错过那几分钟,可能得再折腾几小时才能抓回来。