引言
在 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 之旅:
- VFS 层:
write()→vfs_write()→ 文件系统的a_ops->write_iter() - Page Cache:写入
address_space的 Page Cache,标记脏页 - Writeback:
writeback_worker()发现脏页,调用wb_workfn()→writeback_single_inode() - 文件系统:ext4/xfs 将脏页转换为
bio(通过iomap_submit_io()或mpage_submit_page()) - bio 提交:
submit_bio()→submit_bio_noacct()→__submit_bio()→blk_mq_submit_bio() - blk-mq 调度:
blk_mq_submit_bio()→ 插入软件队列 → IO 调度器处理 → 硬件队列下发 - 驱动层:NVMe 驱动通过 SQ/CQ 提交到 SSD 硬件
- 中断完成: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 SSD | none / kyber | 设备原生并行,调度器瓶颈大于优化 |
| SATA SSD | mq-deadline | 仍需批量合并避免写放大 |
| HDD | mq-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_requests | 256 | 队列最大请求数,NVMe 可调高至 1024 |
| read_ahead_kb | 128 | 预读窗口大小,顺序读场景可增大 |
| max_hw_sectors_kb | 1280 | 硬件最大扇区限制 |
| nomerges | 0 | 0=合并全开, 1=仅 front merge, 2=关闭合并 |
| scheduler | mq-deadline | IO调度器选择 |
| rq_affinity | 1 | 完成中断亲和模式,2=同CPU完成 |
| io_poll | 0 | 开启 polling 模式(NVMe 高性能场景) |
| wbt_lat_usec | 2000 | Write 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 性能调优的核心原则:
- 理解设备特性:NVMe 多队列设计已解决传统调度器痛点,避免过度调度
- 合并优先:提高
max_hw_sectors_kb和nomerges=0最大化单次 I/O 数据量 - 监控先行:使用
iostat -x+blktrace定位瓶颈环节 - cgroup 隔离:多租户场景使用 BFQ +
io.weight实现公平共享 - polling 模式:延迟敏感型数据库可启用
io_poll绕过中断开销 - writeback 调优:增大脏页比例可减少小 I/O 频率
通过深入理解 Block Layer 的请求流转、blk-mq 多级队列调度、IO 调度器算法选型,结合真实场景的监控工具链,可以系统性地解决 Linux 存储 I/O 的各种性能问题。

发表评论 取消回复