1. IO性能基础:存储介质演进与IO栈全景
大家好,我是你们的老朋友。今天咱们来聊聊Android系统里一个特别“接地气”的话题——IO性能。说实话,我做了这么多年系统优化,发现很多性能问题,追根溯源都卡在IO上。CPU算得再快,内存再大,数据读写跟不上,一切都是白搭。
这一章,我们先打好基础。我会带你看看存储介质是怎么一步步变强的,再剖析一下IO栈的完整路径,最后聊聊那几个绕不开的性能指标。嗯,准备好了吗?
1.1 存储介质演进:从eMMC到UFS再到NVMe
先说说存储介质。你想想看,手机里的存储芯片,这些年经历了什么?
1.1.1 eMMC:曾经的王者
eMMC,全称是embedded MultiMediaCard。说白了,它就是把闪存颗粒和控制器封装在一起,对外提供一套标准接口。我记得早期做Android 4.4优化时,很多千元机用的就是eMMC 4.5,顺序读取也就200MB/s左右。
eMMC最大的问题是半双工——读写不能同时进行。你正在写数据,突然要读,就得等。这在多任务场景下特别吃亏。我曾经在项目里遇到过,后台下载文件时,前台滑动列表就卡成PPT,查了半天,罪魁祸首就是eMMC的读写冲突。
1.1.2 UFS:当前的主流
UFS(Universal Flash Storage)的出现,算是把手机存储带入了新纪元。它支持全双工,读写可以同时进行。UFS 2.1的顺序读取能到800MB/s,UFS 3.1更是飙到2000MB/s以上。
我个人习惯把UFS比作“双车道高速公路”——eMMC是单车道,会车就得停;UFS是双向四车道,各走各的,互不干扰。这也是为什么现在手机装应用、加载游戏那么快,UFS功不可没。
不过要注意,UFS也有坑。它的命令队列深度(Command Queue Depth)虽然比eMMC深,但处理不当还是会触发卡顿。我优化过一个案例,某款手机在微信聊天记录备份时,整机响应变慢,最后发现是UFS的写缓存策略太激进,导致前台IO被后台写操作阻塞了。
| 特性 | eMMC 5.1 | UFS 2.1 | UFS 3.1 | NVMe |
|---|---|---|---|---|
| 接口协议 | MMC协议 | SCSI协议 | SCSI协议 | NVMe协议 |
| 全双工 | 不支持 | 支持 | 支持 | 支持 |
| 顺序读取 | ~250 MB/s | ~800 MB/s | ~2100 MB/s | ~3500 MB/s |
| 随机读取(4K) | ~10K IOPS | ~30K IOPS | ~60K IOPS | ~200K IOPS |
| 典型应用 | 低端手机 | 中端手机 | 旗舰手机 | PC/服务器 |
1.1.3 NVMe:未来的方向
NVMe(Non-Volatile Memory Express)最初是为PC和服务器设计的,现在也开始出现在高端Android设备上。它最大的特点是极低的延迟和超高的队列深度——最多支持64K个队列,每个队列64K个命令。相比之下,UFS的队列深度只有32左右。
为什么NVMe这么快?说白了,它绕过了传统的SCSI协议栈,直接通过PCIe总线与CPU通信。路径短了,延迟自然就低了。我测试过一款搭载NVMe的工程机,4K随机读取IOPS轻松突破20万,是同期UFS 3.0的三倍多。
1.2 IO栈全景图:从应用到硬件
好了,介质讲完了。接下来我们看看,一次简单的文件读写,在系统里到底经历了什么?
我习惯把IO栈分成三层:VFS(虚拟文件系统)、Block Layer(块层)、Driver(驱动层)。每一层都有自己的职责,也都有自己的瓶颈。
1.2.1 VFS层:统一的门面
VFS是Linux内核给用户空间提供的统一接口。不管你底层是ext4、f2fs还是其他文件系统,VFS都让你用同样的open/read/write来操作。说白了,它就是个“翻译官”。
VFS层主要做三件事:路径查找、权限检查、缓存管理。其中,缓存管理(Page Cache)对性能影响最大。你读一个文件,VFS会先看Page Cache里有没有,有就直接返回,没有才去读磁盘。这就是为什么第二次打开应用比第一次快得多。
不过,Page Cache也不是万能的。我遇到过一个问题:某款手机在连续拍照后,系统变得异常卡顿。分析后发现,相机应用写入大量图片,把Page Cache撑爆了,导致其他应用的缓存被频繁回收,引发“缓存抖动”。
1.2.2 Block Layer:调度与合并
数据从VFS下来,就进入了Block Layer。这一层负责两件事:IO调度和请求合并。
IO调度器决定哪个请求先处理。Android常用的调度器有:
- CFQ(完全公平队列):为每个进程分配时间片,公平但延迟高。老内核用得多。
- Deadline:给每个请求设置截止时间,读请求优先。适合数据库场景。
- BFQ(预算公平队列):基于进程的IO预算做调度,交互性好。很多手机默认用这个。
- Kyber:根据延迟动态调整,轻量级。适合NVMe这类高速设备。
请求合并也很关键。你想想看,如果上层发来一堆4KB的小请求,Block Layer会把它们合并成一个大的请求(比如128KB),再发给驱动。这样能显著减少硬件交互次数,提升吞吐量。
1.2.3 Driver层:最后的执行者
Driver层直接和硬件打交道。对于UFS来说,就是UFS驱动;对于eMMC,就是MMC驱动。这一层负责发送SCSI命令、处理中断、管理电源状态。
驱动层的性能瓶颈通常出现在两个方面:
- 中断处理:每次IO完成都会触发中断,中断太多会消耗CPU。现代驱动会用“合并中断”或“轮询模式”来缓解。
- 电源管理:存储设备有多个电源状态(Active、Idle、Sleep)。频繁切换状态会增加延迟。我见过一个案例,某款手机在息屏后唤醒时,IO延迟飙升到200ms,就是因为UFS从Sleep状态恢复太慢。
1.3 IO性能关键指标:IOPS、吞吐量、延迟
聊了这么多,怎么衡量IO性能好不好?三个指标:IOPS、吞吐量、延迟。缺一不可。
1.3.1 IOPS:每秒能处理多少个请求
IOPS(Input/Output Operations Per Second)衡量的是并发处理能力。它特别看重随机读写场景,比如数据库查询、应用启动时加载小文件。
举个例子:你打开微信,系统要读取几百个配置文件、数据库文件、缓存文件。每个文件可能只有几KB到几十KB。这时候,IOPS越高,加载越快。
我测试过不同存储介质的4K随机读IOPS:
- eMMC 5.1:约8K-12K IOPS
- UFS 2.1:约25K-35K IOPS
- UFS 3.1:约50K-70K IOPS
- NVMe:约150K-250K IOPS
注意,IOPS不是越高越好。如果IOPS很高但单个请求延迟也高,用户体验反而会变差。这就是为什么我们要看第三个指标——延迟。
1.3.2 吞吐量:每秒能传输多少数据
吞吐量(Throughput)衡量的是数据传输速率,单位是MB/s。它更看重顺序读写场景,比如拷贝大文件、播放高清视频。
吞吐量受两个因素影响:IO大小和IOPS。公式很简单:吞吐量 = IOPS × IO大小。所以,如果你用1MB的块做顺序读写,IOPS只要1000,吞吐量就能到1000MB/s。但如果你用4KB的块做随机读写,IOPS需要25万才能达到1000MB/s——这几乎不可能。
在实际项目中,我通常这样区分:
- 如果应用主要做顺序读写(比如视频录制、文件下载),优先优化吞吐量。
- 如果应用主要做随机读写(比如数据库、应用启动),优先优化IOPS。
1.3.3 延迟:每次请求要等多久
延迟(Latency)是用户最直观的感受。你点一下屏幕,等了半秒才有反应,这就是延迟在作祟。
延迟分为两种:
- 平均延迟:所有请求的平均等待时间。通常用毫秒(ms)表示。
- 尾延迟(P99/P99.9):最慢的那1%或0.1%请求的延迟。这个指标特别重要,因为用户感知到的卡顿,往往就是这些“慢请求”造成的。
我优化过一个案例:某款手机在刷微博时,偶尔会出现“图片加载卡住”的现象。分析IO延迟后发现,平均延迟只有5ms,但P99.9延迟高达800ms。进一步排查,发现是UFS在后台做垃圾回收(GC)时,前台IO被阻塞了。后来我们调整了GC触发策略,把P99.9延迟降到了50ms以内。
好了,这一章的内容就到这里。存储介质的演进、IO栈的分层、三个核心指标,这些都是后续章节的基础。下一章,我会带你深入VFS层,看看Page Cache到底是怎么工作的,以及如何优化它。我们下次见!