引言

在 Linux 内核的存储 I/O 路径中,Block Layer(块层)是连接上层文件系统/应用与底层磁盘驱动的核心枢纽。它负责将分散的 I/O 请求合并、排序、调度,最终高效地发送给块设备驱动。理解 Block Layer 不仅是存储性能调优的基础,更是理解 Linux I/O 栈全貌的关键一环。

本文将从 bio 结构体出发,深入剖析 Block Layer 的请求提交流程、多队列 blk-mq 架构演进、IO 调度器算法、请求合并策略、以及实际的性能监控与调优方法。

一、Block Layer 架构全景

Linux Block Layer 经历了从单队列(Single Queue)到多队列(Multi-Queue,blk-mq)的架构性变革。在传统的单队列架构中,所有 CPU 共享一个请求队列(request_queue),通过一把全局自旋锁(queue_lock)保护。这在多核时代成为严重的扩展性瓶颈——每次 blk_peek_request() 都要争抢同一把锁。

blk-mq 架构在 Linux 3.13 引入(2013年),其核心思想是队列分解:

  • Hardware Dispatch Queue(硬件分派队列):每个硬件中断目标 CPU 一个,减少跨 CPU 缓存失效
  • Software Stage Queue(软件暂存队列):每个 CPU(或 CPU 组)一个,用于 I/O 合并和调度
  • Tags 机制:每个请求分配一个全局唯一 tag,驱动层直接用 tag 索引提交/完成请求,无需锁保护

这种两级队列设计使得 NVMe 等高性能设备可以充分利用多核并行,实现百万级 IOPS。

二、核心数据结构:bio 与 request

2.1 bio 结构体

bio 是 Block Layer 的基本 I/O 单元,代表一次连续或不连续的数据传输操作。其核心字段:

struct bio {
    struct bio *bi_next;          // 链表指针
    struct block_device *bi_bdev; // 目标块设备
    blk_opf_t bi_opf;             // 操作类型(READ/WRITE/DISCARD等)
    unsigned short bi_ioprio;     // I/O 优先级
    blk_status_t bi_status;       // 完成状态
    struct bvec_iter bi_iter;     // 当前分段迭代器
    bio_end_io_t *bi_end_io;      // 完成回调
    void *bi_private;             // 私有数据(由提交者使用)
    struct bio_vec *bi_io_vec;    // 缓冲区向量(bvec 数组)
    struct bio_set *bi_pool;      // 来源的 bio 池
    unsigned bi_vcnt;             // bvec 数量
    unsigned bi_max_vecs;         // 最大 bvec 数
    unsigned bi_iter.bi_size;     // 总字节数
    unsigned bi_iter.bi_idx;      // 当前 bvec 索引
    unsigned bi_iter.bi_bvec_done; // 当前 bvec 内偏移
    atomic_t __bi_remaining;      // 引用计数(来自 blk_mq)
    struct bio_vec inline_vecs[]; // 内联的 bvec 数组
};

关键理解:一个 bio 由多个不连续的内存段(bio_vec)组成,每个bio_vec描述一个物理连续页面的子段(bv_page + bv_offset + bv_len)。这种设计支持散聚 I/O(Scatter-Gather)——一次操作可以读写不连续的多个内存页到同一磁盘 LBA 范围。

2.2 bio_vec — 段描述符

struct bio_vec {
    struct page *bv_page;   // 物理页指针
    unsigned int bv_len;    // 段长度(字节)
    unsigned int bv_offset; // 页内偏移
};

2.3 request 结构体

request 是经过合并/调度后的 I/O 操作,代表设备驱动要执行的一个逻辑 I/O 任务。在 blk-mq 架构中:

struct request {
    struct request_queue *q;
    struct blk_mq_ctx *mq_ctx;      // 软件队列上下文
    struct blk_mq_hw_ctx *mq_hctx;  // 硬件队列上下文
    unsigned int cmd_flags;
    unsigned int rq_flags;
    int tag;                       // 全局唯一 tag
    int internal_tag;              // 硬件队列内部 tag
    struct bio *bio;               // 链表头
    struct bio *biotail;           // 链表尾
    struct list_head queuelist;    // 队列链表节点
    struct __call_head;            // 完成回调列表
    void *end_io_data;
    sector_t __sector;             // 起始扇区
    unsigned int __data_len;       // 数据总长度
    unsigned short stats_sectors;  // 统计扇区
    unsigned short nr_phys_segments; // 物理段数
    unsigned short ioprio;
    struct gendisk *rq_disk;
    struct request *next_rq;       // 关联请求(如 DISCARD 后的 WRITE SAME)
    struct request *prev_rq;
};

三、I/O 提交全链路

