本页目录

块设备层

覆盖: bio → request → plug/unplug → blk-mq (multi-queue) → IO scheduler (mq-deadline/kyber/bfq) → blktrace/blkparse 调试 内核版本: 2.6 ~ 6.x,重点标注 blk-mq 重构 (3.13~5.x)

概述

块层是文件系统和设备驱动之间的转换层。它的输入是文件系统提交的 bio("读第 N 个扇区,放到这些页面,完成后通知我"),输出是发给设备驱动的 request。在这之间,块层做合并、排序、调度,目标是最小化 IO 延迟的同时最大化吞吐。

从 2.6 的单请求队列到 5.x 的多队列 (blk-mq),Linux 块层经历了一次完整重写——驱动模型从"一个设备一个队列一把锁"变成了"per-CPU 提交队列 + 多个硬件队列"。


bio: 块 IO 的原子单元

// include/linux/blk_types.h
struct bio {
    struct block_device *bi_bdev;        // 目标块设备
    sector_t            bi_sector;       // 起始扇区 (512B 单位)
    unsigned int        bi_size;         // 剩余 IO 字节数
    unsigned int        bi_status;       // 完成状态 (BLK_STS_OK/error)

    struct bio_vec      *bi_io_vec;      // page vector (散列-聚集列表)
    unsigned short      bi_vcnt;         // vector 条目数
    unsigned short      bi_max_vecs;

    bio_end_io_t        *bi_end_io;      // 完成回调
    void                *bi_private;     // 回调上下文

    unsigned short      bi_write_hint;   // IO 优先级提示
    unsigned short      bi_ioprio;
};

bio_vec: 描述一个内存页上的 IO

// include/linux/bvec.h
struct bio_vec {
    struct page     *bv_page;    // 目标页面
    unsigned int    bv_len;      // 此段长度
    unsigned int    bv_offset;   // 页内偏移
};

一个 bio 可以包含多个 bio_vec——这就是 scatter-gather (散列-聚集) IO。文件系统可以将非连续的内存页面打包成一次 IO,块层和驱动再拆分为设备可接受的大小。

bio 提交路径

// block/blk-core.c
void submit_bio(struct bio *bio) {
    // 1. 检查是否 using blk-cgroup → account to cgroup
    // 2. 检查 bio 大小是否超过设备限制 → split if needed
    // 3. 调用 submit_bio_noacct() → __submit_bio_noacct_mq()
    //    → blk_mq_submit_bio()  (blk-mq 路径,当前默认)
}

blk-mq: 多队列架构

为什么从 blk-sq 迁移到 blk-mq

blk-sq (单队列, 2.6 ~ 3.x):
  struct request_queue *q;  ← 一把大锁保护所有提交
  所有 CPU 竞争 q->queue_lock → 多核扩展瓶颈
  IO scheduler 的单线程模型 → 高 IOPS 设备 (NVMe) 触顶

blk-mq (3.13+, 5.x 完全替代 blk-sq):
  两层队列:
    Software Queues (ctx): per-CPU 或 per-cgroup → 零锁竞争
    Hardware Queues (hctx): 映射到设备硬件队列 (NVMe SQ, SCSI tag)

  Tags: 用于管理硬件队列的并发请求数
    → 每个硬件队列有固定的 tag pool → 自然限流

数据结构

// include/linux/blk-mq.h
struct blk_mq_hw_ctx {
    unsigned int        queue_num;       // 硬件队列编号
    struct request_queue *queue;
    struct blk_mq_tags  *tags;           // tag pool (限并发)
    unsigned int        nr_ctx;          // 软件队列数

    struct list_head    dispatch;        // 从调度器出来的请求
    unsigned int        dispatched;

    struct blk_mq_ctx   **ctxs;          // 下属的软件队列
};

struct blk_mq_ctx {
    unsigned int        cpu;             // 绑定的 CPU
    struct blk_mq_hw_ctx *hctxs[];       // 映射的硬件队列
    struct list_head    rq_lists[];      // 提交链表
};

提交路径: plug → scheduler → dispatch → driver

bio 提交路径:blk_mq_submit_bio() 的五个阶段 block/blk-mq.c ① bio 太大? 超限 → blk_bio_discard_split()/blk_bio_segment_split() 拆分为多个 bio ② plug merge 尝试 blk_mq_attempt_plug_merge() → front/back merge(合并到现有 request 前/后) ③ 分配 request + tag blk_mq_get_new_requests() → blk_mq_get_tag() 取 tag,耗尽则等待 ④ 初始化 request blk_mq_rq_ctx_init() → 关联 bio,完成初始化 ⑤ 插入调度 blk_mq_sched_insert_request() → 决定走调度器还是直接派发 有 IO scheduler blk_mq_sched_try_insert_merge() → 排队到调度器 没有 IO scheduler blk_mq_run_dispatch_ops() → 直接 dispatch 五阶段依次执行:超限先拆分,能合并就并入已有 request,tag 不够就等待; 最后插入调度阶段决定是排队等 IO scheduler,还是直接派发给硬件队列。

Plug: 批量提交

// block/blk-mq.c + block/blk-plug.c
// plug 是 per-task 的 bio 积累机制:
//   blk_start_plug(current)     // 开启 plug
//   submit_bio() × N            // 积累 N 个 bio, 不立即 dispatch
//   blk_finish_plug(current)    // 一次性 flush 所有 bio

