当一个新的IO请求加入到请求队列时,操作系统的通用块设备层会调用IO调度器来决定这个请求在队列中的确切位置。调度器会让请求队列中的请求按照磁盘上的扇区来进行排序。如果从这个请求列表中按照顺序取出请求进行处理,那么磁道寻址的数量会大幅减少,因为磁头会线性地从内道移向外道,而不是随机地在磁道间来回跳来跳去。这种方法和现实生活中的电梯很相似,电梯上下移动来让不同楼层的乘客进去或者出来。首先电梯只会向一个方向移动;直到该方向最后一个楼层停靠完毕,才会改变往另一方向移动。也正因如此,IO调度器也被成为电梯。
在高负载时,严格按照扇区编号进行处理的调度算法工作得并不好。由于数据传输的完成时间依赖于数据在磁盘上的物理位置,因此如果一个设备驱动器总是处理队列的头部,也就是编号较小的扇区,随着新的IO请求不断进来,而且伴随着编号较小的扇区,那么那些位于队列尾部的编号较大的扇区就很难得到响应和处理而出现饿死现象。因此IO调度算法是非常复杂的。
当前,Linux 2.6提供4种不同的IO调度器类型,也就是电梯,分别叫做:Anticipatory,Deadline,CFQ(CompleteFairnessQueueing)和Noop(NoOperation)。对于大多数块设备来讲,被Linux核心缺省使用的电梯在启动时由核心参数elevator=<name>来决定,name可以是deadline,cfq和noop。如果没有指定核心参数,那么核心会使用anticipatoryIO调度器。设备驱动器也可以替换系统缺省的电梯。设备驱动器可以指定自己的IO调度算法,虽然很少会这么做。此外,系统管理员也可以在线改变某一特定块设备的IO调度器。例如,如果想要改变第一个IDE通道的主磁盘的IO调度器,系统管理员可以把elevator的名字写入到sysfs这个特殊文件系统的/sys/block/hda/queue/scheduler文件中。
我们现在来看一下4种不同的IO调度算法,从最简单的到最复杂的。设计一个IO调度器非常类似设计一个CPU调度器。调度器中采用的常量和探索方法都是基于大量的基准测试而决定的。
一般来讲,所有的算法都会使用一个dispatchqueue,这个队列中包含了所有应该由设备驱动器处理的已经排好序的IO请求。下一个需要处理的请求永远都是dispatch queue中的第一个请求。另外,几乎所有的算法也会使用额外的队列来对IO请求进行分类和排序。它们都允许设备驱动器在已经存在的请求中加入新的块IO,并且在满足条件下将相邻的IO请求进行合并操作。
Noop电梯
这是最简单的IO调度算法。没有排序队列,新的IO请求总是被加入到dispatch queue的头部或者尾部,并且下一个处理的请求永远都是队列的第一个请求。
CFQ电梯
CFQ的全称为CompleteFairnessQueueing,目标是保证在所有提交IO请求的操作系统进程间进行IO带宽的公平分配。为了达到这个目标,这个电梯使用了大量的排序队列,缺省是64个队列,用于存储来自不同进程的IO请求。每当一个请求到达elevator时,操作系统核心就会调用HASH函数将当前进程的线程组标识转换为一个队列索引,紧接着elevator便会将这个新的请求插入到该队列的尾部。因此来自相同进程的请求总是会被插入相同的队列。
为了重新填满dispatchqueue,这个elevator会使用round-robin的方法来扫描每一个排序队列,选择第一个非空的队列并将一批这个队列中的请求插入到dispatchqueue的尾部。
Deadline电梯
除了dispatchqueue,deadlineelevator还使用了4个队列。其中2个是排序的队列,一个用于存放读请求,另外一个用于存放写请求。队列中按照扇区编号进行排序。另外2个队列称为deadline queue,也是分别存放读和写请求,但根据请求的deadline进行排序。这些队列用于避免请求饿死的情况出现。因为elevator倾向于处理和上一个处理的请求物理上接近的请求,这样会导致某些请求长期得不到处理而饿死。缺省情况下,对于读请求的过期时间是500ms,而写请求是5s。读请求比写请求具有更高处理优先级的原因是通常它们会阻塞进程,进程需要等待读取数据才能继续处理。Deadline保证调度器能够照顾到已经等待很长时间的请求,尽管这些请求在排序上不占优势。
当elevator需要重新装满dispatchqueue时,首先需要决定下一个请求的方向,到底是读还是写。如果读和写的请求同时存在,elevator会优先选择读,除非写已经被放弃很多次(为了防止写请求饿死)。这种策略是基于和上面同样的原因,读优先于写。
下一步,elevator会检查对应的deadline队列。由于deadline队列中的请求已经按照deadline排序,所以如果该队列的第一个请求已经超过预设的过期时间,那么elevator会将该请求移至dispatch queue的尾部。同时elevator也会扫描对应的排序队列,将排序队列中从对应过期请求的下一个请求开始,把一个批次的请求插入到dispatchqueue。如果这些请求在磁盘上都是物理相邻的,这个批次会较长;反之较短。最后,如果在deadline队列中没有请求过期,那么elevator只会从排序队列中取出紧接着上一次取出的请求的一次批次的请求并且放入dispatchqueue。当游标到达排序队列的最后,搜索会再次从头部开始。
Anticipatory电梯
Anticipatory电梯是Linux提供的最复杂的IO调度算法。简单来说,它是deadline电梯的进化版本,它借用了deadline的基本概念,具备两个deadline队列和两个排序队列。IO调度器在读和写的排序队列交替扫描,并且倾向于读队列。而扫描也是顺序进行,除非有过期请求。对于读请求缺省的过期时间是125ms,而写是250ms。但不同的是,elevator会有一些额外的探索模式:
l 有时elevator会在排序队列中选择一个方向相反的请求,从而导致磁头向相反方向搜索。这种情况发生在方向相反的请求和当前位置的寻址距离小于同方向的请求和当前位置的距离的一半时。拿电梯来举例,比如电梯当前到达的楼层是5楼并且是上行,电梯下一个需要停下来的楼层是8,这时4楼有人按电梯并且上行,那么电梯会先下行到4楼,接上乘客后再往上。
l Elevator会收集系统中每个进程IO负载类型的统计信息。在从排序队列中取出了进程P的读请求后,elevator会检查排序队列中的下一个请求是否来来自进程P。如果是,则下一个请求也立即取出并插入dispatchqueue中。否则,elevator会根据收集到的进程P的IO统计信息来决定进程P是否非常有可能很快发出下一个读请求,如果是这样elevator会停滞一个很短的时间等待,缺省是7ms。因此,elevator会期望来自于进程P的读请求是非常接近于刚刚dispatch的读请求。但这样会很大程度依赖于统计信息的准确性和IO负载的规律性。