Linux内核块层多队列(blk-mq)深度剖析
引言
随着NVMe SSD的普及,传统单队列块层架构成为性能瓶颈。Linux内核3.13引入的Block Multi-Queue(blk-mq)机制,通过多队列并行处理大幅提升了I/O吞吐量。本文将深入剖析blk-mq的架构设计、核心数据结构、调度机制及性能优化实践。
1. 为什么需要多队列
1.1 单队列架构的瓶颈
传统块层使用单一请求队列(request_queue),所有CPU核心共享一个锁保护的队列。在高并发场景下:
- 锁竞争严重:所有CPU争抢同一个queue_lock
- 缓存行弹跳:多核之间频繁传递缓存行
- 无法发挥硬件并行性:现代NVMe设备支持多达64K个队列
1.2 多队列的性能优势
blk-mq引入三个核心队列概念:
- 硬件分发队列(HW Dispatch Queue):对应硬件队列上下文,直接映射到设备队列
- 软件调度队列(Software Queue):CPU私有的提交队列,避免锁竞争
- 调度器队列(Scheduler Queue):I/O调度器使用的合并与排序队列
2. blk-mq核心数据结构
struct blk_mq_tag_set {
const struct blk_mq_ops *ops;
unsigned int nr_hw_queues;
unsigned int queue_depth;
unsigned int cmd_size;
int numa_node;
void *driver_data;
struct blk_mq_tag *tags[];
};
struct blk_mq_hw_ctx {
struct request_queue *queue;
struct blk_mq_tags *tags;
unsigned int hctx_index;
unsigned int nr_ctx;
struct sbitmap ctx_bitmap;
struct blk_mq_ctx *ctxs;
spinlock_t dispatch_lock;
struct list_head dispatch;
unsigned long state;
struct delayed_work delay_work;
struct hlist_node cpuhp_online;
int cpu;
unsigned int queue_num;
atomic_t nr_active;
struct blk_mq_ctx *dispatch_from;
} ____cacheline_aligned_in_smp;
关键结构关系:
- blk_mq_tag_set:驱动端配置,定义队列数量和深度
- blk_mq_hw_ctx:每个硬件队列的运行时上下文
- blk_mq_ctx:每个CPU的软件队列上下文
- blk_mq_tags:标签集合,管理request分配与完成
3. I/O请求生命周期
3.1 请求提交路径
blk_execute_rq
→ blk_mq_alloc_request
→ blk_mq_request_bypass_insert (bypass路径)
→ blk_mq_try_issue_directly (直接下发)
→ hctx->driver->queue_rq(hctx, &bd)
→ blk_mq_run_hw_queue (调度器路径)
→ __blk_mq_delay_run_hw_queue
→ __blk_mq_run_hw_queue
→ blk_mq_sched_dispatch_requests
→ hctx->driver->queue_rq()
3.2 请求完成路径
驱动中断 → blk_mq_complete_request
→ __blk_mq_complete_request
→ blk_update_request
→ blk_mq_end_request
→ blk_mq_free_request
3.3队列映射策略
blk-mq提供三种队列映射模式:
- BLK_MQ_QUEUE_MAP_NUMA:按NUMA节点分配,减少跨节点访问
- BLK_MQ_QUEUE_MAP_PCI:按PCIe拓扑分配(默认)
- BLK_MQ_QUEUE_MAP_HYBRID:混合映射,优先PCIe affinity
4. 调度器集成
4.1 mq-deadline调度器
mq-deadline是NVMe设备的默认调度器,特点包括:
- 按LBA排序读请求,减少寻道
- 写请求批量合并提交
- 读请求优先级高于写请求
- FIFO队列保证请求延迟上限
4.2 BFQ调度器
BFQ(Budget Fair Queueing)的特点:
- 基于进程的公平带宽分配
- 适合桌面和交互式应用
- 低延迟保障机制
- budget机制防止饥饿
4.3 Kyber调度器
Kyber是轻量级调度器,专为快速存储设备设计:
- 基于延迟的自我调节
- 无需请求排序
- 适合高IOPS设备
5. 中断处理与IRQ亲和性
5.1 中断分发策略
// 设置blk-mq队列的CPU亲和性
blk_mq_map_queues(&tag_set);
blk_mq_alloc_tag_set(&tag_set);
// NVMe驱动注册中断
pci_alloc_irq_vectors(dev, 1, nr_queues, PCI_IRQ_ALL_TYPES);
5.2 中断合并(Coalescing)
blk-mq支持中断合并以减少中断频率:
// blk-mq中断合并配置
#define BLK_MAX_WRITE_HINTS 8
struct blk_mq_hw_ctx {
// ...
unsigned int nr_active; // 当前活跃请求数
int poll_queued; // 轮询模式计数
struct srcu_struct srcu[]; // 延迟释放保护
};
6. 性能优化实践
6.1 队列深度调优
# 查看当前队列深度
cat /sys/block/nvme0n1/queue/nr_requests
cat /sys/block/nvme0n1/queue/io_poll
# 调整队列深度
echo 1024 > /sys/block/nvme0n1/queue/nr_requests
echo 2 > /sys/block/nvme0n1/queue/io_poll_delay
6.2 CPU隔离与绑定
# 查看blk-mq队列的CPU亲和性
for i in /sys/block/nvme0n1/mq/*; do
echo "$(basename $i): $(cat $i/cpu_list)"
done
# 使用taskset绑定应用到特定CPU
taskset -c 0-7 fio --name=test --ioengine=io_uring --rw=randread
6.3 io_uring集成
blk-mq与io_uring深度集成,实现高效用户态I/O:
// io_uring注册固定buffer
io_uring_register_buffers(ring, iovecs, nr_bufs);
// io_uring提交读请求
struct io_uring_sqe *sqe = io_uring_get_sqe(ring);
io_uring_prep_readv(sqe, fd, &iov, 1, offset);
io_uring_submit(ring);
7. perf与性能监控
7.1 关键性能指标
# 查看块层统计
cat /sys/block/nvme0n1/stat
# 输出:read_ios read_merges read_sectors read_ticks
# write_ios write_merges write_sectors write_ticks
# in_flight io_ticks time_in_queue
# iotop查看实时I/O
iotop -o -d 2
7.2 BPF性能分析
// BPF程序统计blk-mq延迟
SEC("tracepoint/block/block_rq_issue")
int trace_block_rq_issue(struct trace_event_raw_block_rq *ctx) {
u64 ts = bpf_ktime_get_ns();
u32 key = ctx->dev;
bpf_map_update_elem(&start, &key, &ts, BPF_ANY);
return 0;
}
SEC("tracepoint/block/block_rq_complete")
int trace_block_rq_complete(struct trace_event_raw_block_rq *ctx) {
u32 key = ctx->dev;
u64 *tsp = bpf_map_lookup_elem(&start, &key);
if (tsp) {
u64 latency = bpf_ktime_get_ns() - *tsp;
blk_dist.increment(bpf_log2l(latency));
}
return 0;
}
8. NVMe驱动中的blk-mq实践
NVMe驱动是blk-mq最典型的应用场景:
static const struct blk_mq_ops nvme_mq_ops = {
.queue_rq = nvme_queue_rq,
.complete = nvme_pci_complete_rq,
.init_hctx = nvme_init_hctx,
.init_request = nvme_init_request,
.timeout = nvme_timeout,
.map_queues = nvme_pci_map_queues,
.poll = nvme_poll,
};
static int nvme_probe(struct pci_dev *pdev, const struct pci_device_id *id) {
struct nvme_dev *dev;
struct blk_mq_tag_set *set;
// 分配tag set,配置队列数量
set = &dev->tagset;
set->ops = &nvme_mq_ops;
set->nr_hw_queues = dev->max_qid;
set->queue_depth = NVME_MAX_QUEUE_DEPTH; // 通常1024
set->numa_node = dev->ctrl.numa_node;
set->cmd_size = sizeof(struct nvme_request);
set->flags = BLK_MQ_F_SHOULD_MERGE | BLK_MQ_F_STACKING;
// 分配tag set
ret = blk_mq_alloc_tag_set(set);
if (ret) return ret;
// 创建request queue
dev->admin_q = blk_mq_init_queue(set);
dev->ctrl.adminq = dev->admin_q;
return 0;
}
NVMe驱动中,每个CPU核心拥有独立的提交队列(SQ),中断绑定到相应CPU,实现零锁争用的I/O路径。
9. 未来发展趋势
1. 异步I/O轮询模式blk-mq已支持io_ring的polling模式,绕过中断进一步降低延迟。
2. Zone Block Device针对SMR硬盘和ZNS SSD,blk-mq增加了zone管理支持。
3. CXL内存设备新兴的CXL设备将利用blk-mq的多队列架构实现内存级I/O性能。
4. Rust驱动支持新一代NVMe驱动可能使用Rust编写,blk-mq的Rust安全抽象正在开发中。
总结
blk-mq通过软硬件队列分离、无锁提交路径、与硬件队列一一映射的设计思路,成功解决了单队列块层的扩展性问题。理解blk-mq的架构对于高端存储系统性能调优至关重要。

发表评论 取消回复