Linux 块设备多队列架构逐层拆解:blk-mq 在 eMMC 驱动中的硬软队列调度机制 Linux 块设备多队列架构逐层拆解blk-mq 在 eMMC 驱动中的硬软队列调度机制一、单队列瓶颈的工程现场当 eMMC 的 IOPS 被内核锁死嵌入式 Linux 设备中使用 eMMC 作为主存储时一个典型的性能瓶颈出现在高并发读写场景下。假设设备同时执行多个应用的数据写入系统监控显示 CPU 的 iowait 达到 60% 以上而 eMMC 的物理带宽仅用了 30%。根因排查后发现传统 Linux 块层的单请求队列blk-sq成为瓶颈——所有 I/O 请求需要通过一把全局自旋锁串行入队多核 CPU 上的并发提交变成了锁竞争的灾难。以四核 Cortex-A53 平台上 eMMC 5.1 设备为例HS400 模式下的理论带宽为 400MB/s随机读 IOPS 可达 30K。但在blk-sq模式下实测随机读 IOPS 仅 12K根本原因是每次提交 bio 都需要获取request_queue的全局锁四个核在锁上轮转等待的时间超过实际 I/O 执行时间。Linux 内核从 3.13 引入 blk-mqMulti-Queue Block Layer就是为了解决多核扩展性问题。blk-mq 为每个 CPU 核心分配独立的软件提交队列消除全局锁竞争并为底层硬件如 eMMC 控制器的命令槽位映射为独立的硬件队列实现 I/O 提交路径的全链路并行化。理解 blk-mq 中软件队列与硬件队列的映射关系是优化 eMMC 存储性能的前提。二、blk-mq 的双层队列架构与 eMMC 请求分发路径blk-mq 的设计分为两层队列软件暂存队列Software Staging Queuectx-rq_list和硬件分发队列Hardware Dispatch Queuehctx-dispatch。上层应用提交的 bio 在内核中转换为struct request后根据当前 CPU 编号进入对应的软件队列。当软件队列积压一定量请求或者硬件队列空闲时请求通过-queue_rq回调被下发到 eMMC 驱动层。标签管理Tag Management是 blk-mq 实现请求跟踪的核心机制。每个硬件队列维护一个固定大小的 tag 池tag 数量等于硬件能同时处理的命令槽位数。当应用层提交 I/O 请求时blk-mq 从 tag 池中分配一个唯一的 tag 编号给该请求。eMMC 驱动在发送 CMD 时将此 tag 编号填入命令参数硬件控制器利用 tag 来匹配返回的响应和对应的请求。当驱动层 I/O 完成时通过 tag 编号找到原始请求完成 bio 的回调通知。三、eMMC blk-mq 驱动的初始化与请求处理流程eMMC 驱动切换到 blk-mq 需要在mmc_blk_probe中设置对应的tag_set参数/* eMMC 块设备驱动 blk-mq 初始化 — drivers/mmc/core/queue.c */ static int mmc_mq_init_queue(struct mmc_queue *mq, struct mmc_card *card) { struct mmc_host *host card-host; struct blk_mq_tag_set *set; int ret; set mq-tag_set; memset(set, 0, sizeof(*set)); /* 1. 配置 tag 集合参数 */ set-ops mmc_mq_ops; /* blk-mq 操作回调 */ set-nr_hw_queues 1; /* eMMC 通常只有 1 个硬件队列 */ /* 关键: 队列深度等于硬件命令槽位数 — 不能超过 eMMC 协议限制 */ set-queue_depth host-cmdq_depth ?: MMC_QUEUE_DEPTH; /* 默认 64 */ set-numa_node NUMA_NO_NODE; set-flags BLK_MQ_F_SHOULD_MERGE | BLK_MQ_F_BLOCKING; /* eMMC 操作可能阻塞 */ /* 2. 设置每个软件队列的最大请求数 */ set-cmd_size sizeof(struct mmc_queue_req); set-nr_maps 1; /* nr_queues nr_hw_queues ≈ 各 CPU 共享到同一个硬件队列的映射表 */ ret blk_mq_alloc_tag_set(set); if (ret) { dev_err(mmc_dev(host), Failed to alloc blk-mq tag set: %d\n, ret); return ret; } /* 3. 创建请求队列绑定到 gendisk */ mq-queue blk_mq_init_queue(set); if (IS_ERR(mq-queue)) { ret PTR_ERR(mq-queue); dev_err(mmc_dev(host), Failed to init blk-mq queue: %d\n, ret); blk_mq_free_tag_set(set); return ret; } /* 4. 设置 eMMC 特定的调度器参数 */ /* eMMC 建议使用 none 或 kyber 调度器 — mq-deadline 适合旋转介质 */ mq-queue-nr_requests set-queue_depth; /* 5. 启动内核线程处理 eMMC 请求 */ /* mmc_queue_thread 从硬件队列中取出请求并转化为 MMC 命令 */ sema_init(mq-thread_sem, 0); mq-thread kthread_run(mmc_queue_thread, mq, mmcqd/%d%s, host-index, mmc_card_mmc(card) ? : rpmb); if (IS_ERR(mq-thread)) { ret PTR_ERR(mq-thread); blk_cleanup_queue(mq-queue); blk_mq_free_tag_set(set); return ret; } return 0; } /* blk-mq 操作回调 — command 被 blk-mq 核心在硬件队列消费时调用 */ static blk_status_t mmc_mq_queue_rq(struct blk_mq_hw_ctx *hctx, const struct blk_mq_queue_data *bd) { struct request *req bd-rq; struct mmc_queue *mq hctx-driver_data; struct mmc_queue_req *mqrq blk_mq_rq_to_pdu(req); int ret; /* 1. 检查卡是否处于正常工作状态 */ if (mq-card mmc_card_removed(mq-card)) { /* 卡已拔出直接失败所有等待请求 */ blk_mq_start_request(req); return BLK_STS_IOERR; } /* 2. 将请求加入 eMMC 命令队列 */ mqrq-req req; blk_mq_start_request(req); /* 3. 唤醒 mmc_queue_thread 处理该请求 */ spin_lock_irq(mq-lock); list_add_tail(mqrq-queuenode, mq-queue_list); spin_unlock_irq(mq-lock); up(mq-thread_sem); /* 唤醒 mmcqd 内核线程 */ /* 返回 BLK_STS_RESOURCE 表示请求已排队但尚未完成 */ return BLK_STS_OK; /* eMMC 使用异步完成模式 */ } /* mmc_queue_thread 的核心处理循环 */ static int mmc_queue_thread(void *data) { struct mmc_queue *mq data; while (1) { /* 等待 blk-mq 提交请求或停止信号 */ if (down_interruptible(mq-thread_sem)) break; /* 批量处理请求减少上下文切换 */ spin_lock_irq(mq-lock); while (!list_empty(mq-queue_list)) { struct mmc_queue_req *mqrq list_first_entry(mq-queue_list, struct mmc_queue_req, queuenode); list_del(mqrq-queuenode); spin_unlock_irq(mq-lock); /* 将请求转化为 MMC 命令并发送 */ int ret mmc_blk_mq_issue_rq(mq, mqrq-req); if (ret) { blk_mq_end_request(mqrq-req, errno_to_blk_status(ret)); } spin_lock_irq(mq-lock); } spin_unlock_irq(mq-lock); } /* 线程退出时刷新队列 */ mmc_queue_flush(mq); return 0; }四、命令排队深度与调度延迟的架构权衡blk-mq 的队列深度参数直接影响 eMMC 的吞吐与延迟。较大的队列深度如 64可以让 eMMC 控制器始终有命令可执行提升带宽利用率但过深的队列意味着单个请求的排队延迟增加对于fsync这类需要顺序保证的操作尤为敏感。eMMC 5.1 引入了 CMD Queue命令队列特性支持最多 32 个排队命令控制器可以乱序执行以最大化吞吐但这与上层文件系统期望的 Barrier 语义产生了冲突。另一个需要权衡的点是软中断上下文与内核线程的处理模型。部分 eMMC 驱动选择在-queue_rq回调中直接发送命令软中断上下文而非通过内核线程转发。前者的延迟更低但软中断不能睡眠的约束限制了可执行的操作类型如不能进行需要睡眠的寄存器配置。后者的调度开销更大线程切换约 5~10us但可以在任何上下文中执行复杂的错误恢复逻辑。对于可插拔的 eMMC 设备推荐使用内核线程模型以支持卡插入/拔出时的热插拔事件处理。五、总结blk-mq 架构通过在软件层面为每个 CPU 分配独立队列、在硬件层面映射为驱动命令槽位解决了多核平台上的块 I/O 扩展瓶颈。eMMC 驱动的适配要点包括tag_set 参数必须匹配硬件命令槽位数tag 数量过多会浪费内存过少会限制并发度eMMC 的 CMD Queue 特性可以进一步提升随机吞吐但需要上层 Barrier 支持的配套修改内核线程 vs 软中断的处理模型选择取决于是否需要睡眠操作和热插拔支持队列深度需要在吞吐与延迟之间权衡建议通过基准测试确定最优值。