向下之旅(十九):块 I/O 层

  系统中能够随机(不需要按顺序)访问固定大小数据片(chunk)的设备被称作块设备,这些数据片就称作块。最常见的块设备是硬盘,除此之外,还有软盘驱动器、CD-ROM驱动器和闪存等许多其他块设备。它们都是以安装文件系统的方式使用的——这也是块设备通常的访问方式。

  另一种基本的设备类型是字符设备。字符设备按照字符流的方式被有序访问,像串口和键盘就都属于字符设备。如果一个硬件设备以字符流的方式被访问的话,那么就应该将它归于字符设备,反过来,如果一个设备是随机(无序)访问的,那么它就属于块设备。随机访问——即能够在访问设备时随意的从一个位置调到另一个位置。例如,当键盘敲击"fox"这个字符串时,需要按照顺序执行。对键盘进行读操作会得到一个字符流,首先是"f",然后是"o",最后是"x",最终是文件的结束(EOF)。当没人敲键盘时,字符流就是空的。硬盘设备的驱动可能要求读取磁盘上任意块的内容,然后又转去读取别的块的内容,而被读取的块在磁盘上位置不一定要连续,所以硬盘可以被随机的访问,即一个块设备。

  解剖一个块设备

  块设备中最小的可寻址单元是扇区。扇区的大小一般是2的整数倍。而最常见的大小是512个字节。扇区的大小是设备的物理属性。扇区是所有块设备的基本单元——块设备无法对比它还小的单元进行寻址和操作。对块的要求是,必须是扇区大小的2的整数倍,并且要小于页面大小。所以通常是512字节、1K或4K。

  扇区——设备最小的寻址单元,块——文件系统的最小寻址单元,有时被称为"文件块"或者"I/O块"。扇区对内核的重要性在于所有设备的I/O操作都必须基于扇区来进行,反过来,块是内核使用的较高层概念,它是比扇区高一层的抽象。

    

  缓冲区和缓冲区头

  当一个块被调入内存时(即在读入后或等待写出时),它要存储在一个缓冲区中。每一个缓冲区对应一个块,它相当于是磁盘块在内存中的表示。块包含一个或多个扇区,但大小不能超过一个页面,所以一个页可以容纳一个或多个内存中的块。由于内核在处理数据时需要一些相关的控制信息(比如块属于哪一个设备,块对应于哪一个缓冲区等),所以每一个缓冲区都有一个对应的描述符。该描述符用buffer_head结构体表示,被称作缓冲区头,结构如下:

  

  bh_state域表示缓冲区状态,合法的标志存放在bt_state_bits枚举中,状态表如下:

  bh_state_bits列表还包含了一个特殊的——BH_PrivateStart,该标志不是可用状态标志,使用它是为了指明可被其他代码使用的起始位。块I/O层不会使用BH_PrivateStart或更高位。那么某个驱动程序希望通过bh_state域存储信息时就可以安全的使用这些位。

  在操作缓冲区头之前,应该先使用get_bh()函数增加缓冲区头的引用计数,确保该缓冲区头不会再被分配出去,当完成对缓冲区头的操作之后,还必须使用put_bh()函数减少引用计数。

  与缓冲区对应的磁盘物理块由b_blocknr域索引,该值是b_bdev域指明的块设备中的逻辑块号。

  与缓冲区对应的内存物理页由b_page域表示,另外,b_data域直接指向相应的块(它位于b_page域所指明的页面中的某个位置上),块的大小由b_size域表示,所以块在内存汇总的起始位置在b_data处,结束位置在(b_data+b_size)处。

  缓冲区头的目的在于描述磁盘块和物理内存缓冲区(在特定页面上的字节序列)之间的映射关系。

  bio结构体

  目前内核中块I/O操作的基本容器由bio结构体表示,该结构体代表了正在现场的(活动的)以片段(segment)链表形式组织的块I/O操作。一个片段是一小块连续的内存缓冲区。这样就无需保证单个缓冲区一定要连续,即使一个缓冲区分散在内存的多个位置上,bio结构体也能对内核保证I/O操作的执行。结构体如下:

  

  使用bio结构体的主要目的是代表正在现场执行的I/O操作,所以该结构体中的主要域都是用来管理相关信息的。关系图如下:

  

  每一个块I/O请求都通过一个bio结构体表示。每个请求包含一个或多个块,这些块存储在bio_vec结构体数组中。这些结构体描述了没个片段在物理页中的实际位置,并且像向量一样被组织在一起。I/O操作的第一个片段由b_io_vec结构体指向,其他的片段在其后依次放置,共有bi_vcnt个片段。

  bi_idx域指向数组中的当前bio_vec片段,块I/O层通过它可以跟踪块I/O操作的完成进度。但该域更重要的作用在于分割bio结构体。

  bi_cnt域记录bio结构体的使用计数,如果该域值减为0,就应该销毁该bio结构体,并释放它占用的内存。通过下面两个函数管理使用计数:

  void bio_get(struct bio *bio)

  void bio_put(struct bio *bio)

  前者增加使用计数,后者减少使用计数。

  缓冲区头和bio结构体对比

  bio结构体代表的是I/O操作,它可以包括内存中的一个或多个页。而buffer_head结构体代表的是一个缓冲区,它描述的仅仅是磁盘中的一个块。因为缓冲区头关联的是单独页中的单独磁盘块,所以它可能会引起不必要的分割,将请求按块为单位划分,只能靠以后才能在重新组合。此外还有如下好处:

  1.bio结构体很容易处理高端内存,以为它处理的是物理页而不是直接指针。

  2.bio结构体既可以代表普通页I/O,同时也可以代表直接I/O(指那些不通过页高速缓存的I/O操作)。

  3.bio结构体便于执行分散—集中(向量化的)块I/O操作,操作中的数据可取自多个物理页面。

  4.bio结构体相比缓冲区头属于轻量级的结构体。因为它只需要包含块I/O操作所需的信息就行了,不用包含与缓冲区本身相关的不必要信息。

  请求队列

  块设备将它们挂起的块I/O请求保存在请求队列中,该队列由reques_queue结构体表示,通过内核中像文件系统这样高层的代码将请求加入到队列中。请求队列只要不为空,队列对应的块设备驱动程序就会从队列头获取请求,然后将其送入对应的块设备上去。请求队列表中的每一项都是一个单独的请求,由request结构体表示,因为一个请求可能要操作多个连续的磁盘块,所以每个请求可以由多个bio结构体组成。注意,虽然磁盘上的块必须连续,但是内存中的这些块不一样连续——每个bio结构体都可以描述多个片段(片段是内存中连续的小区域),而每个请求也可以包含多个bio结构体。

  I/O调度程序

  若简单的以内核产生的请求次序直接的将请求发给设备块的话,性能会大大降低,磁盘寻址是整个计算机中最慢的操作之一。为了优化寻址操作,它会先执行合并与排序的预操作。I/O调度程序和进程调度程序不同,后者的作用是将处理器资源分配给系统中的运行进程,将内存虚拟的提供给用户即多任务和分时操作系统。相反I/O调度程序虚拟块设备给多个磁盘请求,以便降低磁盘寻址时间,确保磁盘性能的最大化。  

  I/O调度程序的工作是管理块设备的请求队列。它决定队列中的请求排列顺序以及在什么时刻派发请求到块设备。这样有利于减少磁盘寻址时间,从而提高全局吞吐量。

  I/O调度程序通过两种方法减少磁盘寻址时间:合并和排序。合并是指将两个或多个请求结合成一个新请求。例如,文件系统提交请求到请求队列——从文件中读取一个数据区(当然,最终所有的操作都是针对扇区和块进行的,而不是文件,此处假定),如果这时队列中已经存在一个请求,它访问的磁盘扇区和当前请求访问的磁盘扇区相邻(比如,同一个文件中早些时候被读取的数据区),那么这两个请求就可以合并为一个对单个和多个相邻磁盘扇区操作的新请求。通过合并,压缩成一个请求,减少系统开销和磁盘寻址次数。排序是将请求通过保持磁盘头以直线方向移动,缩短了所有请求的磁盘寻址时间,该排序算法类似于电梯调度——电梯不能随意的从一层调到另一层,它应该想一个方向移动,当抵达了同一方向上的最后一层后,再掉头向另一方向移动。因此I/O调度程序也成为电梯调度。

  Linus电梯

  上述的I/O调度程序,被称为Linus电梯,后来在2.6内核中被另外两种调度程序取替了。当一个请求加入到队列中时,有可能发生四种操作,它们依次是:

  1.第一,如果队列中已存在一个对相邻磁盘扇区操作的请求,那么新请求将和这个已存在的请求合并成一个请求。

  2.第二,如果队列中存在一个驻留时间过长的请求,那么新请求将被插入到队列尾部,以防止其他旧的请求发生饥饿。

  3.第三,如果队列中以扇区方向为序存在合适的插入位置,那么新的请求将被插入到该位置,保证队列中的请求是以被访问磁盘物理位置为序进行排列的。

  4.第四,如果队列中不存在合适的请求插入位置,请求将被插入到队列尾部。

  最终期限I/O调度程序

  最终期限I/O调度程序是为了解决Linus电梯所带来的饥饿问题而提出的。以降低全局吞吐量为代价,每一个请求都有一个超时时间,默认情况下,读请求的超时时间是500毫秒,写请求的超时时间是5秒。也以磁盘物理位置为次序维护请求队列,这个队列被称为排序队列。与电梯调度程序相似。但是最后期限I/O调度程序同时也会以请求类型为依据将它们插入到额外队列中。

  

  预测I/O调度程序

  预测I/O调度程序的目的在于保持良好的读相应的同时也能提供良好的全局吞吐量。

  完全公正的排队I/O调度程序

  完全公正的排队I/O调度程序(CFQ)是为专有工作负荷设计的,不过,在实际中,也为多种工作负荷提供了良好的性能。与上述的调度程序不同,CFQ I/O调度程序把进入的I/O请求放入特定的队列中,这种队列是根据引起I/O请求的进程组织的。例如,来自foo进程的I/O请求进入foo队列,而来自bar进程的I/O请求进入bar队列。采用时间轮转调度队列,从每个队列中选取请求数(默认值为4,可进行配置),然后进行下一轮调度。

  空操作的I/O调度程序

  最后一种I/O调度程序是空操作I/O调度程序,之所以这样命名是因为它基本上是一个空操作,不做多少事情。不进行排序,也不进行其他形式的预寻道操作,只进行合并操作。

  I/O调度程序的选择

  在2.6内核中四种不同的I/O调度程序。其中的每一种I/O调度程序都可以被启用,并内置在内核中。作为默认,块设备采用预测I/O调度程序。在启动时,可以通过命令行选项 elevator = foo来覆盖默认,这里foo是一个有效而激活的I/O调度程序。如图:

  

  例如,内核命令行选项 elevator = cfq 会启用完全公正的I/O调度程序来给所有的块设备。

 

  参考自:《Linux Kernel Development》.

posted on 2016-03-29 15:56  画家丶  阅读(259)  评论(0编辑  收藏  举报