4. IO栈与内核机制
在 Android 系统中,一次看似简单的文件读写,往往要穿过从应用到硬件的一整条 IO 栈。理解这条栈的层次与各层职责,是定位 IO 性能瓶颈、判断优化方向的基础。本节从 VFS 到块设备,逐层拆解一次 IO 请求的完整旅程,并重点对比直接 IO 与缓冲 IO 的差异。
一、IO 栈的整体分层结构
Android 基于 Linux 内核,其 IO 路径自上而下大致分为五层:应用层、VFS 层、文件系统层、页缓存、块层与 IO 调度器,最终到达物理存储设备。每一层都有明确的抽象职责,上层无需关心下层的实现细节。
二、VFS:统一抽象的入口
VFS(Virtual File System)是内核中承上启下的关键层。它定义了一组通用对象模型:super_block 描述已挂载的文件系统,inode 描述文件元数据,dentry 描述目录项,file 描述进程打开的文件实例。无论底层是 ext4、f2fs 还是 erofs,应用调用 read() 时都通过 VFS 的 file_operations 函数指针分发到具体文件系统实现。
VFS 的价值在于解耦:应用只面向统一的 POSIX 接口,文件系统只需实现自己的操作集。Android 中常见的 /data 使用 f2fs,/system 使用 erofs,/sdcard 可能使用 FAT/exFAT,但应用代码无需区分。
三、页缓存:性能的关键缓冲
页缓存是内核用空闲内存缓存文件数据页的机制,默认页大小为 4KB。当应用读取文件时,内核先查页缓存:命中则直接拷贝到用户缓冲区,无需访问磁盘;未命中则触发缺页,从块设备读取并填充缓存。
写入时默认采用回写(writeback)策略:数据先写入页缓存并标记为脏页,由内核回写线程在合适时机批量刷盘。这带来两个重要影响:一是写操作延迟极低,二是数据在掉电时可能丢失。Android 中可通过 fsync() 强制刷盘,但频繁调用会显著增加 IO 延迟。
# 查看页缓存与脏页情况
cat /proc/meminfo | grep -E "Cached|Dirty|Writeback"
# 手动触发回写
sync
# 查看某进程的 IO 统计
cat /proc/<pid>/io
四、块层与 IO 调度器
文件系统将 IO 请求转化为 bio 结构,块层负责将其合并、排序并放入请求队列。IO 调度器决定这些请求以何种顺序下发给设备,直接影响随机 IO 的吞吐与延迟。
| 调度器 | 适用场景 | 特点 |
|---|---|---|
| none | NVMe / UFS 等高速设备 | 不做排序,直接下发,依赖设备自身并行能力 |
| mq-deadline | 通用场景 | 保证请求在截止时间内完成,兼顾吞吐与延迟 |
| BFQ | 桌面/交互敏感场景 | 按权重分配带宽,交互流畅但吞吐略低 |
| kyber | 低延迟设备 | 基于延迟目标控制队列深度 |
Android 设备通常根据存储类型选择调度器。UFS 设备多使用 none 或 mq-deadline,以充分发挥硬件并行度。
五、直接 IO 与缓冲 IO 的对比
缓冲 IO 是默认路径,数据经过页缓存,适合大多数场景,尤其是重复读取和小块写入。直接 IO 通过 O_DIRECT 标志打开文件,绕过页缓存,数据在用户空间与设备之间直接传输。
// 缓冲 IO:默认路径,经过页缓存
int fd = open("/data/test.dat", O_RDWR);
read(fd, buf, 4096);
// 直接 IO:绕过页缓存,需对齐
int fd = open("/data/test.dat", O_RDWR | O_DIRECT);
posix_memalign(&buf, 4096, 4096); // 缓冲区需按页对齐
read(fd, buf, 4096);
直接 IO 有严格的对齐要求:缓冲区地址、文件偏移、读写长度通常都需按块大小(512B 或 4KB)对齐,否则会返回 EINVAL。Android 中 SQLite 在 WAL 模式下会谨慎使用直接 IO,以平衡一致性与性能。
六、典型场景与误区复盘
场景:某应用启动时读取大量配置文件,首次启动耗时 800ms,二次启动仅 120ms。原因正是页缓存命中:首次读取需从存储加载,二次读取直接从内存返回。
误区一:认为 write() 返回即数据已落盘。实际上数据仅进入页缓存,掉电可能丢失。需要持久化时应调用 fsync()。
误区二:盲目使用直接 IO 追求“高性能”。直接 IO 绕过了缓存合并与预读,小块随机读写性能反而更差,且对齐要求增加开发复杂度。
/proc/meminfo 中的 Dirty 页数量。脏页过多会触发集中回写,造成 IO 毛刺。可通过调整 vm.dirty_ratio 和 vm.dirty_background_ratio 平滑回写压力。