摘要
linux block layer第一篇介绍了bio数据结构及bio内存管理,本文章介绍bio的提交、拆分、io请求合并、io请求完成时的回调处理。由于“bio的提交”涉及内容太多,所以该小节只描述一些概要信息,在介绍完multi-queue机制后(待整理),再对着代码细说submit_bio流程。
内核源码:linux-5.10.3
一、bio的提交(submit)
提交bio的函数是submit_bio,这个函数做完account工作统计读写量后,调用submit_bio_noacct进行实际的提交操作。submit_bio_noacct根据存储设备是否定义了自己的提交函数(见struct block_device_operations中的submit_bio成员,比如dm设备执行dm_submit_bio),执行不同的操作。
图1 处理bio的路径
blk_mq_submit_bio将bio提交给存储设备处理,bio经不同路径被提交至不同的链表,最终由器件的host controller处理。bio的处理路径见图1,后继文章(待整理)会对代码细节展开描述。图1遵循以下原则:
1)按照路径标号值从小到大顺序,决策路径流。
2)io调度器队列、软件队列ctx是二选一关系。若存储设备有调度器,则启用io调度器队列,否则启用软件队列ctx。
路径1:flush、fua类型且有数据需要传输的bio执行此路径流。这类bio需要尽快分发给器件处理,不能缓存在上层队列中,否则会因io调度等不可控因素导致该bio延迟处理。这类bio首先被加入requeu list,rq加入requeu list后立即唤醒高优先级的工作队列线程kbockd,将rq其从requeue list移至硬件队列hctx头,并执行blk_mq_run_hw_queue将rq分发给器件处理。引入requeue list是为了复用已有代码,尽量让block flush代码着眼于flush机制本身,外围工作交给已有代码,可以使代码独立、精简。
路径2:io发起者执行了blk_start_plug(发起者预计接下来有多个io请求时,会执行此函数),且满足条件:“硬件队列数量为1”或者“存储设备定义了自己的commit_rqs函数”或者“存储设备是旋转的机械磁盘”,并且存储设备使用了io调度器,执行此路径流。这个场景是针对慢速存储设备的,将rq暂存至进程自己的plug list,然后将plug list中rq移至io调度器队列,由调度器选择合适的rq下发给器件处理。
路径3:io发起者执行了blk_start_plug(发起者预计接下来有多个io请求时,会执行此函数),且硬件队列hctx不忙的情况下,执行此路径流。这种场景充分利用存储器件处理能力,将rq在进程级plug list中短暂暂存后,将plug list中的rq尽快地下发给处于不忙状态的器件处理。
路径4:io发起者执行了blk_start_plug(发起者预计接下来有多个io请求时,会执行此函数),且存储器件没有使用io调度器、且硬件队列hctx处于busy状态,执行此路径流。这是一个通用场景,rq先在进程级的plug list缓存,然后存在软件队列ctx中,接着存至硬件队列hctx,最后分发给器件处理。
路径5:存储设备存在io调度器,执行此路径流。rq被放入调度器队列,由调度器决定rq的处理顺序。既然存在io调度器,就把rq交给调度器处理吧。
路径6:io发起者执行了blk_start_plug(发起者预计接下来有多个io请求时,会执行此函数),且存储设备设置了QUEUE_FLAG_NOMERGES(不允许合并io请求)、且没有io调度器,执行此路径流。这个场景下,仅做有限的合并,若bio与plug list中的rq可以合并则合并,否则就添加到plug中。plug list存在属于相同存储设备的rq,尝试将bio合并到plug list的rq,接着执行blk_mq_try_issue_directly将plug list中的rq发送到下层队列中,这体现了“有限合并”的含义,且只做一次合并,另外也意味着,任何时候,在plug list中只属于一个存储设备的rq有且只有一个。
路径7:未使用io调度器的前提下,硬件队列数量大于1且io请求是同步请求,或者硬件队列hctx不忙,执行此路径流。io请求通过rq->q->mq_ops->queue_rq(nvme设备回调函数是nvme_queue_rq)分发至host controller。
路径8:默认处理路径,上面路径条件都不满足,执行此路径流。这个路径是没有io调度器的(如果有调度器,执行路径5)。举些例子,在没有io调度器、没有启动plug list的前提下,执行该路径流:
1)如果是emmc、ufs这些单队列设备,io请求就执行此路径流。
2)如果是nvme这类支持多硬件队列的设备,io请求是异步的,执行此路径流。
3)如果是nvme这类支持多硬件队列的设备,io请求是同步的,但硬件队列busy,执行此路径流。
二、bio的拆分(split)
提交bio过程中,视情况,需要对bio拆分、合并(拆分后得到的bio不允许合并),逻辑图见图2。
图2 bio的拆分、合并
单个bio处理的数据大小是有限制的,这些限制参数存放在struct queue_limits结构体中。当bio处理的数据大小超出限制,这个bio就会被拆分成多个满足条件的bio,并组成一个chain,然后依次处理这些bio,直至chain中的所有bio处理完。
类型 | discard、 secure erase | write zeroes | write same | read、write |
块设备层 | max_discard_sectors | 无 | 无 | max_sectors |
器件 | max_hw_discard_sectors discard_granularity discard_alignment | max_write_zeroes_sectors | max_write_same_sectors | chunk_sectors logical_block_size physical_block_size max_segments max_segment_size virt_boundary_mask |
表1 io请求限制参数
1,参数说明
1)discard类bio
max_discard_sectors:block layer下发的单个discard大小的上限。
max_hw_discard_sectors:device能处理的单个discard大小的上限。
max_discard_sectors要小于等于max_hw_discard_sectors。
device处理大范围的discard比较耗时,将导致其他io较大的延迟。通过在block layer对单个discard的大小进行限制,将大范围的discard拆分成多个满足限制条件的小范围discard,从而减少其他io的延迟。
discard_granularity:discard操作的最小单元,值为0表示不支持discard。
discard_alignment:理解这个参数需要知道logic sector、physical sector概念(参考flash存储器件介绍,待整理)。physical sector是存储器件一次读写的最小单元,大小是logical sector的整数倍。内核filesystem、block layer都是基于512B sector设计的(即physical sector = 512B),现如今出现了sector大于512B 的器件(例如physical sector = 4K),为了适配内核,flash器件firmware逻辑上将physical sector分成多个512B大小的segment,这里的segment就称为logical sector。文件系统可以访问任意的logical sector,firmware读取logical sector所属的physical sector后,经过处理,返回对应logical sector的数据。分区可以从任意一个logical sector开始(即可以不与physical sector边界对齐,但这会降低性能)。图3用一个physical sector包含4个logical sector的例子(另假设dicard granularity等于1个physical sector大小)来说明这些概念的关系。
图3 physical sector、logical sector、discard_alignment关系
在上图中,dicard_alignment=7*512 bytes。
2)write zero类bio
write zero又称作single overwrite、zero fill erase、zero-fill。清零操作用于擦除device上的数据,防止数据被恢复、泄露,执行write zero后,读到的数据都是0。
max_write_zeroes_sectors:单次清零操作可以处理的最大范围。值为0代表device不支持write zero。
3)write same类bio
max_write_same_sectors:单个write same命令可以处理的最大范围。
4)read、write类bio
max_sectors:block layer允许单个io处理的数据长度上限,小于等于max_hw_sectors。
chunk_sectors:NVMe 1.3支持NOIOB(Namespace Optimal IO Boundaries),read、write请求涉及的sector不横跨(cross)chunk_sectors时(即按chunk_sectors对齐时),性能最佳(横跨chunk_sectors时,需要读2个chunk,没有横跨时只需读1个chunk)。值为0表示不支持chunk_sectors。如果NVMe低版本协议没有定义NOIOB,那么chunk_sectors等于max_hw_sectors,否则设置为NVMe器件定义的值struct nvme_id_ns->noiob(见nvme_set_chunk_sectors函数)。
图4 chunk_sectors示意图
logical_blo ck_size:device能够寻址的最小单元(参考图3说明)。
physical_block_size:device执行读写操作的最小单元((参考图3说明))。
max_segments:一个io请求包含的segment数量的最大值(struct bio结构体成员bi_io_vec是一个链表,链表的成员称作vector,vector描述的内存区域称作segment,详细描述见https://blog.csdn.net/geshifei/article/details/119959905)。
max_segment_size:一个segment的最大长度。
virt_boundary_mask:segment结束地址按其对齐。除第一个segment的起始地址、最后一个segment的结束地址可以不用对齐,其他的segment的起止地址需要按virt_boundary_mask对齐。
2,代码分析
write zeroes、write same的拆分很简单,仅需考虑一个限制参数,将bio按照该限制值进行拆分即可。discard类、read-write类不但要考虑大小限制,还得考虑boundary对齐,尽量使拆分后形成的每个bio能够按粒度对齐,这样才能达到最大的性能或者达到最优的状态。
1)discard类bio的拆分
先看一个例子,假设discard_alignment=0、discard_granularity=64、max_discard_sectors=128、bio对[2,257]范围内的sector执行discard的场景,如果仅仅根据max_discard_sectors分割bio,那么该bio将被拆分成[2,129]、[130,257]2个bio(每一个bio处理128个sector),如下图:
图5 未考虑discard粒度进行拆分(有问题,[128,191]没有discard)
对于bio1而言,器件不会对[128,129]执行discard。因为器件认为[130,191]这些sector中的数据是有效的,所以不能执行discard,如果对[128,129]执行discard,由于discard最小粒度限制,势必会将[130,191]这些sector也discard掉。同理[130,191]这些sector也没有执行discard。最终的效果是连续的、按粒度对齐的、满足max_discard_sectors要求的[128,191]共64个sector没有执行discard,这显然不是我们期望的。
正确的做法是:拆分一个bio时,将discard_alignment、discard_granularity限制条件考虑进去,拆分后形成的第1个bio起始sector可以不满足限制条件,但能确保后继bio的起始sector满足条件,这样就可以让拆分后形成的bio最多只可能有2个bio不能执行discard(第1个bio的起始sector不满足限制条件、最后一个bio的结束sector不满足限制条件)。对[2,257]范围内的sector执行discard,将会拆分成[2,127]、[128,255]、[256,257]这3个bio,[63,255]这些sector被执行discard(不会出现连续的、对齐的、满足粒度要求的[128,191]共64个sector没有执行discard的情况)。
图6 考虑discard粒度进行拆分(正确方法)
discard类bio拆分流程见:
submit_bio --> submit_bio_noacct --> __submit_bio_noacct_mq --> blk_mq_submit_bio --> __blk_queue_split --> blk_bio_discard_split:
//split bio要计算从bio中拆分出多少个的sector,作为拆分后得到的第一个bio的数据长度。即计算出第一个bio的结束位置,让后继的bio的起始sector都能满足表1限制条件, 62 static struct bio *blk_bio_discard_split(struct request_queue *q, 63 struct bio *bio, 64 struct bio_set *bs, 65 unsigned *nsegs) 66 { 67 unsigned int max_discard_sectors, granularity; 68 int alignment; 69 sector_t tmp; 70 unsigned split_sectors; 71 72 *nsegs = 1; 73 //将单个bio能处理的最大sector数量按discard粒度对齐,(max_discard_sectors % granularity)得到的余数部分对执行discard来说是没有意义的,因不满足最小粒度要求,器件是不会这些sector执行discard的,以避免属于同粒度范围内的其他sector也被discard掉。 74 /* Zero-sector (unknown) and one-sector granularities are the same. */ 75 granularity = max(q->limits.discard_granularity >> 9, 1U); 76 77 max_discard_sectors = min(q->limits.max_discard_sectors, 78 bio_allowed_max_sectors(q)); 79 max_discard_sectors -= max_discard_sectors % granularity; 80 81 if (unlikely(!max_discard_sectors)) { 82 /* XXX: warn */ 83 return NULL; 84 } 85 //判断bio待处理的sector数量,如果小于一次能处理的最大值,那么是不需要拆分的,直接返回。 86 if (bio_sectors(bio) <= max_discard_sectors) 87 return NULL; 88 //从89行开始,进入拆分逻辑,找到第一个bio的结束位置。 //一个bio能处理的最多sector数量不能超过max_discard_sectors,所以这里设置拆分出来的新bio的sector数量为max_discard_sectors。 89 split_sectors = max_discard_sectors; 90 91 /* 92 * If the next starting sector would be misaligned, stop the discard at 93 * the previous aligned sector. 94 */ //计算偏移量内(discard_alignment)的sector有多少个sector不满足一个粒度要求。 95 alignment = (q->limits.discard_alignment >> 9) % granularity; 96 //bio->bi_iter.bi_sector + split_sectors - alignment计算拆分后得到的第一个bio的结束sector号,确保从第2个的bio开始,他们的起始sector号(也即前一个bio的结束sector号)都是按physical logical对齐的。参考图3,如果一个分区的起始sector不是按physical sector对齐的,那么bio->bi_iter.bi_sector + split_sectors算出来的第一个bio的结束sector大概率(除了某个些特定的bio->bi_iter.bi_sector值)是无法按physical sector对齐(因为split_sectors已经是按granularity对齐,也必定按physical sector对齐,所以未按physical sector对齐的bio->bi_iter.bi_sector加上已经按physical sector对齐的split_sectors,最终得到的值肯定也不能按physical sector对齐)。解决方法是做个减法,让partition的起始sector按照physical sector对齐,即减去:(sectors of discard alignment)% (sectors of discard granularity)。 97 tmp = bio->bi_iter.bi_sector + split_sectors - alignment; //下面几行代码,将前面计算出的第一个bio结束sector号,但该sector可能没按discard granularity,这几行代码做个调整,将第一个bio的结束的sector按granularity对齐。最终,达到使第一个bio的结束sector号按physical sector对齐、按discard granularity对齐。 98 tmp = sector_div(tmp, granularity); 99 100 if (split_sectors > tmp) 101 split_sectors -= tmp; 102 //前面已经计算出需要从bio中拆分出多少个sector,下面开始实际的拆分操作 103 return bio_split(bio, split_sectors, GFP_NOIO, bs); 104 } |
2)read、write类bio拆分
需考虑3个条件:
第1个条件,一些器件不允许segment list存在gap,如果bio中的segment存在gap,则需要拆分(Another restriction inherited for NVMe - those devices don't support SG lists that have "gaps" in them. Gaps refers to cases where the previous SG entry doesn't end on a page boundary. For NVMe, all SG entries must start at offset 0 (except the first) and end on a page boundary (except the last).)。由virt_boundary_mask决定。
第2个条件,不能超过单个io请求能处理的最多sectors数量。由get_max_io_size函数根据max_sectors、chunk_sectors、logical_block_size、physical_block_size这几个参数计算得出。
第3个条件,不能超过单个io请求能处理的做多segment数量。由queue_max_segments函数根据max_segments、max_segment_size这几个参数计算得出。
read、write类bio拆分流程见:
submit_bio --> submit_bio_noacct --> __submit_bio_noacct_mq --> blk_mq_submit_bio --> __blk_queue_split --> blk_bio_segment_split
251 static struct bio *blk_bio_segment_split(struct request_queue *q, 252 struct bio *bio, 253 struct bio_set *bs, 254 unsigned *segs) 255 { 256 struct bio_vec bv, bvprv, *bvprvp = NULL; 257 struct bvec_iter iter; 258 unsigned nsegs = 0, sectors = 0; //根据限制参数,计算出一个io请求允许的max sectors、max segments。 259 const unsigned max_sectors = get_max_io_size(q, bio); 260 const unsigned max_segs = queue_max_segments(q); 261 262 bio_for_each_bvec(bv, bio, iter) { 263 /* 264 * If the queue doesn't support SG gaps and adding this 265 * offset would create a gap, disallow it. 266 */ //检查第1个条件(见前面描述),segment list是否有gap,如果有,就需要拆分。 267 if (bvprvp && bvec_gap_to_prev(q, bvprvp, bv.bv_offset)) 268 goto split; 269 //累加每个bio vector(也即bio segment)的sector,检查第2个条件看看是否超出max sectors,如果超出,就需要拆分。 bvec_split_segs函数中,还会检查第3个条件,看看累加的segment值是否超过max segments,如果超过了,也需要拆分。 270 if (nsegs < max_segs && 271 sectors + (bv.bv_len >> 9) <= max_sectors && 272 bv.bv_offset + bv.bv_len <= PAGE_SIZE) { 273 nsegs++; 274 sectors += bv.bv_len >> 9; 275 } else if (bvec_split_segs(q, &bv, &nsegs, §ors, max_segs, 276 max_sectors)) { 277 goto split; 278 } 279 280 bvprv = bv; 281 bvprvp = &bvprv; 282 } 283 284 *segs = nsegs; 285 return NULL; 286 split: 287 *segs = nsegs; //bio_split函数进行实际的拆分。新申请一个bio结构体(称作split bio),并复制原bio中的信息,将split bio读写数据的长度split->bi_iter.bi_size,并更新原bio的bvec_iter信息(用于bvec_iter只是bio完成的情况,因为拆分出去一部分交由split bio处理了,所以拆分出去的部分对于原bio而言视为已处理完成)。 288 return bio_split(bio, sectors, GFP_NOIO, bs); 289 } |
代码中涉及segment的概念,图示如下。
图7 vector与segment
拆分后的bio通过submit_bio --> submit_bio_noacct --> __submit_bio_noacct_mq --> blk_mq_submit_bio --> __blk_queue_split --> bio_chain形成chain(用struct bio的bi_private成员组成链表),并给新分配的bio设置REQ_NOMERGE标记,不允许该bio做merge操作。bio拆分示意图见图8。
图8 bio拆分示意图
三、请求的合并(merge)
1,合并的类型
io请求的合指bio的合并、rq的合并。submit_bio的入参是struct bio结构体,bio如果不能被合并到已有的rq中,就需要新申请一个rq存放bio,如果能合并到已有的rq中,rq中多个bio通过struct bio->bi_next 组成链表,链入struct request->biotail。
read、write类型请求合并存在ELEVATOR_BACK_MERGE、ELEVATOR_FRONT_MERGE两种类型。
ELEVATOR_BACK_MERGE:如果待合并的bio的结束sector号=rq中bio结束sector号,则将bio合并到已有bio的后面。
ELEVATOR_FRONT_MERGE:如果待合并的bio的结束sector号=rq中bio起始sector号,则将bio合并到已有bio的前面。
discard类型请求不关系请求的起止sector号,只要合并后不超过单个discard请求允许的max segment、max sector就可以合并。
图9展示了read、write类bio合并的过程,新提交的bio1、bio2、bio3需要尝试合并到已有的request A,request B中:
bio1的结束sector号等于rq A的起始sector号,做ELEVATOR_FRONT_MERGE。
bio2的起始sector号等于rq A的结束sector号,做ELEVATOR_BACK_MERGE。
bio2合并到rq A后,rq A、rqB变成连续空间,又可以做合并。
bio3不能合并到任何rq中,需要新申请一个struct request结构体存放bio3信息。
图9 read、write类型bio合并示意图
2,合并的目的
一个io的处理需要经由block layer、IO controller、device、hard IRQ、soft IRQ的处理,路径是很长的,在这些路径中会产生额外的开销。多个io请求合并成一个io,额外开销就很小。
multi-queue中硬件队列的tags数量是固定的(每一个tags与一个struct request关联),io请求超出tags限制值,需等待空闲可用的tags。
3,合并的策略
在不违反表1限制条件的情况下,才可以合并。可通过/sys/block/XX/queue/nomerges设置合并策略,见图10。
图10 io请求合并策略
4,代码分析
blk_mq_submit_bio代码:
blk_qc_t blk_mq_submit_bio(struct bio *bio) { __blk_queue_split(&bio, &nr_segs); // bio拆分 if (!bio_integrity_prep(bio)) goto queue_exit; //plug list merge:尝试将入参bio合并到plug list管理的rq中。 blk_queue_nomerges判断q有没有QUEUE_FLAG_NOMERGES标记,如果有就不尝试merge。 if (!is_flush_fua && !blk_queue_nomerges(q) && blk_attempt_plug_merge(q, bio, nr_segs, &same_queue_rq)) goto queue_exit; //调度器 merge:尝试将入参bio合并到调度器队列或者软件队列ctx管理的rq中。 if (blk_mq_sched_bio_merge(q, bio, nr_segs)) goto queue_exit; rq_qos_throttle(q, bio); |
bool blk_attempt_plug_merge(struct request_queue *q, struct bio *bio, unsigned int nr_segs, struct request **same_queue_rq) { struct blk_plug *plug; struct request *rq; struct list_head *plug_list; //当前进程没有启用plug list机制,直接退出 plug = blk_mq_plug(q, bio); if (!plug) return false; plug_list = &plug->mq_list; //遍历plug list中的rq(注意这里不需要加锁。若是将bio合并到调度器队列或者软件队列管理的rq中,就需要加锁。在无锁状态做merge操作,这是引入plug list的一个原因。) list_for_each_entry_reverse(rq, plug_list, queuelist) { if (rq->q == q && same_queue_rq) { /* * Only blk-mq multiple hardware queues case checks the * rq in the same queue, there should be only one such * rq in a queue **/ //same_queue_rq作用见前文描述的路径6,用于实现“有限合并”,确保任何时候,在plug list中属于一个存储设备的rq有且只有一个。 *same_queue_rq = rq; } if (rq->q != q) continue; //在plug list中找到了属于同一个存储器件的io请求rq,尝试将bio合并到这个rq中。 if (blk_attempt_bio_merge(q, rq, bio, nr_segs, false) == BIO_MERGE_OK) return true; } return false; } |
static enum bio_merge_status blk_attempt_bio_merge(struct request_queue *q, struct request *rq, struct bio *bio, unsigned int nr_segs, bool sched_allow_merge) { //检查软件层面的一些限制,合并的bio必须都是读或者都是写,io优先级必须一样,必须属于同一个存储器件等等。 if (!blk_rq_merge_ok(rq, bio)) return BIO_MERGE_NONE; //通过blk_try_merge选择合并策略,见前文 switch (blk_try_merge(rq, bio)) { //back merge,见前文。通过struct bio->bi_next 组成链表,链入struct request->biotail。 case ELEVATOR_BACK_MERGE: if (!sched_allow_merge || blk_mq_sched_allow_merge(q, rq, bio)) return bio_attempt_back_merge(rq, bio, nr_segs); break; //front merge,见前文。 case ELEVATOR_FRONT_MERGE: if (!sched_allow_merge || blk_mq_sched_allow_merge(q, rq, bio)) return bio_attempt_front_merge(rq, bio, nr_segs); break; //discard merge,见前文。 case ELEVATOR_DISCARD_MERGE: return bio_attempt_discard_merge(q, rq, bio); default: return BIO_MERGE_NONE; } return BIO_MERGE_FAILED; } |
以back merge为例(front merge、discard merge代码大同小异):
static enum bio_merge_status bio_attempt_back_merge(struct request *req, struct bio *bio, unsigned int nr_segs) { const int ff = bio->bi_opf & REQ_FAILFAST_MASK; //检查merge会不会导致单个io请求的max sector、max segment超出限制,检查merge会不会引起segment的“gap”。 if (!ll_back_merge_fn(req, bio, nr_segs)) return BIO_MERGE_FAILED; trace_block_bio_backmerge(req->q, req, bio); rq_qos_merge(req->q, req, bio); if ((req->cmd_flags & REQ_FAILFAST_MASK) != ff) blk_rq_set_mixed_merge(req); //将bio合并到已有rq管理的bio中 req->biotail->bi_next = bio; req->biotail = bio; req->__data_len += bio->bi_iter.bi_size; bio_crypt_free_ctx(bio); blk_account_io_merge_bio(req); return BIO_MERGE_OK; } |
四、io完成时的处理(completion)
以multi queue + nvme为例,io完成时流程如下:
图11 io完成时的处理流程
1,nvme注册硬中断处理函数nvme_irq
nvme初始化时执行reset work,注册irq函数。调用链为nvme_reset_work --> nvme_setup_io_queues --> nvme_setup_irqs --> queue_request_irq。
static int queue_request_irq(struct nvme_queue *nvmeq) { struct pci_dev *pdev = to_pci_dev(nvmeq->dev->dev); int nr = nvmeq->dev->ctrl.instance; //注册中断处理函数 if (use_threaded_interrupts) { return pci_request_irq(pdev, nvmeq->cq_vector, nvme_irq_check, nvme_irq, nvmeq, "nvme%dq%d", nr, nvmeq->qid); } else { return pci_request_irq(pdev, nvmeq->cq_vector, nvme_irq, NULL, nvmeq, "nvme%dq%d", nr, nvmeq->qid); } } |
2,block注册软中断处理函数blk_done_softirq
subsys_initcall(blk_mq_init),这个subsys_initcall函数在《linux block layer第一篇bio 子系统数据结构及初始化》中已经描述过,系统启动过程中会执行blk_mq_init函数。这里只看blk_mq_init函数实现:
static int __init blk_mq_init(void) { int i; for_each_possible_cpu(i) INIT_LIST_HEAD(&per_cpu(blk_cpu_done, i)); //注册block软中断 open_softirq(BLOCK_SOFTIRQ, blk_done_softirq); cpuhp_setup_state_nocalls(CPUHP_BLOCK_SOFTIRQ_DEAD, "block/softirq:dead", NULL, blk_softirq_cpu_dead); cpuhp_setup_state_multi(CPUHP_BLK_MQ_DEAD, "block/mq:dead", NULL, blk_mq_hctx_notify_dead); cpuhp_setup_state_multi(CPUHP_AP_BLK_MQ_ONLINE, "block/mq:online", blk_mq_hctx_notify_online, blk_mq_hctx_notify_offline); return 0; } subsys_initcall(blk_mq_init); |
3,blk_done_softirq代码分析
blk_done_softirq执行rq的complete回调函数rq->q->mq_ops->complete(rq),即nvme_pci_complete_rq。调用链如下,注意何时会执行bio、rq的回调函数:
nvme_pci_complete_rq
|--- nvme_complete_rq
|--- nvme_end_rq
|--- blk_mq_end_request
|--- blk_update_request
| |--- req_bio_endio
| |--- bio_endio
|--- __blk_mq_end_request
|--- rq->end_io(如果有回调的话),否则blk_mq_free_request