news 2026/7/16 19:45:56

Linux 块设备多队列架构逐层拆解:blk-mq 在 eMMC 驱动中的硬软队列调度机制

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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-mq(Multi-Queue Block Layer)就是为了解决多核扩展性问题。blk-mq 为每个 CPU 核心分配独立的软件提交队列,消除全局锁竞争;并为底层硬件(如 eMMC 控制器的命令槽位)映射为独立的硬件队列,实现 I/O 提交路径的全链路并行化。理解 blk-mq 中软件队列与硬件队列的映射关系,是优化 eMMC 存储性能的前提。

二、blk-mq 的双层队列架构与 eMMC 请求分发路径

blk-mq 的设计分为两层队列:软件暂存队列(Software Staging Queue,ctx->rq_list)和硬件分发队列(Hardware Dispatch Queue,hctx->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 驱动的适配要点包括:

  1. tag_set 参数必须匹配硬件命令槽位数,tag 数量过多会浪费内存,过少会限制并发度;
  2. eMMC 的 CMD Queue 特性可以进一步提升随机吞吐,但需要上层 Barrier 支持的配套修改;
  3. 内核线程 vs 软中断的处理模型选择取决于是否需要睡眠操作和热插拔支持;
  4. 队列深度需要在吞吐与延迟之间权衡,建议通过基准测试确定最优值。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/16 19:44:55

CANN/asc-devkit SIMD-API函数文档

asc_set_l13d_fmatrix_b 【免费下载链接】asc-devkit 本项目是CANN 推出的昇腾AI处理器专用的算子程序开发语言,原生支持C和C标准规范,主要由类库和语言扩展层构成,提供多层级API,满足多维场景算子开发诉求。 项目地址: https:/…

作者头像 李华
网站建设 2026/7/16 19:44:21

Spyc测试驱动开发:使用PHPUnit进行YAML解析器单元测试

Spyc测试驱动开发:使用PHPUnit进行YAML解析器单元测试 【免费下载链接】spyc A simple YAML loader/dumper class for PHP 项目地址: https://gitcode.com/gh_mirrors/sp/spyc Spyc作为一款简单的PHP YAML加载器/转储器类,采用测试驱动开发(TDD)模…

作者头像 李华
网站建设 2026/7/16 19:43:47

Ursine:Bear风格的Typora主题,让你的写作体验焕然一新

Ursine:Bear风格的Typora主题,让你的写作体验焕然一新 【免费下载链接】typora-theme-ursine A Typora theme, inspired by Bear 项目地址: https://gitcode.com/gh_mirrors/ty/typora-theme-ursine 想要提升你的Typora写作体验吗?Urs…

作者头像 李华
网站建设 2026/7/16 19:43:42

如何通过Aave Protocol V2成为流动性提供者:新手入门赚取被动收入

如何通过Aave Protocol V2成为流动性提供者:新手入门赚取被动收入 【免费下载链接】protocol-v2 Aave Protocol V2 项目地址: https://gitcode.com/gh_mirrors/pro/protocol-v2 Aave Protocol V2是一个开源的去中心化借贷协议,允许用户通过提供流…

作者头像 李华
网站建设 2026/7/16 19:42:01

openharmony1.1.3 通过i2c进行温湿度采集

温湿度传感器 也是挂载这个i2c总线上初始化?读数据写数据1.1.3 版本的i2c底层接口:默认 系统没有开启i2c# CONFIG_I2C_SUPPORT is not set CONFIG_I2C_SUPPORT y开启i2c写一套通过i2c 读写温湿度的文件:aht20.h:#ifndef AHT20_H#define AHT2…

作者头像 李华
网站建设 2026/7/16 19:38:22

全网最全|2026Java八股文题库(持续更新),背完直接上岸

很多 Java 工程师的技术不错,但是一面试就头疼,10 次面试 9 次都是被刷,过的那次还是去了家不知名的小公司。 问题就在于:面试有技巧,而你不会把自己的能力表达给面试官。 应届生:你该如何准备简历&#…

作者头像 李华