☰
Android存储疑难杂症:从Ext4到jbd2的底层排查指南
2026/10/8 10:43:18 网站建设 项目流程

最近连续被几波用户报障拉进了同一个泥潭:手机存储空间显示还剩几十GB,但App就是打不开已经下载好的内容;游戏把资源包写进了/storage/emulated/0/Android/data/com.tencent.tmgp.sgame/files/pandora/这个目录,结果自己都读不回来;更夸张的是有同事反馈线上服务器CPU直接飙到100%,拔日志一看全是jbd2和sync在等锁。排查到最后,大家不约而同翻出了内核里 Ext4 文件系统的那本老账。

这个现象在Android生态里太典型了。很多人一看到卡顿、CPU满载、文件消失、权限报错,第一反应是查内存、清缓存、怪ROM优化差,但真正做过底层问题定位的工程师都知道,大量疑难杂症最终都会落到文件系统这一层:要么是Ext4的日志线程卡住了,要么是延迟分配导致文件没真正落盘,要么是应用目录权限隔离带来的“看得见但摸不着”。这篇文章就来把我最近一轮完整的排查思路、关键命令和几个能直接复用的案例复盘一遍,希望能帮你在下次遇到类似问题时少走弯路。

1. 先搞明白:Ext4 在 Android 里到底扛着哪几摊事

1.1 /data、/system、/sdcard 之间的关系

Android的存储体系和一台传统Linux服务器不太一样,它是多个分区各司其职。老的Android设备上,/system分区通常是只读挂载的Ext4,里面放系统镜像和预装应用;/data分区负责所有应用数据、用户配置和WiFi密码等,通常也是Ext4;到了用户能直接看到的“内部存储”,也就是/storage/emulated/0,底层其实是挂载在/data/media/0下的一个目录,再由sdcardfs或fuse做了一层映射。

很多排查的人会犯一个错误:把/storage/emulated/0当成一个独立分区来查。实际上它的数据落在/data分区里。也就是说,/data分区的Ext4文件系统出现异常,既可能表现为应用崩溃、数据库损坏,也可能表现为你手机相册里的照片打不开。理解这个层级关系,比记住任何一条命令都重要。

现在的手机硬件升级很快,部分新机型为了提升随机读写性能,已经在/data分区改用F2FS,但Ext4在系统分区、老旧设备、以及大量国产定制ROM里仍然占大头。所以排查时先别急着套经验,第一步永远是看清楚当前设备的分区格式:

adb shell mount | grep -E " /data | /system "

这一条命令能立刻告诉你每个关键分区用了什么文件系统、挂载了哪些选项。比如看到rw,seclabel,noatime,errors=panic这种挂载参数,你就知道这个分区开了noatime,而且文件系统错误时会直接触发内核panic而不是只读降级,这对后续策略选择很关键。

1.2 症状和文件系统的对应关系

把常见故障按文件系统视角重新分类之后,你会发现很多看似无关的症状其实是同一个根因。

  • CPU 100%但实际是I/O卡死:多见于jbd2日志线程在刷盘时抢锁,或者block层IO队列被慢速存储拖死,这部分后面会详细展开。
  • App提示“文件不存在”/“下载失败”:有时候不是文件真的丢了,而是应用把文件写在了自己的私有目录里,另一个进程根本无法访问,或者文件系统延迟分配后还没把数据真正刷到存储介质上。
  • 重启后东西消失:Ext4默认有延迟分配机制,没来得及落盘的数据遇到异常断电或强制重启,就真的丢了。
  • 权限操作失败:比如unable to chmod '/storage/emulated/0/android/data/...': Operation not permitted,这种情况多半碰上了作用域存储和FUSE文件系统的硬限制。

拿到问题先归个类,再去想是查内核栈、跑e2fsck还是改应用代码,这样排查效率会高很多。

2. 一卡顿就怪磁盘?先查这几个内核机制

2.1 日志线程 jbd2 和 sync 的冲突

Ext4 是个带日志的文件系统,jbd2线程负责把元数据变更写进日志,保证掉电后能重放。这个机制平时很安静,但一旦日志设备(也就是存储芯片)响应变慢,或者日志提交需要的小锁争抢激烈,问题就来了。

我遇到过几次比较典型的卡顿现场,top里 CPU 占用最高的不是某个App,而是jbd2/mmcblk0p57-8这种内核线程。再往深了看,整个系统的进程都在等一个叫fsync的系统调用返回,而这些调用几乎都阻塞在文件系统内部的事务提交上。

