摘要

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

linux block layer第二篇bio 的操作相关推荐

  1. 【Linux系统】第二篇、权限管理篇

    文章目录 一.Linux下的用户 二.文件的权限 1. 文件访问者的分类 2. 文件类型和访问权限 3. 文件权限值的表示方法 三.文件访问权限的相关设置方法 1. chmod 2. chown 3. ...

  2. Linux进程资源管理第二篇   -------- 工作及资源管理

        工作管理 1.工作管理简介 Linux中的工作管理指的是在单个登录的终端中,同时管理多个工作的行为.在Linux工作管理中,其实就是将当前的工作放到后台执行.查询当前系统中的后台程序使用的命令 ...

  3. linux设备驱动第二篇:构造和运行模块

    上一篇介绍了linux驱动的概念,以及linux下设备驱动的基本分类情况及其各个分类的依据和差异,这一篇我们来描述如何写一个类似hello world的简单测试驱动程序.而这个驱动的唯一功能就是输出h ...

  4. 鸟哥的Linux私房菜(基础篇)- 第二十六章、Linux 核心编译与管理

    第二十六章.Linux核心编译与管理 最近升级日期:2009/09/18 我们说的 Linux 其实指的就是核心 (kernel) 而已.这个核心控制你主机的所有硬件并提供系统所有的功能,所以说,他重 ...

  5. linux IO Block layer 解析

    早期的 Block 框架是单队列(single-queue)架构,适用于"硬件单队列"的存储设备(比如机械磁盘),随着存储器件技术的发展,支持"硬件多队列"的存 ...

  6. 初学Python——文件操作第二篇

    前言:为什么需要第二篇文件操作?因为第一篇的知识根本不足以支撑基本的需求.下面来一一分析. 一.Python文件操作的特点 首先来类比一下,作为高级编程语言的始祖,C语言如何对文件进行操作? 字符(串 ...

  7. Linux 块设备,Block Layer层架构演变

    前言 Block Layer层在整个I/O中负责承上启下,上接文件系统,下接块驱动. 我不想直接讨论代码,希望从一个架构的演变来初探一下Block Layer层. 一.1.0版本 首先我们来了解几个重 ...

  8. imx6ull移植Linux系统第二篇——Linux内核的移植

    imx6ull移植Linux系统第二篇--Linux内核的移植 花了大概两周的时间,把Linux的移植认真学了一遍,期间踩了不少坑,花费了不少时间去解决各种奇奇怪怪的问题,最终完成了uboot.系统镜 ...

  9. Linux常用命令(本篇包括,Linux目录结构介绍、Linux Shell介绍、9个常见命令介绍、文件的概念、文件的操作(20个)、目录的操作、文件和目录的权限、文件压缩及解压缩)

    Linux常用命令(本篇包括,Linux目录结构介绍.Linux Shell介绍.9个常见命令介绍.文件的概念.文件的操作(20个).目录的操作.文件和目录的权限.文件压缩及解压缩)         ...

最新文章

  1. 富士通打印机调整位置_打印机为什么卡纸 打印机四种卡纸原因及解决办法【介绍】...
  2. 有监督回归:鲁棒学习
  3. PASSWORD,ENABLE,CONSOLE,VTY及TACACS认证顺序及区别
  4. linux 网络装机,如何搭建和配置PXE网络装机服务
  5. pytorch 回归预测(时间序列)
  6. Java-线程中sleep()、wait()和notify()和notifyAll()、suspend和resume()、yield()、join()、interrupt()的用法和区别
  7. 获取论坛cookie_注意:这是你成为Cookie时尚社区OG的最后机会!
  8. ODBC数据源的配置方法
  9. f(x)=sinx的求导过程
  10. html跳转浏览器打开新页面打开新窗口,用JS控制打开新窗口
  11. 史密斯圆图matlab,用MATLAB程序实现Smith圆图的图解过程
  12. WINDOWS操作系统发展历程
  13. OllyDbg插件编写
  14. java 小工 到_测试小工——从0到1搭建最简单的Linux网站
  15. 365天挑战LeetCode1000题——Day 124 单调队列模板
  16. Android 使用JS注入获取网页视频链接
  17. linux没有ifconfig命令
  18. 中国网络电视台首推呜呜组啦消除版(vuvuzela free)世界杯视频
  19. 基于几何约束的传动机构设计
  20. 三星s7 android已停止,如何从三星Galaxy S7 / S7 Edge恢复已删除/丢失的数据

热门文章

  1. sqlserver2017镜像文件下载地址
  2. 英特尔方之熙:钟情嵌入式 摩尔定律多片用武之地
  3. 现代HYUNDAI EDI需求分析
  4. MIT 18.06 线性代数公开课笔记 Lecture02 矩阵消元
  5. Python【算法设计与分析】穷举法
  6. Modbus寄存器分类及地址分配
  7. 2022年全球与中国质谱分析软件市场现状及未来发展趋势
  8. RS232电平与TTL电平
  9. Ubuntu14.04键盘布局乱码
  10. 【NOI2008】【BZOJ1061】志愿者招募