一次文件写入的 Block Layer 之旅:

  1. VFS 层:write() → vfs_write() → 文件系统的 a_ops->write_iter()
  2. Page Cache:写入 address_space 的 Page Cache,标记脏页
  3. Writeback:writeback_worker() 发现脏页,调用 wb_workfn() → writeback_single_inode()
  4. 文件系统:ext4/xfs 将脏页转换为 bio(通过 iomap_submit_io() 或 mpage_submit_page())
  5. bio 提交:submit_bio() → submit_bio_noacct() → __submit_bio() → blk_mq_submit_bio()
  6. blk-mq 调度:blk_mq_submit_bio() → 插入软件队列 → IO 调度器处理 → 硬件队列下发
  7. 驱动层:NVMe 驱动通过 SQ/CQ 提交到 SSD 硬件
  8. 中断完成:SSD 完成 → MSI-X 中断 → CQ 完成回调 → bio_endio() → 释放 bio

3.1 submit_bio 到 blk-mq 的关键路径

void submit_bio(struct bio *bio)
{
    // 1. 插件拦截(dm-crypt、dm-integrity等)
    if (!submit_bio_noacct(bio)) return;
}

static bool __submit_bio_noacct(struct bio *bio)
{
    // 2. bio 拆分(超过 max_hw_sectors 或 max_segments)
    if (blk_bio_segment_split(...)) goto queue_new;
    // 3. Plug 状态下的合并尝试
    if (current->plug) blk_add_bio_plug(...);
    // 4. 队列映射 + 插入
    blk_mq_submit_bio(bio);
}

blk_status_t blk_mq_submit_bio(struct bio *bio)
{
    // 5. 获取硬件队列映射
    blk_mq_map_queue(q, cpu);
    // 6. 经 IO 调度器插入软件队列
    blk_mq_sched_insert_request(rq, false, true, true);
    // 7. 触发下发(direct 或 ksoftirqd)
    blk_mq_run_hw_queue(hctx, true);
}

四、blk-mq 多队列调度体系

4.1 硬件队列映射

blk-mq 通过 blk_mq_ops 定义硬件队列与 CPU 的亲和关系:

struct blk_mq_ops {
    void (*queue_rq)(struct blk_mq_hw_ctx *, struct request_queue *);
    int  (*commit_rqs)(struct blk_mq_hw_ctx *, bool);
    void (*complete)(struct request *);
    void (*map_queues)(struct blk_mq_tag_set *);
    int  (*init_hctx)(struct blk_mq_hw_ctx *, void *, unsigned int);
    void (*exit_hctx)(struct blk_mq_hw_ctx *, unsigned int);
    int  (*init_request)(struct blk_mq_tag_set *set, struct request *,
                          unsigned int, unsigned int);
    void (*exit_request)(struct blk_mq_set *, struct request *, unsigned int);
    void (*cleanup_rq)(struct request *);
    bool (*poll)(struct blk_mq_hw_ctx *);
    int  (*set_params)(struct request_queue *, struct elevator_queue *);
    void (*timeout)(struct request *);
    void (*show_rq)(struct seq_file *, struct request *);
    int  (*busy_iter)(struct blk_mq_hw_ctx *, struct request *, void *, bool);
    int  (*map_queue)(struct request *, int);
    const struct blk_mq_debugfs_attr *debugfs_attrs;
};

map_queues 回调决定每个硬件队列绑定到哪些 CPU 中断。对于 NVMe 而言,MSI-X 模式通常为每个 CPU 独占一个 Completion Queue,实现完全无锁的中断处理。

4.2 软件队列分配

每个 CPU 的写操作都会映射到一个软件暂存队列:

struct blk_mq_ctx {
    struct request_queue   *queue;
    struct blk_mq_ctxs     *ctxs;
    unsigned int            cpu;
    struct list_head        rq_lists[HRTICK_MAX];
    struct blk_mq_ctx      *queue_ctx;
    struct kobject          kobj;
};

4.3 Tag 分配机制

blk-mq 为每个硬件队列维护一个 static tag 表(struct sbitmap_queue):

  • 静态 Tag:每个硬件队列有固定数量的 tag(如 1024),驱动可以直接用 tag 索引到对应的 request 指针数组
  • Tag 耗尽处理:当硬件队列 tag 全满时,新请求等待blk_mq_get_driver_tag() 释放。此时 CPU 可能阻塞在blk_mq_dispatch_rqs_list()
  • Shared Tag:多设备共享 tag 池(适合 multipath 和 Android UFS)

五、IO 调度器算法与选择

内核 Block Layer 提供多种 IO 调度器:

5.1 mq-deadline

基于 Sorted Queue + Batched Dispatch 模型的核心调度器:

  • 读优先:读请求有严格过期时间(默认 500ms),超时后会抢占写请求
  • Sorted List:所有请求按 LBA 排序,减少寻道时间
  • FIFO 批处理:写请求按 FIFO 打包提交(默认批量 16)
  • Starvation Prevention:写 FIFO 过期后(默认 5s)强制提交,防止读饿死写