简单说就是:日志线程把元数据变更集中打包提交,如果这个提交过程被卡住,后面所有需要文件系统一致性的操作全部排队。哪怕你手机CPU有八个核,依旧表现为系统级卡死。排查时可以关注这个:

adb shell dmesg -T | grep -i "jbd2\|ext4"

如果看到大量callbacks suppressed、end_request I/O error、JBD2: Detected IO errors while flushing file buffer之类的日志,基本可以断定问题不在用户态,而在文件系统与存储设备之间。

2.2 dirty page 堆积、writeback 和 CPU 占用的关系

另外一个常年背锅的机制是 writeback。应用写文件时,数据先进入Page Cache,标记为dirty,再由内核的writeback线程落盘。Ext4的延迟分配更进一步:它可能连磁盘块号都还没定,先给你一个“已经写入成功”的假象,真正分配和落盘都在后台完成。

这个设计对性能有帮助,但对排查制造了很大的迷惑性。表现就是CPU曲线看起来狂飙,其实是kworker/u线程和jbd2线程在疯狂刷脏页,I/O队列打满,CPU大部分时间花在等iowait上。你用传统的CPU分析工具去看,火焰图里全是submit_bio、__ext4_find_entry这类调用,很容易误判成Redis或者数据库问题。

想确认是不是脏页堆积,可以看两个文件:

adb shell cat /proc/meminfo | grep -E "Dirty|Writeback" adb shell cat /proc/vmstat | grep -E "nr_dirty|nr_writeback|pgwriteback"

当Dirty数值长期不下降,或者nr_dirty还在持续增长,说明后台刷新速度跟不上写入速度。在低端eMMC设备上尤其典型,因为这类存储的随机写延迟高,刷脏页的吞吐上不去,最终导致CPU高、App等待、ANR连环出现。

2.3 VFS 层速查:什么时候该看什么

遇到文件系统问题,VFS(虚拟文件系统)层是整个Linux存储栈的“总调度台”。sync、fsync、fdatasync这些系统调用都要先经过VFS,再分发到具体文件系统。很多时候我们查/proc/pid/stack看到线程停在sync上,并不代表Ext4有问题,而是Ext4下层拿不到足够的IO带宽。

我的经验是,排查顺序应该是:先看是“进程主动等IO”还是“文件系统内部锁等待”。前者通常是存储硬件性能不足或IO调度策略问题,后者往往和日志提交、元数据锁、truncate线程有关。可以通过/proc/pid/stack大概区分:

adb shell cat /proc/12345/stack

如果栈顶停在wait_on_page_bit或ext4_wait_completion,多半是page回写没完成;如果停在jbd2_log_wait_commit,那是应用在等日志事务提交;如果卡在percpu_down_read这类锁上,那更可能是文件系统级或全局锁竞争。这些线索比单纯看CPU利用率精准得多。

3. 一次完整排查:从 100% CPU 到揪出 ext4 元数据锁

3.1 用 adb 快速抓现场

线上报障说“CPU到了100%,不知道在跑什么”,但CPU100%只是个现象,要在它降到0之前把现场留下来,顺序很重要。

我常用的套路是四连拍:先抓顶层CPU线程,再抓IO状态,然后抓对应线程栈,最后把内核日志拉出来。

# 1. 顶层线程CPU排序 adb shell top -H -b -n 1 # 2. 看IO等待和内存脏页 adb shell cat /proc/loadavg adb shell cat /proc/meminfo | grep -E "Dirty|Writeback" # 3. 找到异常进程后,抓它的线程栈 adb shell cat /proc/$PID/task/$TID/stack # 4. 内核诊断里最关键的几行 adb shell dmesg -T | tail -100 | grep -E "ext4|jbd2|fuse|sdcardfs|iowait"

这里有个容易踩的坑:很多Android设备对/proc/<pid>/stack直接读取会提示权限不足,需要root或者通过su -c来执行。在没有root的商用机上,退而求其次的做法是开启Systrace:

adb shell atrace --async_start -t 10 -b 8192 sched freq idle workqueue sleep 5 adb shell atrace --async_stop -o /data/local/tmp/trace.txt adb pull /data/local/tmp/trace.txt

通过sched事件能看到线程被调度到CPU上的频率和停顿点,虽然不如内核栈那么深,但也能定位到“线程卡在哪个内核函数”的上下文。

3.2 追踪线程栈和锁等待

如果没有root权限,又碰上系统级卡死,还有一个技巧:看dmesg里有没有低内存杀进程或IO错误,再配合/sys/kernel/debug/tracing抓函数调用。有root的情况下,我比较喜欢直接开一个小的ftrace脚本,专门跟踪jbd2提交路径:

adb shell "su -c 'echo 20480 > /sys/kernel/debug/tracing/buffer_size_kb'" adb shell "su -c 'echo > /sys/kernel/debug/tracing/trace'" # 只跟踪 ext4 和 jbd2 相关的函数 adb shell "su -c 'echo 0 > /sys/kernel/debug/tracing/events/enable'" adb shell "su -c 'echo 1 > /sys/kernel/debug/tracing/events/ext4/enable'" adb shell "su -c 'echo 1 > /sys/kernel/debug/tracing/events/jbd2/enable'"

跑一段时间再导出trace,你会看到这样的序列:jbd2_journal_start->ext4_journal_check_start->start_this_handle-> 后面是漫长的等待。这个等待往往就是事务提交次数太频繁、日志设备吞吐不够导致的锁竞争。

还有一次我在跑trace时发现,App线程大量进入fsync,每条线程都在等不同的日志事务,而日志所在分区的block层队列深度只有个位数,整个链路就从“文件系统锁竞争”演化成了“存储设备吞吐瓶颈”。如果只盯着Ext4的内部锁去调参数,问题根本解决不了。

3.3 日志合并分析:dmesg + logcat + blocked task

出现整机卡死时,光看一条日志往往不够。logcat里能看到的通常是应用层的ANR信息,比如“Application Not Responding”“Input dispatching timed out”。但触发方在文件系统时,应用层日志往往只有一堆android.os.FileUtils的耗时记录,看起来像应用bug,实际却是底层存储回不来。

我习惯把三层日志拼起来看:

  • dmesg看内核是否报告了blocked for more than 120 seconds,这是最直接的“内核线程卡死”信号。
  • /proc/pid/tasks下的各个线程状态,如果有大量D状态(不可中断睡眠),说明进程卡在等待IO。
  • logcat -b system里搜Watchdog和am_anr附近的时间点,确定用户可感知的卡死窗口。

三层日志的时间对齐很重要。Android设备如果设置了自动同步网络时间,logcat的时间戳和内核实时时间可能不一致,我一般先执行adb shell date和adb shell "cat /proc/uptime"做对齐。否则排查十几分钟,最后发现三段日志对不上,非常憋屈。

4. 分区坏了怎么救:e2fsck 的使用时机与边界

4.1 常见损坏表象和可恢复性判断

Ext4作为一个成熟文件系统,单点故障率不算高,但架不住低端eMMC的坏块、异常掉电、以及一些第三方内核模块的不当操作。最典型的表现是:正常使用的手机突然进入只读模式,或者某天开机后卡在启动动画,进Recovery能看到metadata_csum校验失败。

要判断是否损坏,先读出超级块信息:

adb shell "dumpe2fs -h /dev/block/bootdevice/by-name/userdata"

重点看三样东西:文件系统状态(clean还是not clean)、挂载次数、错误处理方式。如果状态显示errors虽然还能挂载,但每次开机都会反复回放日志,建议尽早备份重要数据,因为这意味着底层存储可能开始出现不稳定区域。

不是所有问题都要跑e2fsck。有些只是日志回放失败,正常重启就能恢复;有些是文件系统元数据逻辑错误,跑一次完全检查能修好;有些则是硬件坏块引发的,e2fsck只能标记坏块地址,救不了已经物理损坏的数据。动手修之前,一定先想清楚数据价值,不要拿一张正在崩溃的存储卡反复折腾。

4.2 在 recovery 下执行 fsck 的正确姿势

Android设备进Recovery模式后,不同品牌的可用命令差异很大。原生Recovery通常带一个fsck工具,小米、一加等机型的官方Recovery里也往往保留了完整工具链。

优先推荐的做法是OTA方式:adb reboot recovery,然后在Recovery菜单选择“清除数据”或者工厂复位,这本质上也是擦掉/data分区的用户数据。如果只是想修复文件系统而保留数据,需要确认Recovery里有e2fsck:

