0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

深度剖析基于块层的组成“request层”

Linux阅码场 2018-02-03 16:29 次阅读

Linux 块层向上为文件系统和块设备提交接口,使得上层能够以统一的方式访问各种类型的后端存储设备。同时,它也向下为设备驱动提供接口,让驱动层能够以一致的方式来接受请求。一些驱动如上一篇文章中提到的DRBD和RBD设备,只使用bio层提供的一些接口,对bio请求进行处理。其它的驱动则可以从IO请求plugging机制,各种请求排序和请求合并中受益。为了给驱动层提供服务,块层做了不少事情,我且称之为"request层"。

现在,"request层"并存着两种模型:单队列(single-queue) 和 多队列(multi-queue)。多队列的出现也就是近几年的事情,也许总有一天会完全取代单队列的,但是目前来看两者在内核的使用都相当活跃。有了这两种不同的排队方法(queuing approach)做参考,我们就可以两相对比来学习学习,所以我们将花点时间都看看,分析他们如何把请求呈现给驱动层的。首先,我们来看看两者的共同之处,分析这两个关键的结构体是个不错的入手点: struct request_queue 和 struct request。

请求队列和请求

struct request_queue之于struct request的关系,非常像struct gendisk之于struct bio的关系:一个代表具体的设备,一个代表IO请求。需要注意的是,每个gendisk都有一个关联的request_queue结构体,但是只有那些使用了"request层"的设备才会分配request结构体。按理说,适用于所有块设备的域,比如struct queue_limits,都应该放到gendisk结构体里面,而对于一些仅适用于"request层"队列管理的域,其实只应当分配给那些使用了"request层"的设备。现在来看,这些结构体里有些域的安排可能就是历史巧合,也不值得去纠正了。各种队列相关的域,都可以在/sys/block/*/queue/目录下查看。

request结构体代表单个IO请求,最终要传递到底层设备。一个request包含一个或多个代表连续IO请求的bio,一些跟踪总体状态的信息(比如时间戳,哪个CPU发来的请求),和一些用来链入更大数据结构的“锚点”,比如用struct list_head queuelist链入一个简单的队列结构,用struct hlist_node hash链入一个哈希表(用来查找与新bio相邻的请求),用struct rb_node rb_node来把请求放在一棵红黑树上。在分配一个request结构体的时候,有时候要分配一些额外的空间来给底层驱动保存一些额外的信息。有时候这些空间用来保存命令头部,以便发送给底层设备,比如 SCSI command descriptor block,驱动可以自由地决定如何使用这块空间。

相应的make_request_fn()函数 (单队列是blk_queue_bio,多队列是blk_mq_make_request())为IO请求创建一个request结构体,然后把交给IO调度器, 也叫电梯算法"elevator", 这个名字来自电梯算法( elevator algorithm),电梯算法曾是磁盘IO调度一个里程碑式的成就。我们将快速看一下单队列的实现,然后再对比地去看多队列。

深度剖析基于块层的组成“request层”

单队列上的请求调度

过去,大多存储设备都是机械硬盘,要通过磁头寻道,盘片旋转来定位数据。机械盘一次只能处理一个请求,从盘片上一个位置挪动到下一个位置代价很大。单队列的实现就是为这种类型的设备而生的,然后渐渐地也能支持其它快速设备,然而单队列整体结构依然反映了旋转类型存储设备的需求。

单队列调度器主要有三个任务:

积聚代表连续IO操作的多个bio,合并成更大的IO请求,充分发挥硬件性能,但是请求不能太大超高硬件设备的限制;

把请求进行排序来减少寻道时间,但是又要保证重要的请求得到及时处理。不断寻求较优方案,来解决这个问题,是这一部分代码复杂度的根源。一般很难知道这两点:一个请求有多重要,和一个请求需要多少寻道时间,我们只能依赖启发式方法来判断怎样排序比较好,然而启发式方法绝不是完美的;

把经过整理的请求列表交给底层驱动,让驱动从队列取下请求去处理,同时提供一个通知机制来告诉上层请求处理的结果。

最后一个任务很直接了当。驱动会通过blk_init_queue_node()来注册一个request_fn()策略例程,只要队列上有新请求已经准备好,策略例程被调用去处理这个请求。驱动负责用blk_peek_request()来从队列上取下请求,然后进行处理。当请求处理完毕 [有些设备可以并行处理多个请求],驱动会从队列上摘下另一个请求来处理,而不用再去调用request_fn() [因为request_fn()一次拿到了整个列表上的所有request]。请求一旦处理完毕,就可调用blk_finish_request()来通知上层。

第一个任务有部分是用elv_attempt_insert_merge()完成的,它会快速地检查队列,看是否可以找到一个已经存在的request,把新的bio合并进入。如果成功,调度器就会允许合并,如果失败,调度器还有一次机会尝试在调度器内部进行一次合并。如果没有机会合并,就分配一个新的请求,然后交给调度器。稍后一会,如果某个请求因合并而长大,使得它与该请求变成了连续的,调度器就可以再次尝试合并操作。

第二个任务可能是最复杂的。如何把请求按照"适当"顺序排队非常依赖我们如何来解释"适当"的含义。这三种不同的单队列调度器: noop, deadline和cfq,对“适当”的解释就非常不一样。

"noop"会对请求做一些简单的排序,不允许把读请求挪到写请求之前,反之亦然;依据电梯算法,一个同类型的请求可以插入到另一个之前。除了elv_dispatch_sort()所做简单排序,"noop"就是一个FIFO队列。

“deadline”会把提交时间相近的请求放在一批。在同一批中,请求会被排序。当一批请求达到了大小上限或着定时器超时,这批请求就会提交到设备队列上去。这个算法尝试给每一个请求都设置一个延迟时间上限,同时尽量聚集比较大的一批请求。

"cfq"即"Complete Fairness Queuing",相比其它几个调度器要复杂很多,目的是在不同进程或进程组间保证IO资源使用的公平性。cfq调度器内部维护了多个队列,每一个进程都有一个队列来保存来自该进程的同步请求(通常是读),而对于异步请求(通常是写),每一个优先级都有一个队列,所有请求不论来自哪个进程都按照优先级放到相应的队列上。在提交请求时,按照优先级每个队列都有机会得到调度。每个队列都有一定的时间片,在时间片内才能提交一定数量的请求。当一个同步队列中的请求不足一定数量时,这个设备可以空闲一会,即使其它队列里可能有请求等待处理。通常,同步请求之间在磁盘上的物理位置是连续的,所以让磁盘稍等一会来接收更多连续的请求,这样做可以提高吞吐量。以上对CFQ的描述仅仅是点皮毛。内核文档(Documentation/block/cfq-iosched.txt)讲的更详细点,还列出了所有参数,通过调整这些参数能够适应各种不同的场景。

之前提到过,一些高端点的设备可以一次处理多个请求,即在一个请求还没有处理完成之前,就能够处理新的请求。通常,这个要用到"tagging"功能,给每一个请求加一个标签,这样请求完成通知就能和原来的请求正确的对应起来。单队列的"request层"可以对任意深度的设备提供"tagging"功能。

一个设备内部可以通过真正地并行处理请求,来支持被标记的命令,比如通过访问一个内存缓存,通过设计多模块而每个模块都能处理一个请求,或者通过其内部队列,这样的队列比"request层"更加了解设备。

多队列和多CPU

多队列的另一个动机就是减少锁的开销,因为我们的系统处理器越来越多,而请求从多个处理器放到一个队列中时需要加锁,锁的开销变得越来越大。"plugging"机制能帮得上一些忙,但是不够理想。如果我们能够分配更多队列:每个NUMA节点一个队列,或者一个CPU一个队列,那么把请求放到队列的锁开销就会减少很多。如果硬件支持并行处理多个请求,那么这样做的优势就更大了。如果硬件只支持一次提交一个请求,那么多个per-CPU队列仍然需要合并。如果他们比"plugging"机制批处理的效果更好,那么这样做也是有益处的。假如不能提高批处理的效果,写程序的时候小心点应该也能够保证,至少不会有什么损失。

之前说过,cfq调度器内部已经有多个队列,但是它们跟multi-queue的目的完全不一样,它们把请求与进程和优先级关联起来,而multi-queue的队列是跟硬件密切相关的。multi-queue "request层"维护着两组硬件相关的队列:软件的"staging"队列和硬件的"dispatch"队列。

软件staging队列(struct blk_mq_ctx)是依CPU硬件情况而分配的,每个CPU分配一个,或每个NUMA节点分配一个。当块层的"plugging"机制拔开"塞子"时(blk_mq_flush_plug_list()),request请求在一个spinlock的保护下被添加到队列上,锁竞争应该很少。multi-queue的队列可以选择由某一个multi-queue调度器来管理, 现在有三种multi-queue调度器:bfq, kyber和mq-deadline.

硬件dispatch队列是基于目标硬件块设备进行分配的,所以有可能只有一个,也有可能多达2048个队列 (或与硬件支持的中断源个数一样)。"request层"为每一个硬件队列(或"硬件上下文")分配一个数据结构struct blk_mq_hw_ctx,维护着一个CPU和队列之间的映射表,而队列本身就是为底层驱动而服务的。“request 层”时不时地把硬件队列中的请求传递给底层驱动。接下来,请求就全由驱动处理了,通常情况下,又会很快按照接收的顺序交给硬件。

与single-queue相比有另一个重要区别,multi-queue使用的request结构体都是预分配的。每个request结构体都关联着一个不同tag number,这个tag number会随着请求传递到硬件,再随着请求完成通知传递回来。早点为一个请求分配tag number,在时机到来的时候,request 层可随时向底层发送请求。

single-queue只需要一个request_fn()就可以了,但是multi-queue需要底层驱动提供一个struct blk_mq_ops结构体,包含了多达11个函数。其中,最核心的一个函数是queue_rq(),其它的函数实现了超时,请求完成轮询,请求初始化,等类似操作。

一旦请求就绪,并且调度器不想再把请求保持在队列上来排序或扩展,调度器就会调用queue_rq()函数。single-queue把收集请求的责任交给了驱动层,与之不同,multi-queue却把这个责任交给了"request层"。queue_rq()有个参数代表的是硬件上下文,通常会把请求放在内部FIFO队列上,也有可能会直接处理。queue_rq()可以拒绝一个请求,然后返回BLK_STS_RESOURCE,让请求继续在staging队列上等待。除了BLK_STS_RESOURCE和BLK_STS_OK,其它返回值都被视为IO错误。

多队列调度

multi-queue不是必须要配置一个调度器,如果不指定的话,那么就会用类似单队列中的“noop”调度器。连续的bio会被合并到一个请求,而不连续的bio各成为一个独立的请求。这些请求以FIFO的顺序被放到staging队列中,尽管有多个submission队列,默认的调度器会尝试直接提交新请求,仅仅在收到BLK_STS_RESOURCE返回值时才会使用staging队列。当块设备上的plugging机制拔开“塞子”时,调度器会调用blk_mq_run_hw_queue()或blk_mq_delay_run_hw_queue()把软件队列传递给驱动层处理。

可插拔的multi-queue调度器有22个入口点,其中有两个函数,一个是insert_requests()把一组请求添加到软件staging队列中,另一个是dispatch_request()会选择一个请求然后传递给硬件设备。如果没有实现insert_request()函数,请求就会被简单的插入到列表末尾。如果没有提供dispatch_request()函数,就会从staging队列里取下请求,然后以任意顺序传递到相应的硬件队列。This "collect everything" step is the main point of cross-CPU locking and can hurt performance, so it is best if a device with a single hardware queue accepts all the requests it is given.[最后一句我理解不到那个点,大致理解是说staging队列有多个,硬件队列只有一个的情况,那么多个软件队列上的请求最终要收集到这个硬件队列上来,那么必然要加锁,会比较影响性能]

mq-deadline调度器跟单队列的deadline调度器发挥的功能很相似。它有个insert_request()函数,不会使用多个staging队列,而是把请求放到两个全局的基于时间的队列中 - 一个放读请求,一个放写请求,先尝试把该新请求与已经存在的请求合并,如果合并不了,则把这个新请求放到队列尾部。dispatch_request()函数会从这些队列中返回第一个请求:基于时间的队列,基于请求批大小,以及避免写饥饿的队列。

bfq调度器,即Budget Fair Queueing, 一定程度上是借鉴cfq实现的。内核里有介绍bfq的文档(Documentation/block/bfq-iosched.txt),几年前lwn上有篇讲bfq的文章(https://lwn.net/Articles/601799/),后来又出了一篇文章(https://lwn.net/Articles/709202/),跟进了bfq被吸纳进multi-queue的情况。bfq有一点比较像mq-deadline,没有使用多个per-CPU staging队列。bfq有多个队列,每一个队列由一把自旋锁保护。

与mq-deadline和bfq不一样,kyber IO调度器,在这篇文章中(https://lwn.net/Articles/720675/)有简单介绍,它使用了per-CPU的staging队列,也没有实现自己的insert_request()函数,而用的是默认行为。dispatch_request()函数为每一个硬件上下文都维护着各种内部队列,如果这些队列是空的,它就会从给定的硬件上下文所对应的所有staging队列来收集请求,然后在内部将请求做个分布,如果硬件队列不是空的,它就直接从对应的内部队列里下发请求。关于kyber调度器的这几个方面内核没有相应的注释和文档:策略解释,请求分布,以及处理顺序。

单队列要寿终正寝了?

在块层中,并存两套不同的队列系统,两套调度器和两套不同的驱动接口很明显是不理想的。我们可以期待single-queue的代码很快被移除掉吗?multi-queue到底又有多好呢?

不幸的是,这些问题的回答需要在不同的硬件上做很多测试,而笔者只是个做软件的。从软件的角度来说,很清楚,在支持多队列并行提交请求的硬件上,multi-queue应该能带来很多好处。当用在单队列硬件上时,multi-queue至少应该和单队列旗鼓相当,但是不要期望一夜之间就能达到旗鼓相当的水平,因为任何新事物都不是完美的。

说到不完美,举个例子,最近一个补丁集进了Linux 4.15。这些补丁对mq-deadline调度器做了一些修改来解决redhat在内部存储系统测试中发现的性能问题。假如切换到multi-queue, 存储领域的各家厂商很有可能在测试中发现类似的性能回退。在未来几个月里,这些被发现的问题很有希望得到修复。2017可能不会成为multi-queue-ony Linux的一年,但是这样的一天不会太远。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • cpu
    cpu
    +关注

    关注

    68

    文章

    10417

    浏览量

    206476
  • Linux
    +关注

    关注

    87

    文章

    10981

    浏览量

    206689
  • 队列
    +关注

    关注

    1

    文章

    46

    浏览量

    10848

原文标题:块层介绍 第二篇: request层

文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    《C语言深度剖析》【超经典书籍】

    本帖最后由 zgzzlt 于 2012-8-16 14:23 编辑 《C语言深度剖析》【超经典书籍】
    发表于 08-02 08:59

    C语言深度剖析

    C语言深度剖析——一本关于C语言学习的教程,里面包含C语言编写规范,各种变量指针用法等。以含金量勇敢挑战国内外同类书籍
    发表于 08-14 11:36

    c语言深度剖析

    c语言深度剖析
    发表于 04-02 09:12

    陈正冲《C语言深度剖析

    陈正冲编写的《C语言深度剖析》,挺经典,刚来论坛,多多指教~~
    发表于 08-17 12:06

    【资料分享】C语言深度剖析

    C语言深度剖析
    发表于 10-16 15:16

    C语言深度剖析

    C语言深度剖析
    发表于 08-25 09:08

    AD10画4板内电一般都设计成什么

    各位大侠们,这里两天我画了一4板,由于第一次画4板,有几个问题不清楚,还望赐教!(本人是学生,问的问题低级了,还望各位不要嘲笑。) (1)我的板子是双电源(正负5V),内电(p
    发表于 01-23 06:36

    C语言深度剖析

    C语言深度剖析[完整版].pdfC语言深度剖析[完整版].pdf (919.58 KB )
    发表于 03-19 05:11

    关于四板差分的参考是电源平面的问题

    1.四板叠:S-G-P-S因差分线序交叉需打孔换走BOTTOM,现在第四差分对应的参考平面是电源平面。一直有个疑惑:第四差分对应的
    发表于 08-06 09:45

    浅析串口通讯协议的物理和协议

    什么是串口通讯?串口通讯协议物理的结构是由哪些部分组成的?串口通讯协议的协议的主要标准是什么?
    发表于 10-22 09:30

    AUTOSAR基础软件是由哪些部分组成

    基础软件主要是用于提供基础软件服务,包括标准化的系统功能以及功能接口,并且由一系列的基础服务软件组成,包括系统服务、内存服务、通信服务等。一、基础软件模块按照类型可以分为驱动模块、接口模块、处理模块以及管理器。驱动模块:包含
    发表于 02-17 08:00

    BERT中的嵌入组成以及实现方式介绍

    解决nlp相关任务的深度学习模型一样,BERT将每个输入token(输入文本中的单词)通过token嵌入传递,以便将每个token转换为向量表示。与其他深度学习模型不同,BERT有额外的嵌入
    发表于 11-02 15:14

    C语言深度剖析

    C语言深度剖析
    发表于 05-05 17:40 7次下载

    C语言深度剖析

    C语言深度剖析
    发表于 12-20 22:50 0次下载

    C语言深度剖析.zip

    C语言深度剖析
    发表于 12-30 09:20 5次下载