block多队列分析 - 2. block多队列的初始化

本文解析了null_device的初始化过程,深入剖析了null_init函数中的null_b_add_dev,重点关注了blk_mq_tag_set的初始化、request_queue的创建与blk_mq_tag_set与request_queue的关系。介绍了如何通过blk_mq_alloc_tag_set和blk_mq_init_queue_data构建软硬件队列映射,以及elevator初始化的重要步骤。

1. 前言

本专题文章是对多队列部分学习的笔记。主要以null_dev为例来研究多队列的工作机制, 为了能够凸显多队列的工作流程,只对block层做重点分析说明,对其它代码只做注释性说明。本文主要通过分析null blk块设备的初始化流程,来理解多队列的初始化,流程。

kernel版本:5.10
平台:arm64

注:
为方便阅读,正文标题采用分级结构标识,每一级用一个"-“表示,如:两级为”|- -", 三级为”|- - -“

2. null_init

static int __init null_init(void)
    |--struct nullb *nullb;
    |  struct nullb_device *dev;
    |--config_group_init(&nullb_subsys.su_group)
    |--configfs_register_subsystem(&nullb_subsys)
    |--register_blkdev(0, "nullb")
    \--for (i = 0; i < nr_devices; i++)
        dev = null_alloc_dev();
        null_add_dev(dev);

null_init为null dev创建gendisk并注册进系统,创建软硬件队列及其映射关系,期间也会为null设备创建派发队列request_queue并初始化

  1. config_group_init 初始化cofigfs group以供使用

  2. configfs_register_subsystem: 将config group注册进configfs

  3. null_alloc_dev 分配nullb_device,并初始化nullb_device的成员

  4. register_blkdev完成了nullb块设备的注册,实际是添加到全局major_names数组中

  5. null_add_dev为null设备创建gendisk并注册进系统,同时也创建了软硬队列的映射关系

static int null_add_dev(struct nullb_device *dev) 
    |--struct nullb *nullb;
    |--null_validate_conf(dev)
    |  //tag_set嵌入nullb,因此分配nullb时连同nullb->tag_set一起分配
    |--nullb = kzalloc_node(sizeof(*nullb), GFP_KERNEL, dev->home_node);
    |  nullb->dev = dev;
    |  dev->nullb = nullb
    |
    |--setup_queues(nullb)
    |--if (dev->queue_mode == NULL_Q_MQ)
    |      null_init_tag_set(nullb, nullb->tag_set)
    |      nullb->q = blk_mq_init_queue_data(nullb->tag_set, nullb);
    |  else if (dev->queue_mode == NULL_Q_BIO)
    |      nullb->q = blk_alloc_queue(dev->home_node);
    |      rv = init_driver_queues(nullb);
    |--nullb->q->queuedata = nullb;
    |--blk_queue_logical_block_size(nullb->q, dev->blocksize)
    |--blk_queue_physical_block_size(nullb->q, dev->blocksize)
    |--null_config_discard(nullb);
    \--null_gendisk_register(nullb)
        |--struct gendisk *disk;
        |--disk = nullb->disk = alloc_disk_node(1, nullb->dev->home_node);
        |--set_capacity(disk, size);
        |--if (queue_is_mq(nullb->q))
        |      disk->fops = &null_rq_ops;
        |  else
        |      disk->fops = &null_bio_ops;
        \--add_disk(disk)

null_add_dev创建tag set并初始化,创建软硬件队列及其映射关系,为null设备创建request_queue并初始化,为null设备创建gendisk并注册进系统

  1. setup_queues:主要为nullb_queue分配空间,个数由nr_cpu_ids来定义,同时也设置了队列深度
    nullb->queue_depth = nullb->dev->hw_queue_depth

  2. null_init_tag_set:对nullb的内嵌blk_mq_tag_set进行初始化,为set->mq_map分配空间,用于保存软硬队列的映射关系建立软硬件队列的映射关系,set->mq_map数组下标为cpu编号,数组元素为硬队列号。根据队列深度为每个硬件队列分配bitmap,创建request。

  3. blk_mq_init_queue_data创建request_queue并对其进行初始化,这里只考虑了多队列的情况

  4. null_gendisk_register注册nullb磁盘设备。首先alloc_disk_node分配gendisk,add_disk 将gendisk注册进系统

|- -blk_mq_alloc_tag_set

static int null_init_tag_set(struct nullb *nullb, struct blk_mq_tag_set *set)
	|  //对nullb的内嵌blk_mq_tag_set进行初始化
	|--set->ops = &null_mq_ops;
	|  set->nr_hw_queues = nullb ? nullb->dev->submit_queues : g_submit_queues;//初始化硬队列个数
	|  set->queue_depth = nullb ? nullb->dev->hw_queue_depth : g_hw_queue_depth;//初始化硬队列深度
	|  set->numa_node = nullb ? nullb->dev->home_node : g_home_node;
	|  set->cmd_size   = sizeof(struct nullb_cmd);
	|  set->flags = BLK_MQ_F_SHOULD_MERGE; 
	|  if (g_no_sched)
	|		set->flags |= BLK_MQ_F_NO_SCHED;
	|  if (g_shared_tag_bitmap) 
	|  		set->flags |= BLK_MQ_F_TAG_HCTX_SHARED;
	|  if ((nullb && nullb->dev->blocking) || g_blocking)
	|  		set->flags |= BLK_MQ_F_BLOCKING;
    \--blk_mq_alloc_tag_set(set)
    	|  //分配set->nr_hw_queues个struct blk_mq_tags 
    	|--blk_mq_realloc_tag_set_tags
<think>我们正在讨论Linux块层多队列(blk-mq)架构中BIO进入软件队列的时机。根据之前的引用和blk-mq的设计,我们可以分析如下: 在blk-mq架构中,BIO(Block I/O)请求在提交过程中会被转化为请求(request),然后分配到对应的软件队列(software queue,也称为软件上下文,由`blk_mq_ctx`结构表示)。BIO进入软件队列的时机主要发生在请求分配(allocation)和调度(scheduling)阶段。 具体步骤和时机如下: 1. **BIO提交**:当上层(文件系统、直接I/O等)提交一个BIO时,会调用`submit_bio`函数,该函数将BIO传递给块层处理。 2. **BIO转换为request**:在blk-mq中,BIO会被尝试合并到现有请求中(在软件队列的调度器中进行,如果有调度器的话),或者分配一个新的request。这个转换过程发生在请求分配阶段。 3. **选择软件队列**:在分配request之前,需要确定将请求放入哪个软件队列。软件队列是按CPU或节点(node)分配的,通常根据当前CPU ID来选择对应的软件队列(即`blk_mq_ctx`)。选择软件队列的时机是在BIO提交时,通过调用`blk_mq_get_ctx`函数获取当前CPU对应的软件队列上下文。 4. **获取请求标签(tag)**:在blk-mq中,每个请求需要一个标签(tag)来标识它在硬件队列中的位置。标签的分配是通过`blk_mq_get_tag`函数尝试从硬件队列的标签集中获取一个空闲标签。如果获取成功,则可以为该BIO分配一个request结构体。 5. **分配request结构体**:一旦获取到标签,就会分配一个request结构体,并将BIO的内容复制到request中(或者将BIO链接到request中)。 6. **将请求加入软件队列**:分配request后,这个request会被加入到之前选择的软件队列(`blk_mq_ctx`)中。这个加入操作是在`blk_mq_add_to_requeue_list`函数(或类似函数)中完成的,它会将request添加到软件队列的某个链表(例如,`ctx->rq_list`)中。注意,这时候请求已经进入了软件队列。 7. **调度请求到硬件队列**:软件队列中的请求不会立即提交到硬件队列,而是等待调度。调度可以由以下几种情况触发: - 当软件队列中的请求数量达到一定阈值(例如,批量处理)时; - 定时器超时(延迟提交); - 直接提交(例如,需要立即提交的请求)。 调度过程会将软件队列中的请求按照映射关系(通过`mq_map`)发送到对应的硬件队列(由`blk_mq_hw_ctx`表示),然后由硬件队列的驱动(例如NVMe驱动程序)处理。 因此,BIO进入软件队列的时机是在**BIO被转换为request并分配到当前CPU对应的软件队列**时,具体发生在`blk_mq_get_ctx`获取软件队列上下文之后,并在`blk_mq_add_to_requeue_list`(或类似函数)将请求加入软件队列链表时。 引用说明: - 引用[1]提到,为每个CPU核心创建一个软队列,应用访问软队列可以做到完全私有访问,这表明BIO提交时绑定到当前CPU的软队列。 - 引用[2]描述了软硬队列映射的初始化,这为BIO选择软队列提供了基础。 - 引用[3]指出多队列避免了锁竞争,因为每个CPU有自己的软队列,因此BIO提交到软队列时不需要加锁。 总结:BIO在提交过程中,通过当前CPU ID选择对应的软队列(`blk_mq_ctx`),然后分配请求并将请求加入该软队列的链表中,此时BIO就进入了软件队列。 接下来,我们可以生成相关问题以延伸讨论。 </think>### Block多队列中BIO进入软件队列的时机分析 在Linux块层多队列(blk-mq)架构中,BIO进入软件队列的时机发生在以下关键阶段: #### 1. BIO提交阶段 ($ \text{submit\_bio}() $) 当上层提交BIO时: ```c void submit_bio(struct bio *bio) { blk_mq_submit_bio(bio); // 入口函数 [^2] } ``` 此时BIO尚未进入任何队列,处于原始提交状态。 #### 2. 软件队列选择 ($ \text{blk\_mq\_get\_ctx}() $) 内核根据当前CPU ID选择目标软件队列: $$ \text{ctx} = \text{blk\_mq\_get\_ctx}(q) $$ 其中: - $ q $ = 请求队列 (`request_queue`) - $ \text{ctx} $ = 软件队列上下文 (`blk_mq_ctx`) **映射逻辑**: - 默认:CPU ID → 软件队列 1:1映射 - NUMA优化:CPU节点 → 软件队列映射 [^1] #### 3. 请求分配 ($ \text{blk\_mq\_get\_tag}() $) ```c struct request *rq = blk_mq_get_request(q, bio, &tag); ``` 此阶段完成: 1. 从硬件队列标签集分配空闲tag 2. 创建request结构封装BIO 3. **关键动作**:将request加入`ctx->rq_list`软件队列链表 [^2] #### 4. 调度器介入时机 软件队列类型决定后续流向: | 队列类型 | BIO进入后动作 | 触发硬件队列时机 | |----------------|----------------------------------|--------------------------| | 无调度器 | 直接映射到硬件队列 | 立即触发`->queue_rq()` | | 有调度器(e.g., kyber) | 先进入调度器队列 | 调度器决策后触发 | #### 5. 提交到硬件队列 ($ \text{blk\_mq\_run\_hw\_queue}() $) 最终通过软中断或工作队列触发: ```c blk_mq_run_hw_queue(hctx, async); // 实际提交到硬件 [^3] ``` ### 性能关键路径图解 ```mermaid graph TD A[BIO提交] --> B{选择软件队列} B -->|依据CPU ID| C[blk_mq_ctx] C --> D[分配request/tag] D --> E[加入ctx->rq_list] E --> F{调度器?} F -->|无| G[直接映射硬件队列] F -->|有| H[进入调度器队列] H --> I[调度决策] I --> G G --> J[激活硬件队列hctx] ``` 设计优势: 1. **零锁竞争**:CPU私有访问软件队列 [^1] 2. **延迟优化**:BIO→软件队列路径无阻塞 3. **并行扩展**:软件队列数随CPU线性增长 典型延迟瓶颈出现在调度器决策阶段(如有)或硬件队列饱和时[^3]。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值