// 设计原理:
//   1. 减少锁竞争 (一次拿锁, 插入 N 个请求)
//   2. 增加合并机会 (插 plug 期间可以 front/back merge)
//   3. 文件系统通常在写路径中利用 plug:
//      ext4_writepages() → blk_start_plug() → 多次 submit_bio → blk_finish_plug()

IO 调度器

多队列 IO 调度器与旧单队列调度器的根本区别:⁠调度器不是全局的,是 per-hctx 的⁠。每个硬件队列独立调度,因为不同队列对应不同 CPU 的提交。

mq-deadline

// block/mq-deadline.c
// 读写分别维护红黑树 (按 sector 排序) + FIFO (按时钟排序)
// 
// 核心参数:
//   read_expire:  读请求最长等待时间 (默认 500ms)
//   write_expire: 写请求最长等待时间 (默认 5000ms)
//   writes_starved: 写请求配额 (优先处理多少批读后才处理一批写)
//
// 调度逻辑:
//   1. 先查 FIFO: 有超时的请求 → 从红黑树取该 sector 附近的请求 (批量下发)
//   2. 否则: 按 direction batch 下发 (优先读, 但 writs_starved 保证写不被饿死)
//   3. 每次 dispatch 不超过 16 个请求 (避免 starving 其他队列)
//
// 适合: NVMe, 通用 SSD

kyber

// block/kyber-iosched.c
// 基于 token bucket 的自适应调度
// 目标: 控制读/写队列深度,使延迟保持在目标以内
//
// 工作原理:
//   维护读/写的延迟直方图
//   根据延迟动态调整该方向的 token bucket rate
//   延迟低 → 增大 bucket → 更多 IO 并发
//   延迟高 → 减小 bucket → 减少 IO 并发 (降低设备队列深度)
//
// 适合: 对延迟敏感的 SSD 负载 (如数据库)

bfq (Budget Fair Queuing)

// block/bfq-iosched.c
// 按 cgroup/进程分配 IO 带宽
// 每个进程有一个 "budget" (可发出的扇区数)
// 基于 B-WF2Q+ 算法 (类似 CFS 的公平调度)
//
// 特点:
//   - 交互式进程自动获得更高权重 (检测 think time)
//   - 支持 cgroup blkio controller
//   - 顺序读被检测到 → 给予更多 budget → 高吞吐
//
// 适合: 桌面系统 (确保后台备份不影响前台应用响应)

调度器切换

cat /sys/block/nvme0n1/queue/scheduler
# [mq-deadline] kyber bfq none

echo kyber > /sys/block/nvme0n1/queue/scheduler

队列参数调优

# 硬件队列数 (通常等于 CPU 数)
cat /sys/block/nvme0n1/mq/*/cpu_list

# max_sectors_kb: 单次 IO 最大大小 (默认 512KB = 1024 扇区)
cat /sys/block/sda/queue/max_sectors_kb

# nr_requests: 每个硬件队列的最大请求数
cat /sys/block/nvme0n1/queue/nr_requests  # 默认 256

# scheduler 参数
cat /sys/block/nvme0n1/queue/iosched/read_expire  # mq-deadline

调试与观测

# blktrace: 追踪每个 IO 的完整生命周期
blktrace -d /dev/nvme0n1 -o trace
blkparse -i trace > trace.txt
# 输出: time, CPU, action (Q=queued, G=get, I=inserted, D=dispatch, C=complete)

# iostat: 设备级统计
iostat -x 1 nvme0n1
# r/s, w/s, r_await, w_await, aqu-sz (平均队列深度), %util

# 单设备原始计数
cat /sys/block/nvme0n1/stat
# 字段: read_ios, read_merges, read_sectors, read_ticks, write_ios, ...

# blk-mq 调试
cat /sys/kernel/debug/block/nvme0n1/hctx*/tags
# 每个硬件队列的 tag 使用情况

# ftrace 追踪块层事件
echo 1 > /sys/kernel/debug/tracing/events/block/block_bio_queue/enable
echo 1 > /sys/kernel/debug/tracing/events/block/block_rq_complete/enable
cat /sys/kernel/debug/tracing/trace_pipe

关键配置与编译

# 内核启动参数
scsi_mod.use_blk_mq=1    # 强制 SCSI 使用 blk-mq (5.0+ 默认)
elevator=mq-deadline      # 默认 IO scheduler
elevator=bfq              # 桌面推荐

# 内核编译选项
CONFIG_BLK_MQ=y           # 启用 blk-mq
CONFIG_IOSCHED_BFQ=y      # BFQ 支持
CONFIG_BLK_DEV_THROTTLING=y  # cgroup blkio

参考与延伸

  • 源码⁠: block/blk-mq.c (~4000行, blk-mq 核心), block/blk-merge.c, block/mq-deadline.c, block/bfq-iosched.c, block/kyber-iosched.c
  • 内核文档⁠: Documentation/block/, Documentation/block/bfq-iosched.rst
  • LWN:
    • "The multi-queue block layer" (lwn.net/Articles/552904/)
    • "blk-mq and the I/O schedulers" (lwn.net/Articles/738449/)

关键词: bio, bio_vec, blk-mq, software queue, hardware queue, plug, mq-deadline, kyber, bfq, blktrace, iostat, IO scheduler