adb shell # 这里要看设备实际分区节点 e2fsck -f -y /dev/block/mmcblk0p68

-f是强制检查,即使文件系统状态看起来是clean也要查;-y是自动回答“是”,避免交互式检查卡住。但我要提醒一句:-y在处理冲突时可能会把一些文件移动到lost+found目录,使用者需要接受这个结果。

如果你的设备只能通过fastboot连接,也可以临时拉出一个支持完整e2fsck的内核命令行环境,但操作成本和风险都比较高。更稳妥的做法是拆下存储芯片在另一个设备上读出镜像再处理。不过对绝大多数人来说,数据都已经凶多吉少了,所以平时定期做照片、文档的云端备份,远比“出事后再想ertools修复”靠谱。

4.3 Windows 上查看 ext4 的几种现实手段

很多做测试和售后的人,设备是Windows,但拿到的分区镜像却是Ext4格式,直接在Windows资源管理器里是看不见的。这里分享几个我实测过的方式。

第一种是用现成的读取工具,比如 DiskGenius、Linux Reader 这类软件,可以直接识别Ext4分区,浏览并导出文件。优点是速度快,适合单文件提取;缺点是对metadata_csum等新特性的支持有时不完整,部分工具看到64bit特性的分区会认成未格式化。

第二种是开WSL2,在里面把读出来的分区镜像通过loop设备挂载:

sudo mkdir /mnt/ext4 sudo mount -o loop,ro userdata.img /mnt/ext4

这块有个大坑:WSL2的默认内核并不带ext4的loop支持吗?其实内核已经内置,但Windows 11和Windows 10的镜像版本必须较新,否则错误百出。挂载之前先执行file userdata.img确认镜像格式,有些导出工具导出的是稀疏镜像,还需要先转换。

第三种是把完整镜像dd出来之后,在另一台Linux服务器上用dumpe2fs检查超级块,确认有没有损坏。这也顺便解决了“多拷几遍会不会弄坏全盘”的担忧,因为只读挂载的最坏后果也就是把坏块信息报一遍,不会继续写数据。

5. 文件“找不到”和“打不开”的另类根因:作用域存储与路径权限

5.1 Android 10 之后的 scoped storage 对排查的影响

很多时候文件并没有丢,是应用层面的访问权限被系统隔离了。Android 10(API 29)开始强制作用域存储,应用访问其他应用在Android/data/下的文件,会直接报Permission Denied。

我在排查一个下载类App的问题时,用户反馈“文件下载成功了,但在文件夹里找不到”。查logcat发现App其实是通过MediaStore.Downloads写入文件,但因为作用域存储的限制,文件被收纳到了应用私有目录而没有真正出现在用户可浏览的公共目录里。这属于典型的新老接口混用问题,和Ext4没关系,但一旦你顺着dmesg去找文件系统日志,大概率一无所获。

这里给几条直接的判断方法:

  • 路径带/Android/data/前缀的,多半是应用私有目录,Android 11 之后第三方文件管理器也无法直接列目录。
  • 用户真的要在公共目录读到文件,应用应该写/Download、/Pictures等公共路径,并通过MediaStore通知扫描。
  • 遇到Operation not permitted,先查是SELinux策略还是作用域存储限制,不要一上来就跑chmod 777,因为FUSE层上根本不会真正改掉权限位。

5.2 /sdcard/Android/data/... 为什么有那么多限制

很多用户和开发者不理解,为什么明明是自己的手机,却连/storage/emulated/0/Android/data/com.tencent.tmgp.sgame/files/pandora/这种目录都进不去。

原因分两层。底层/data/media/0是普通Linux目录,但FUSE或sdcardfs在上层加了一道“权限滤镜”。系统对每个应用目录都会检查UID/GID,不同应用无法互访;系统还会对某些路径强制设置目录权限,应用就算有root权限去改,重启后也会被重新规整。

另一个常见坑是unable to chmod '/storage/emulated/0/android/data/com.xjs.ehviewer': Operation not permitted。这个报错不一定是文件系统损坏,而是FUSE层的chmod操作默认就不支持。你看到的是“操作不被允许”,实际原因是Android的安全设计不想让你通过chmod把私有目录变成公共目录。如果真的需要在应用之间共享文件,正确姿势是使用FileProvider,在XML里配置好路径共享。

5.3 FileProvider 混用引发的“预览失败”

