| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> 系统运维 -> linux block layer第二篇bio 的操作 -> 正文阅读 |
|
[系统运维]linux block layer第二篇bio 的操作 |
摘要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处理完。
表1 io请求限制参数 1,参数说明1)discard类biomax_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类biowrite zero又称作single overwrite、zero fill erase、zero-fill。清零操作用于擦除device上的数据,防止数据被恢复、泄露,执行write zero后,读到的数据都是0。 max_write_zeroes_sectors:单次清零操作可以处理的最大范围。值为0代表device不支持write zero。 3)write same类biomax_write_same_sectors:单个write same命令可以处理的最大范围。 4)read、write类biomax_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:
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
代码中涉及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代码:
以back merge为例(front merge、discard merge代码大同小异):
四、io完成时的处理(completion)以multi queue + nvme为例,io完成时流程如下: ?图11 io完成时的处理流程 1,nvme注册硬中断处理函数nvme_irqnvme初始化时执行reset work,注册irq函数。调用链为nvme_reset_work --> nvme_setup_io_queues --> nvme_setup_irqs --> queue_request_irq。
2,block注册软中断处理函数blk_done_softirqsubsys_initcall(blk_mq_init),这个subsys_initcall函数在《linux block layer第一篇bio 子系统数据结构及初始化》中已经描述过,系统启动过程中会执行blk_mq_init函数。这里只看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 |
|
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/15 17:15:29- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |