IO
块IO层
系统中能够随机(不需要按顺序)访问固定大小数据片的硬件设备称作块设备,这些固定大小的数据片就称作为块。最常见的块设备是硬盘,它是以安装文件系统的方式使用的。另一种基本的设备类型是字符设备,字符设备按照字符流的方式被有序访问,像串口和键盘就属于字符设备。
对于这两种设备,它们的区别在于是否可以随机访问数据。块设备的复杂性要远高于字符设备,并且块设备对执行性能要求很高,对块设备的优化会给系统整体的性能带来提升。
块设备
块设备(例如磁盘)中最小的可寻址单元是扇区
,扇区的大小一般为2的整数倍,512字节是最常见的。块设备无法比对扇区还小的单元做寻址操作。
软件(例如文件系统)访问磁盘最小逻辑可寻址单元是块
(虽然物理磁盘的最小单位是扇区,但是内核执行的所有磁盘操作都是基于块
来进行的),对于块单位的要求是:数倍于扇区,2的整数倍,并且要小于页面大小,所以通常块的大小为512、2k、4k。
页面 = 块 x N = ( 扇区 x M ) x N ;
IO缓冲区
缓冲区头
当一个块被调入内存时,它要存储在一个缓冲区中,每个缓冲区都与一个块对应(如上图,一个页面通常只有一个缓冲区),它相当于磁盘块在内存中的表示
。每个缓冲区用结构体buffer_header表示(如下),称作缓冲区头,包含了磁盘块的一些控制信息,定义在<linux/buffer_head.h>文件中。
缓冲区头的目的在于描述磁盘块和物理内存之间的一个映射关系
,但使用缓冲区头作为IO操作单元有两个弊端:
(1)缓冲区头是一个很大且不受控制的结构体(上面的是已经缩减过的),而且缓冲区头对数据的操作既不方便也不清晰,对内核来说,它更倾向操作页面(内存章节中讲到分配内存时按页分配),页面比缓冲区大,效率更高,同时更为简单;
(2)第二个弊端是,缓冲区头仅仅能够描述单个缓冲区,当对大块数据IO操作时,会分解为多个buffer_head结构体进行操作,这样势必会造成浪费和效率低下。在2.5版本之后引入了bio。
bio结构体
目前内核中块IO操作的基本容器由bio结构体表示,它定义在文件<linux/bio.h>中。该结构体描述了以片段列表形式组织正在活动中的块IO操作。一个片段是一小块连续的内存缓冲区。通过用段来描述的缓冲区,单个缓冲区不一定要连续,可以分散在内存中的多个位置上(聚散IO)。
bio结构体的描述,如下:
bio结构体表示正在执行的IO操作,所以该结构体中主要域都是用来管理相关信息的,其中比较关键的域是bi_io_vecs(应该是bio_vecs列表,上图注释有误)、bi_vcnt和bi_idx。如下所示:
每个bio_vec结构都是一个形式为<page, offset, len>的向量,它描述的是特定的段,如下所示:
bio和缓冲区头对比
-
缓冲区头和bio之间存在显著的差异,bio结构体代表的是
IO操作
,它可以包括内存中的一个或多个页;而buffer_head结构体代表的是一个缓冲区,它描述的仅仅是磁盘中的一个块到内存的映射
。 -
因为缓冲区头中关联的是单独页中单独的磁盘块,所以它可能会引起不必要的分割。将一个请求的数据按块为单位分割,后面再重新组合。bio则不需要分割操作,因为它描述的块是把分散的数据页聚集在一起的(不需要连续存储)。
-
bio是轻量级的结构,只需要包含块IO操作所需要的信息就行了,不像缓冲区头,还需要包含缓冲区相关的非必要信息。
值得注意的是,缓冲区头的概念也不可少,因为它还负责描述磁盘块到页面的映射
。bio不包含任何和缓冲区相关的状态信息,它仅仅是一个矢量数组,描述了一个IO操作
的数据片段和相关信息。
总的来说,bio结构体描述当前正在使用的IO操作,buffer_head结构体则包含缓冲区信息。内核需要通过这两个结构体分别保存各自的信息,以保证每种结构体的信息量尽可能少。
注:linux使用bio的目的就是为了将当前IO操作相关的数据块信息从缓冲区头中分离出来
。各自为政,避免混乱。
请求队列
块设备将它们挂起的IO请求保存在请求队列中,该队列由request_queue结构体表示。内核中的高层代码(像文件系统)将请求加入到队列中,只要队列不为空,队列所属的块设备驱动程序就会从对头获取请求,并将其送到对应的块设备去。请求中每一个项使用request表示,因为一个请求可能需要操作多个连续的磁盘块,因此每个项中可以包含多个bio结构,如下图所示:
IO调度程序
如果简单的以内核产生的请求次序直接将请求发向块设备的话,性能肯定让人难以接受。磁盘寻址是整个计算机中最慢的操作之一,所以尽量缩短寻址时间是提高系统性能的关键。为了优化寻址操作,内核在向块设备提交IO请求前,会执行名为合并和排序
的预操作。在内核中,负责提交IO请求的子系统统称为IO调度程序
。
IO调度程序的工作
I/O调度程序管理块设备的请求队列,它决定队列中的请求顺序以及什么时候将请求派发到块设备。IO调度程序是通过合并和排序
这种两种方法来减少磁盘寻址时间的。如下所示:
调度算法
电梯调度
保持磁头以直线方向移动,该算法类似电梯调度:电梯不能随意的从一层跳到另一层,它应该按照一个方向移动,当抵达了同一个方向的最后一层后,再掉头向另一个方向移动。出于这种类似性,IO调度算法也被称为电梯调度。
linus电梯调度是2.4版本内核的默认调度算法,其合并和排序流程如下:
最终期限I/O调度程序
最终期限I/O调度程序是为了解决Linus电梯所带来的饥饿问题
而提出的。饥饿问题主要原因是:出于对减少磁盘寻址时间的考虑,对磁盘的某个区域的频繁操作,导致其他位置上的操作请求得不到运行的机会。
注意,在内核中,写操作和提交它的应用程序是异步的,读操作和提交它的应用程序是同步的。因此读请求饥饿会严重影响系统性能。
最终期限算法使用了不同类型的队列来缓饥饿问题。在最终期限IO调度程序中,每个请求都有一个超时时间(写请求是5s,读请求是500ms)。像Linus电梯算法一样,以磁盘物理位置为次序维护请求队列,这个队列称为排序队列
,排序队列也像linus算法一样执行合并和排序。当一个新的请求来临时,除了会将它放入排序队列中外,还会按照请求的类型将其插入到额外的FIFO队列中
(如下图)。当检测到FIFO队列队头请求超时时,调度程序会将其立即插入到派发队列中。
预测I/O调度程序
最终期限IO调度程序降低了读操作的响应时间,但是它同时也降低了系统的吞吐量
。假设系统处于很繁重的写操作期间,每次提交读请求,IO调度程序都会对读请求迅速作出处理,所以磁盘首先为读操作进行寻址,然后再返回进行写操作寻址,这样的过程重复进行会损害系统的全局吞吐量(试想,如果正在顺序读一个文件,需要提交多次读请求)。
预测IO调度程序就是为了保持良好的读响应的同时也能提供良好的全局吞吐量
。预测IO算法在最终期限算法的基础上增加了预测启发能力。通过在处理完请求后有意识地等待一段时间
(非常短,通常几ms)来避免新寻址和旧寻址位置相邻,但却需要两次寻址地问题。如果不等待的话,调度程序就会返回处理其他请求,当下一次请求到来时需要再次寻址(这次请求很可能和前几ms的请求位置相邻)。即使在等待过程中相邻位置没有请求到来,它给系统带来的损害也是轻微的(相比带来的优势是可以接收的)。
预测IO调度程序是linux中缺省
的I/O调度程序。
完全公正的排队I/O调度程序(CFQ)
完全公正排队I/O调度程序是为了专有工作负荷设计的。它让每个进程都有自己的I/O队列。并通过时间片轮转调度队列,从每个队列中选取固定数量的请求数,然后进行下一轮的调度。
CFQ IO调度程序主要推荐给桌面工作负荷使用。
空操作的I/O调度程序
该调度程序就是最轻量级的调度,只提供请求合并的功能。除了这一操作,空操作IO调度程序什么都不做,只是维护请求队列以近乎FIFO的顺序排列。块设备驱动程序(真正的随机访问设备)便可以从这种队列中摘取请求。
空操作IO调度程序专为随机访问设备而设计(没有寻道负担的块设备,例如闪存卡)。