一个IO的传奇一生(5)

2025-08-07

会将bio放入请求队列中进行调度处理,调度处理的方法有CFQ、Deadline和Noop之分。设置请求队列的目的是考虑了磁盘介质的特性,普通磁盘介质一个最大的问题是随机读写性能很差。为了提高性能,通常的做法是聚合IO,因此在块设备层设置请求队列,对IO进行聚合操作,从而提高读写性能。关于IO scheduler的具体算法分析请见后续文章。

在Linux的通用块层,提供了一个通用的请求队列压栈方法:blk_queue_bio,在老版本的Linux中为__make_request。在初始化一个有queue块设备驱动的时候,最终都会调用blk_init_allocated_queue函数对请求队列进行初始化,初始化的时候会将blk_queue_bio方法注册到q->make_request_fn。在generic_make_request转发bio请求的时候会调用q->make_request_fn,从而可以将bio压入请求队列进行IO调度。一旦bio进入请求队列之后,可以好好的休息一番,直到unplug机制对bio进行进一步处理。

另一类块设备是无queue的。无queue的块设备我们通常可以认为是一种块设备过滤驱动,这类驱动程序可以自己实现请求队列,绝大多数是没有请求队列的,直接对bio进行转发处理。这类驱动程序一个很重要的特征是需要自己实现q->make_request_fn方法。这类驱动的make_request_fn方法通常可以分成如下几个步骤:

1)根据一定规则切分bio,不同的块设备可能存在不同的块边界,因此,需要对请求bio进行边界对齐操作。

2)找到需要转发的底层块设备对象。

3)直接调用generic_make_request函数转发bio至目标设备。

因此,无queue的块设备处理过程很直观。其最重要的作用是转发bio。在Linux中,device_mapper机制就是用来转发bio的一种框架,如果需要开发bio转发处理的驱动程序,可以在device_mapper框架下开发一个target,从而快速实现一个块设备驱动。

通过上述描述,我们知道,IO通过writeback或者DirectIO的方式可以抵达块设备层。到了块设备层之后遇到了两类块设备处理方法。如果遇到无queue块设备类型,bio马上被转发到其他底层设备;如果遇到了有queue块设备类型,bio会被压入请求队列,进行合并处理,等待unplug机制的调度处理。IO曾经在page cache游玩了很长时间,大家都很高兴。所有得请求在page cache受到得待遇是相同的,大家都会比较公平得被调度走,继续下面的旅程。但是,在块设备层情况就变的复杂了,不同IO受到的待遇会有所不同,这就需要看请求队列中的io scheduler具体算法了。因此,IO旅程在块设备这一站,最为重要的核心就是io scheduler。

IO调度器

当IO旅行到调度器的时候,发现自己受到的待遇竟然很不一样,有些IO倚仗着特权很

快就放行了;有些IO迟迟得不到处理,甚至在有些系统中居然饿死!面对这样的现状,IO显然是很不高兴的,凭什么别人就能被很快送到下一个旅程,自己需要在调度器上耗费青春年华?这里是拼爹的时代,人家出身好,人家是读请求,人家就可以很快得到资源。咱们是写请求,出生贫寒,只能等着,但也不会让你饿死。这就是我们常见的deadline策略。在更加糟糕的地方,deadline都没有,拼的是家族血脉关系,相邻的IO可以很快处理掉,其他的等着吧,那就会出现饿死的情况。这就是我们常说的noop策略,其实就是Linus电梯。在文明一点的社会,大家会比较公平,从应用的整体来看,大家会享有相同的IO带宽,但是,从IO的个体来看,公平性还是没有的。这个社会没有绝对的公平,只要保证所有家庭的公平性,那么社会就会比较和谐。当然,我们发现有些家庭(应用)不是特别合群,我们也可以对其进行惩罚,IO带宽的分配就会对其进行缩减。这就是我们常见的CFQ策略。在IO调度器层,可以有很多的策略,不同的系统可以定义不同的策略,目的都是在与更好的聚合IO,并且对不同的应用进行QOS控制。

在Linux系统中,可以注册自己的调度算法,如果不注册自己的调度器,那么可以采用上述提到的三种调度器之一。其中,deadline是在Linus电梯的基础上发展起来的,其对读写请求进行了有区别的调度,还会考虑到IO饥饿的情况。最为传统的调度器不能规避IO饥饿问题。CFQ调度器考虑了应用的公平性,在很多情况下可以得到最佳性能,有关于这三种调度器的设计比较会在下面篇章中详细阐述。

当IO请求通过generic_make_request进行转发时,如果被访问的设备是一个有queue的块设备,那么系统会调用blk_queue_bio函数进行bio的调度合并。blk_queue_bio函数说明如下:

对于blk_queue_bio函数主要做了三件事情:

1)进行请求的后向合并操作 2)进行请求的前向合并操作

3)如果无法合并请求,那么为bio创建一个request,然后进行调度

在bio合并过程中,最为关键的函数是elv_merge。该函数主要工作是判断bio是否可以进行后向合并或者前向合并。对于所有的调度器,后向合并的逻辑都是相同的。在系统中维护了一个request hash表,然后通过bio请求的起始地址进行hash寻址。Hash表的生成原理比较简单,就是将所有request的尾部地址进行分类,分成几大区间,然后通过hash函数可以寻址这几大区间。Hash函数是:

hash_long(ELV_HASH_BLOCK((sec)), elv_hash_shift)

一旦通过hash函数找到所有位于这个区间的request之后,通过遍历的方式匹配到所需要的request。具体该过程的实现函数如下:

采用hash方式维护request,有一点需要注意:当一个request进行合并处理之后,需要对该request在hash表中进行重新定位。这主要是因为request的尾地址发生了变化,有可能会超过一个hash区间的范围。

如果后向合并失败,那么调度器会尝试前向合并。不是所有的调度器支持前向合并,如果调度器支持这种方式,那么需要注册elevator_merge_fn函数实现前向调度功能。例如deadline算法采用了红黑树的方式实现前向调度。如果前向调度无法完成合并。那么调度器认为该合并失败,需要产生一个新的request,并且采用现有bio对其进行初始化,然后加入到request queue中进行调度处理。

当IO利用generic_make_request来到块设备层之后,对其进行处理的重要函数blk_queue_bio主要任务是合并IO。由于不同的调度器有不同的合并方法、IO分类方法,所以,具体调度器的算法会采用函数注册的方式实现。blk_queue_bio仅仅是一个上层函数,最主要完成后向合并、调用调度器方法进行前向合并以及初始化request准备调度。

IO调度器设计考虑

通过前面的分析已经知道IO调度器主要是为了解决临近IO合并的问题。磁介质存储盘最大的性能瓶颈在于寻道。当用户访问一个指定地址时,磁盘首先需要进行寻道操作,找到访问地址所属的区域。这种操作往往是毫秒级别的,相对于性能不断提升的CPU而言,这种性能显然是不可接受的。所以,磁盘最大的问题在于机械操作引入的寻道时间过长,对外表现就是随机读写性能太差了。

为了弥补这种性能弱点,Linux操作系统系统在设计的时候引入了IO调度器,尽最大可能将随机读写转换成大块顺序读写,减少磁盘抖动,降低若干IO操作之间的寻道时间。IO

调度器的目的就在于此,它的初衷是面向存储介质设计的。因此,当一个系统的存储介质发生变化之后,IO调度器就需要改变。例如,针对SSD存储介质,传统意义上的IO调度算法就不再适用了。SSD不存在机械操作,不存在漫长的寻道时间问题,因此,不存在传统磁介质的随机读写问题。但是,SSD存在写放大的问题,一个小写会引入大量的数据读写操作,从而使得IO性能下降。所以,传统磁盘的IO调度器在SSD面前就没有价值了,可以采用最简单的Noop调度器对IO进行后向合并就可以了,而写放大等问题往往都在SSD内部的Firmware解决了。

考虑一下,如果设计的IO调度器完全面向存储介质,那么设计的调度算法只需要考虑IO请求的前向/后向合并就可以了,这样的IO合并减少了磁头的抖动,就像一部电梯一样,一直往一个方向移动。这种算法看上去很完美,但是,在实践中我们会发现有些请求会长时间得不到服务,就像一辆电梯厂时间停在一个楼层,其他楼层的人长时间得不到服务。特别对于一些读操作,往往是同步请求,如果长时间得不到服务,那么会大大影响应用的性能。所以,仅仅简单的考虑存储介质的特性,进行IO的前向/后向合并是不够的,还需要考虑IO的本身属性。

从IO的属性来看,最需要区分的是读写请求,读写请求的优先级是不一样的,大多数写请求可以异步完成,读请求需要同步完成,所以,对于读写请求可以分开处理。虽然分开之后可能会引入更多的磁盘抖动,但是,应用的整体性能还是会提高。

考虑了IO的基本属性之外,是不是就够了呢?其实还是不够的。例如在一个服务器中存在多个应用,这些应用会访问相同的存储,有些应用读请求多,有些应用写请求多,如果仅仅考虑IO的基本属性,那么对于这些不同的应用就会表现出不同的IO性能。有些应用得到较多的IO带宽,性能较好;有些应用得到较少的带宽,性能较差。这显然是不合理的,由于调度算法的策略问题,导致一个系统中,不同应用具有不同的IO性能。为了解决这个问题,需要考虑应用属性。

在现有的Linux系统中,提供了多种IO调度器,这些调度器有些仅仅考虑了存储介质属性(Noop),有些考虑了IO属性(Deadline),还有的考虑了应用属性(cfq)。不同调度器的设计考虑范畴不同,所以复杂度也有很大差别。下面会对Linux中的这几种调度器进行阐述。


一个IO的传奇一生(5).doc 将本文的Word文档下载到电脑 下载失败或者文档不完整,请联系客服人员解决!

下一篇:16春华师《幼儿科学教育》在线作业

相关阅读
本类排行
× 游客快捷下载通道(下载后可以自由复制和排版)

下载本文档需要支付 7

支付方式:

开通VIP包月会员 特价:29元/月

注:下载文档有可能“只有目录或者内容不全”等情况,请下载之前注意辨别,如果您已付费且无法下载或内容有问题,请联系我们协助你处理。
微信:xuecool-com QQ:370150219