另外一个高频问题就是“App里既打不开预览,下载的文件系统里又找不到”。这种通常出现在应用A把文件交给应用B处理的时候,代码里还在用file://协议。

Android 7.0 以后(API 24),跨应用分享文件必须用content://URI,由FileProvider生成临时授权路径。比如搜索热词里就有人百度某个content://com.baidu.searchbox.fileprovider/...的路径,这类fileprovider配置本身没什么问题,问题往往出在XML映射错了路径。

举个例子,应用A把文件写到了/data/data/com.example/cache/shared/,但FileProvider的<paths>里配的是external-files-path,映射关系对不上,应用B拿到的content://URI直接404。从用户视角看就是“下载好的文件打不开”。遇到这类报障,第一件事是让开发把getContentResolver().openInputStream(uri)返回值打出来,而不是去查底层分区。先确认URI映射,再决定要不要查文件系统。

6. 复盘三个真实案例

6.1 游戏 Pandora 目录下载件消失,其实是延迟分配没回写

有一次用户反馈,某游戏下载的资源包文件明明显示“下载完成”,但重启后再进去就提示文件损坏,路径指向/storage/emulated/0/android/data/com.tencent.tmgp.sgame/files/pandora/pr/。

排查时我先确认了dmesg里没有IO错误,再跑dumpe2fs看文件系统状态是clean。问题出在应用自己的下载逻辑:它只调用了FileOutputStream.write(),没有调fsync()或者FileDescriptor.sync(),数据停在Page Cache里,而用户又正好在写入结束的瞬间强制重启手机,延迟分配的块和日志事务都没提交,文件元数据写到了坏块日志之外的地方,重启后文件就不是完整状态。

这个案例给我最大的提醒是:文件系统不是数据库,别把“写入接口返回成功”当成“数据已经在硬盘上”。关键数据在写入后必须fsync,否则任何掉电、重启、内核panic都可能导致数据丢失。

6.2 下载文件打不开,根因在磁盘满和 extent 树异常

另一个案例是某个文件解压工具,用户批量解压Zip时提示“分区空间不足”,紧接着所有下载件都打不开。检查发现/data分区可用空间为0,Ext4 在完全没有可用块的情况下,ext4_ext_insert_extent这类函数会反复重试,次数多了以后日志里出现EXT4-fs error (device mmcblk0p68): ext4_find_entry: ...,最终把分区标记为errors状态。

处理方式是用Recovery里的e2fsck -f -y清了一遍孤儿文件,然后删掉一个不常用的占位大文件,释放空间后再开机,文件就恢复了。这个案例说明,磁盘满不只是提示“空间不足”那么简单,它会诱发元数据层的连锁错误,甚至把原本健康的目录项搞成“疑似损坏”。日常运维里,给/data保留至少1GB以上的余量,比任何修复工具都管用。

6.3 定制系统里银行类App卡死,别忽略挂载点兼容性

最后一个是某国产定制ROM上,某银行类App一打开就卡在启动页,应用无响应。抓logcat时,满屏都是在访问/storage/emulated/0/android/data/com.xxx.xxx/files/log/时超时,执行mount一看,发现上层FUSE挂载点确实存在,但某个子目录的挂载状态异常,应用反复读取时FUSE迟迟没有响应。

这本质上不是数据丢失,而是定制ROM在做SD卡模拟层时,没有处理好挂载传播。App通过content://FileProvider去访问时,底层实际经过了sdcardfs -> /data/media,中间某个衔接没有正确初始化,导致读操作一直在内核里等待。

遇到这种问题,最有效的临时方案是清一下该App的数据缓存,让应用重新初始化目录;如果是系统级的问题,就要联系ROM研发确认挂载配置。普通用户不需要去看内核代码,但至少要知道:不是所有“打不开”都能靠重启解决,也不是所有“卡死”都是文件系统坏了,很多时候是上层虚拟文件系统和服务器的配合问题。

这轮排查下来,我自己最大的体会是,Android存储问题要分层看:应用层先确认Uri和权限,框架层再查sdcardfs/FUSE,最后才轮到Ext4和block层。很多人一步到位跳到“e2fsck修一下”,反而容易把真正根因盖住。最后再分享一个小技巧:日常多抓几个正常状态下的mount输出和dmesg存档,出问题时和正常基线做对比,往往三两分钟就能锁定异常挂载点。这个习惯帮我避免过无数次重复排查,建议你也养成。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询