1. 从单队列到多队列:Block层的革命性进化
记得我第一次在Linux 2.6内核中看到块设备层的代码时,那个经典的request_queue结构让我印象深刻。当时的架构就像个单车道的高速公路——所有IO请求都要挤在同一个队列里,通过一把大锁(queue_lock)来保证线程安全。这就像早高峰时所有车辆都在一个收费站排队,后面的车只能干等着前车交完费。
单队列架构的痛点在NVMe SSD这类高性能设备上暴露无遗。当我在实验室用fio测试时发现,随着CPU核心数增加,IOPS性能几乎不增长。用perf工具分析,大量时间消耗在自旋锁竞争上。具体来说,submit_bio()到blk_queue_bio()的路径中,几乎所有操作都要抢queue_lock这把锁。
多队列(blk-mq)的出现彻底改变了局面。它的核心思想就像把单车道改造成立体交通枢纽:
- 软件队列层:每个CPU核心有独立的
blk_mq_ctx - 硬件队列层:设备实际的物理队列
blk_mq_hw_ctx - 动态映射:通过
mq_map实现软硬件队列的灵活映射
我在NVMe设备上的实测数据显示,启用blk-mq后,32核环境下的随机读IOPS从单队列的80k飙升到620k,提升了近8倍!
2. 核心数据结构解剖:多队列的骨架系统
第一次看blk-mq的代码时,那些数据结构的关系让我头晕目眩。经过反复梳理,我总结出这几个关键结构的关系网:
2.1 三层核心结构
struct blk_mq_ctx { // Per-CPU软件队列 struct list_head rq_list; // 待处理请求链表 unsigned int cpu; // 所属CPU编号 }; struct blk_mq_hw_ctx { // 硬件队列抽象 struct blk_mq_tags *tags; // 标签集合 struct blk_mq_ctx **ctxs; // 映射的软件队列 unsigned int queue_num; // 硬件队列编号 }; struct blk_mq_tag_set { // 全局资源池 struct blk_mq_tags **tags; // 所有硬件队列的标签 unsigned int *mq_map; // CPU到硬件队列的映射表 };这三个结构的关系就像公司里的项目组:
blk_mq_ctx是各个开发小组(每个CPU一个)blk_mq_hw_ctx是执行项目的服务器资源blk_mq_tag_set是公司的资源管理部门
2.2 标签分配机制
最精妙的设计要数blk_mq_tags这套标签系统。它通过sbitmap实现无锁化的tag分配:
struct blk_mq_tags { struct sbitmap_queue bitmap_tags; // 常规tag位图 struct sbitmap_queue breserved_tags; // 预留tag位图 struct request **static_rqs; // 预分配的request数组 };实际使用时的分配流程:
- 通过
sbitmap_get()获取空闲tag(相当于数组下标) static_rqs[tag]直接获取预分配的request- 请求完成后通过
sbitmap_clear()释放tag
这种设计带来两个优势:
- 内存局部性好:request对象在初始化时就预分配完成
- 分配效率高:sbitmap的原子操作比传统锁更高效
3. IO路径全解析:从bio到硬件队列
让我们跟踪一个实际的IO请求旅程。假设在CPU3上发起写操作:
3.1 请求入口:blk_mq_make_request()
static void blk_mq_make_request(struct request_queue *q, struct bio *bio) { // 获取当前CPU对应的软件队列 ctx = blk_mq_get_ctx(q); // 返回per-CPU的blk_mq_ctx // 获取映射的硬件队列 hctx = blk_mq_map_queue(q, ctx->cpu); // 尝试合并到现有请求 if (blk_attempt_plug_merge(q, bio, &request_count)) return; // 分配request和tag rq = blk_mq_sched_get_request(q, bio, bio->bi_rw, &data); // 根据请求类型选择派发路径 if (is_flush_fua) { blk_insert_flush(rq); blk_mq_run_hw_queue(hctx, true); } else if (plug && q->nr_hw_queues == 1) { list_add_tail(&rq->queuelist, &plug->mq_list); } else { blk_mq_try_issue_directly(hctx, rq); } }3.2 关键映射关系建立
初始化阶段建立的映射关系决定了请求的路由路径:
// 在blk_mq_init_allocated_queue()中 q->mq_map = set->mq_map; // 继承tag_set的映射表 // 在blk_mq_map_swqueue()中建立映射 for_each_possible_cpu(i) { hctx_idx = q->mq_map[i]; // 获取CPU对应的硬件队列编号 hctx = q->queue_hw_ctx[hctx_idx]; ctx = per_cpu_ptr(q->queue_ctx, i); hctx->ctxs[hctx->nr_ctx++] = ctx; // 建立硬件队列到软件队列的反向映射 }3.3 调度器集成
当使用IO调度器(如mq-deadline)时,请求会先进入调度队列:
void blk_mq_sched_insert_request(struct request *rq, bool at_head) { if (e && e->type->ops.mq.insert_requests) { e->type->ops.mq.insert_requests(hctx, list, false); blk_mq_run_hw_queue(hctx, true); } else { blk_mq_insert_request(rq, at_head); } }调度器的介入位置就像交通指挥中心,决定哪些车辆(请求)优先通行。
4. 性能优化实战:从理论到实践
在云原生环境中调试blk-mq性能时,我总结出几个关键调优点:
4.1 队列深度配置
通过sysfs可以动态调整参数:
# 查看NVMe设备的队列配置 ls /sys/block/nvme0n1/mq/ # 调整硬件队列深度 echo 1024 > /sys/block/nvme0n1/queue/nr_requests但要注意两个陷阱:
- 队列深度不是越大越好,过大会增加延迟
- 需要与设备支持的队列深度匹配(通过
nvme id-ctrl查看)
4.2 CPU亲和性优化
默认的CPU到硬件队列映射可能不是最优的。通过以下方式优化:
# 查看当前映射 cat /sys/block/nvme0n1/mq/*/cpu_list # 自定义映射(示例:将队列0绑定到CPU0-3) echo 0-3 > /sys/block/nvme0n1/mq/0/cpu_list在NUMA系统中更要特别注意:
// 在驱动初始化时设置numa_node set->numa_node = dev_to_node(dev->dev);4.3 中断亲和性
配合队列绑定,需要设置对应的中断亲和性:
# 查看NVMe中断 grep nvme /proc/interrupts # 设置中断亲和性 echo 1 > /proc/irq/123/smp_affinity5. 深度调试技巧:解决实际问题的工具箱
遇到blk-mq相关问题时,我的调试三板斧:
5.1 动态ftrace跟踪
# 启用blk_mq相关跟踪点 echo 1 > /sys/kernel/debug/tracing/events/block/enable # 过滤特定设备 echo "dev == 8:0" > /sys/kernel/debug/tracing/events/block/filter # 查看实时事件 cat /sys/kernel/debug/tracing/trace_pipe5.2 请求生命周期监控
通过blktrace观察请求流转:
blktrace -d /dev/nvme0n1 -o - | blkparse -i -典型问题现象:
- 请求在plug队列停留过久 → 检查调度器配置
- 硬件队列dispatch堆积 → 检查中断负载均衡
5.3 性能热点分析
使用perf定位锁竞争:
perf record -e cycles -g -a -- sleep 5 perf report --no-children常见热点:
sbitmap_get竞争 → 考虑增加硬件队列数blk_mq_get_driver_tag等待 → 调整队列深度
6. 未来演进:多队列架构的新挑战
尽管blk-mq已经取得巨大成功,但在我们的测试中仍发现一些待改进点:
6.1 混合负载场景
当同一个硬件队列同时处理:
- 延迟敏感的元数据操作
- 吞吐优先的大数据块IO
解决方案探索:
- 优先级队列:华为提出的
blk-mq-prio补丁 - 动态调度:根据IO模式自动调整调度策略
6.2 虚拟化环境优化
在VM场景下观察到的问题:
- 多vCPU竞争同一个物理CPU的软件队列
- 中断迁移导致的缓存失效
当前解决方案:
// 在virtio-blk驱动中设置 set->flags |= BLK_MQ_F_NO_SCHED;6.3 新硬件支持
面对DPU、CXL等新技术:
- 需要更灵活的队列映射机制
- 跨NUMA节点的队列共享
- 硬件加速的tag管理
某次在调试RDMA存储问题时,我们发现默认的队列映射导致跨NUMA访问。通过自定义map_queues回调,最终获得了30%的延迟改善:
static int nvme_rdma_map_queues(struct blk_mq_tag_set *set) { // 根据NUMA拓扑重新规划队列映射 return blk_mq_map_queues(&set->map[HCTX_TYPE_DEFAULT]); }