适用场景:数据库(MySQL/PostgreSQL)、HDD、混合读写负载。

5.2 BFQ (Budget Fair Queueing)

BFQ 是比例公平调度器,基于 Van Jacobson 的 STRF 算法扩展:

  • 进程级公平:为每个进程分配 I/O budget(扇区数),按 budget 比例分发请求
  • 低延迟模式:互动进程抢占机制,桌面操作不会因后台任务而卡顿
  • 权重调节:通过io.prio.class(RT/BE/Idle)实现三级优先级
  • 非线性预测:通过悲观因子(predictive参数)预估突发流量

适用场景:桌面系统、虚拟机(如 QEMU/KVM)、实时多媒体。

5.3 Kyber

Kyber 是基于延迟目标的调度器(Linux 5.0 引入):

  • 自适应深度控制:为读/写分别维护目标延迟(默认 读10ms / 写100ms)
  • 令牌桶限流:通过调度队列深度(scheduling queue depth)平滑 I/O 突发
  • 轻量性:O(1) 调度开销,极简数据结构(仅需 4 个链表)

适用场景:NVMe SSD、延迟敏感型工作负载。

5.4 none / noop

无调度器模式,直接将 bio 简单合并后送往驱动:

  • HDD:不推荐,导致磁头摆动
  • NVMe:推荐(blk-mq 已处理并发,调度器额外开销无益)
  • dm-raid / md:建议在硬件层使用 'none'

5.5 选择决策表

设备类型推荐调度器理由
NVMe SSDnone / kyber设备原生并行,调度器瓶颈大于优化
SATA SSDmq-deadline仍需批量合并避免写放大
HDDmq-deadline / BFQ寻道优化关键
数据库mq-deadline读延迟可控,批量写减少 fsync 抖动
KVM/QEMU 虚拟机BFQ进程级公平,避免 Guest I/O 饿死
云端块存储none / kyber优化在远端,本地只负责合并

六、请求合并策略

Block Layer 通过三次合并尝试最大化每次请求的数据量:

6.1 Plug 阶段的 Front/Back Merge

当进程通过 blk_start_plug() 开启 Plug 窗口时,连续的写操作会在用户态即被合并:

struct blk_plug {
    struct request *mq_list;    // 私有的直接插入链表(仅 blk-mq)
    struct list_head cb_list;   // 回调链表
    unsigned short rq_count;
    bool multiple_queues;
};

// Plug 合并检查(blk-mq)
static bool blk_mq_attempt_bio_merge(struct request_queue *q, struct bio *bio,
                                     unsigned int nr_segs)
{
    // 1. 寻找即将提交的请求(在 mq_list 末尾)
    // 2. 检查尾部/头部扇区是否相邻(Front/Back merge)
    // 3. 检查同一 Page 内是否可以合并
    blk_merge(rq, bio);
}

6.2 调度器内部的 Merge

当 bio 在 IO 调度器(如 BFQ/mq-deadline)内部被排序时,二次合并尝试:

  • BFQ:按进程上下文内的 I/O 进行同方向同 LBA 聚合
  • mq-deadline:Sorted List 插入时检查左右邻居是否可合并

6.3 Last Resort: BIO 拆分

当 bio 无法合并但设备有max_hw_sectors_kb限制时,通过 blk_queue_split() 将 bio 拆分为两个独立请求。

七、性能监控与故障诊断工具链

7.1 /proc/diskstats

提供块设备级统计信息(内核 5.5+ 扩展了字段):

# cat /proc/sda/diskstats
   8       0 sda 123456 7890 9876543 12345 56789 123 4567890 67890 0 12345 89012 0 0 0 0

字段含义:主设备号、次设备号、设备名、读完成数、读合并数、读扇区数、读耗时(ms)、写完成数、写合并数、写扇区数、写耗时、I/O 进行中、I/O 耗时、加权 I/O 耗时。

7.2 iostat(sysstat 包)

iostat -xz 1
Device      r/s    w/s   rkB/s   wkB/s  rrqm/s  wrqm/s  %util  await  r_await  w_await
nvme0n1  1234.0  567.0  15800   7240    12.0    45.0    98.5   0.62    0.45     0.92

关键指标:

  • %util:设备利用率(接近 100% 表示设备满负荷)
  • await:平均 I/O 响应时间(含排队+服务时间)
  • r_await / w_await:读写分离延迟
  • rrqm/s / wrqm/s:合并率

7.3 blktrace + blkparse

blktrace 提供内核级别的 I/O 事件追踪:

# 追踪 /dev/nvme0n1 的完整路径
blktrace -d /dev/nvme0n1 -o trace | blkparse -i trace -o parse.log

