早期的 Block 框架是单队列(single-queue)架构,适用于“硬件单队列”的存储设备(比如机械磁盘),随着存储器件技术的发展,支持“硬件多队列”的存储器件越来越常见(比如 NVMe SSD),传统的单队列架构也因此被改成了多队列(multi-queue)架构。早在 3.13 内核就已经加入了多队列代码,但是还不太稳定,经过多年的发展 multi-queue 越来越稳定,linux 5.0+ 已经默认使用 multi-queue。本篇文章介绍 Block 层框架及调度器相关知识,让读者对 Block 层有一个宏观的认识。
一、Block 层的作用
用户发起读写操作时,并不是直接操作存储设备,而是需要经过较长的 IO 栈才能完成数据的读写。读写操作大体上需依次经过虚拟文件系统 vfs、磁盘文件系统、block 层、设备驱动层,最后到达存储器件,器件处理完成后发送中断通知驱动程序,流程见图 1。
图1 IO 栈
备注:page cache 机制用来提高性能。在内存资源不紧张的情况下,用户访问过的数据不会被丢弃,而是缓存在内存中,下次可以访问快速的内存中数据,无需访问慢速的存储设备。mapper layer 用来将用户操作文件偏移量转换成磁盘文件系统的 block 偏移量。
Block 层连接着文件系统层和设备驱动层,从 submit_bio 开始,bio 就进入了 block 层,这些 bio 被 Block 层抽象成 request 管理,在适当的时候这些 request 离开 Block 层进入设备驱动层。IO 请求完成后,Block 层的软中断负责处理 IO 完成后的工作。Block 层主要负责:
管理 IO 请求
IO 请求暂存、合并,以及决定以何种顺序处理IO请求。这里面涉及到 single-queue、multi-queue 框架以及具体的 IO 调度器。
IO 统计
主要是task io accounting统计各个进程的读写情况,统计信息见struct task_io_accounting。
注意,虽然 Block 层中存放着很多的 request,但正常情况下 Block 层不会主动“下发” request 给设备驱动程序(在线切换 IO 调度器、存储器件 offline 场景会主动下发 request)。当设备空闲时,设备驱动程序从 Block 层的“分发队列”头部依次取 request 进行处理,设备驱动程序拿到 request 后,根据 request 中的信息及器件协议生成 cmd 命令交由器件处理。
二、Block 框架演变
Block 层软件设计与存储器件的特性紧密相关,大致经历了 2 个阶段。
图2 single-queue与multi-queue 架构
*引用自Linux Block IO: Introducing Multi-queue SSD Access on Multi-core Systems
single-queue 框架
早期的存储设备是磁盘,特点是机械运动寻址、且不支持多硬件队列并发处理 io,所以代码逻辑自然地设计了一个软件分发队列,这种软件逻辑上只有一个分发队列的架构称作 single-queue 架构,由于软件本身的开销(多核访问 request queue 需要获取 request_queue->queue_lock 等原因),single-queue 的 IOPS 能达到百万到千万级别的数据量,由于早期存储器件速度慢,百万的 IOPS 已经完全能够满足需求。
multi-queue 架构
当支持多队列的高速存储器件出现后,器件端处理的时间变短,single-queue 引入的软件开销变得突出,软件成为性能瓶颈,导致性能瓶颈的因素有 3 个:</