# 典型输出(D=Issue, C=Complete)
8,0    3  12345 0.001234567  1000  I   D  WS 123456 + 8 [mysqld]

事件类型:Q=Queued, G=Growth, D=Issue, P=Plug, U=Unplug, M=Merge, S=Split, R=Remap。

7.4 bpftrace 实时追踪

bpftrace -e 'kprobe:blk_mq_start_request {
    @start[args->rq] = nsecs;
}
kprobe:blk_mq_end_request /@start[args->rq]/ {
    @us = hist((nsecs - @start[args->rq]) / 1000);
    delete(@start[args->rq]);
}'

这将实时输出 Block Layer I/O 延迟的直方图。

7.5 /sys/block/$device/queue 调优参数

参数默认值说明
nr_requests256队列最大请求数,NVMe 可调高至 1024
read_ahead_kb128预读窗口大小,顺序读场景可增大
max_hw_sectors_kb1280硬件最大扇区限制
nomerges00=合并全开, 1=仅 front merge, 2=关闭合并
schedulermq-deadlineIO调度器选择
rq_affinity1完成中断亲和模式,2=同CPU完成
io_poll0开启 polling 模式(NVMe 高性能场景)
wbt_lat_usec2000Write Throttle 延迟目标

八、实战调优案例

8.1 案例一:MySQL 高并发写入优化

问题:InnoDB 写入在 NVMe 上出现周期性延迟尖刺。

诊断:

  • iostat -x 1 显示 w_await 间歇性飙升至 20ms+
  • blktrace 发现 mq-deadline 的 Sorted List 导致 fsync 线程的请求被批处理延迟

解决:

# 切换到 none
echo none > /sys/block/nvme0n1/queue/scheduler
# 提高队列深度
echo 1024 > /sys/block/nvme0n1/queue/nr_requests
# 开启 polling
echo 2 > /sys/block/nvme0n1/queue/io_poll
# 关闭写入节流
echo 0 > /sys/block/nvme0n1/queue/wbt_lat_usec

结果:写延迟 P99 从 25ms 降至 2ms,吞吐量提升 40%。

8.2 案例二:KVM 虚拟机 I/O 隔离

问题:多个 VM 共享 NVMe 时,后台备份 VM 的顺序读干扰在线交易 VM 的随机写。

解决:使用 BFQ + cgroup v2 io.weight 隔离:

# 启用 BFQ
echo bfq > /sys/block/nvme0n1/queue/scheduler
# 在线交易 VM 权重高
echo 500 > /sys/fs/cgroup/mariadb-vm/io.weight
# 备份 VM 权重低
echo 50 > /sys/fs/cgroup/backup-vm/io.weight
# 限制备份 VM 的 I/O 带宽
echo "259:0 rbps=104857600" > /sys/fs/cgroup/backup-vm/io.max

九、内核 6.x Block Layer 新特性

9.1 Zero-Copy Send(io_uring 集成)

Linux 6.1+ IORING_SEND_ZC 配合 MSG_ZEROCOPY 可以实现用户态到 socket 的零拷贝传输,绕过 Page Cache。结合 blk-mq 的 polling 模式,可构建全内核旁路的高性能 I/O 路径。

9.2 folio block dirty tracking

6.2+ 内核引入 folio 层面脏页追踪,减少了 lock_page() 调用次数,尤其在大文件顺序写场景下减少了 15%-20% 的 CPU 占用。

9.3 新 IO 统计接口(blk-cgroup)

6.3+ 重构统一的 blk-cgroup 统计接口,支持按 cgroup v2 精确统计每个 IOPS 和带宽。/sys/fs/cgroup/XXX/io.stat 输出:

259:0 rbytes=1234567 wbytes=987654 rios=1234 wios=567 dbytes=0 dios=0

9.4 mq-deadline prio_class 增强

Linux 6.4+ 为 mq-deadline 引入 prio_class 字段,允许用户态通过 ioprio_set() 指定请求的 I/O 优先级(如 RT/High/Normal),调度器据此调整过期时间常数。

总结与最佳实践

Block Layer 性能调优的核心原则:

  1. 理解设备特性:NVMe 多队列设计已解决传统调度器痛点,避免过度调度
  2. 合并优先:提高 max_hw_sectors_kb 和 nomerges=0 最大化单次 I/O 数据量
  3. 监控先行:使用 iostat -x + blktrace 定位瓶颈环节
  4. cgroup 隔离:多租户场景使用 BFQ + io.weight 实现公平共享
  5. polling 模式:延迟敏感型数据库可启用 io_poll 绕过中断开销
  6. writeback 调优:增大脏页比例可减少小 I/O 频率

通过深入理解 Block Layer 的请求流转、blk-mq 多级队列调度、IO 调度器算法选型,结合真实场景的监控工具链,可以系统性地解决 Linux 存储 I/O 的各种性能问题。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部