zoukankan      html  css  js  c++  java
  • Linux14-块I/O层

    Linux第14章块I/O层

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

    另一种基本的设备是字符设备。字符设备按照字符流的方式被有序访问,像串口和键盘就属于字符设备。如果一个硬件设备是以字符流的方式被访问的话,那就应该将它归于字符设备;反过来,如果一个设备是随机(无序的)访问的,那么它就属于块设备。

    对于这两种设备,它们的区别在于是否可以随机访问数据-----就是能否在访问设备时随意地从一个位置跳转到另一个位置。

    内核管理块设备要比管理字符设备细致得多,这是因为字符设备仅仅需要控制一个位置----当前位置,而块设备访问的位置必须能够在介质的不同区间前后移动。所以事实上内核不必提供一个专门的子系统来管理字符设备,但是对块设备的管理却必须要有一个专门的提供服务的子系统。因为块设备的复杂性远远高于字符设备,块设备对执行性能的要求很高;对硬盘每多一份利用都会对整个系统的性能带来提升,其效果要远远比键盘吞吐速度成倍的提高大得多。

    本章讨论内核如何对块设备和块设备的请求进行管理,该部分在内核中称作块I/O层。

    14.1 剖析一个块设备

    块设备中最小的可寻址单元是扇区。扇区大小一般是2的整数倍,而最常见的是512字节。扇区的大小是设备的物理属性,扇区是所有块设备的基本单元----块设备无法对比它还小的单元进行寻址和操作,尽管许多设备能够一次对多个扇区进行操作。

    因为各种软件的用途不同,所以它们都会用到自己的最小逻辑可寻址单元----块。块是文件系统的一种抽象---只能基于块来访问文件系统。虽然物理磁盘寻址是按照扇区级进行的,但是内核执行的所有磁盘操作都是按照块进行的。由于扇区是设备的最小可寻址单元,所以块不能比扇区还小,只能数倍于扇区大小。另外,内核(对有扇区的硬件设备)还要求块大小是2的整数倍,而且不能超过一个页的长度

    扇区---设备的最小寻址单元,有时会称作“硬扇区”或“设备块”‘块----文件系统的最小寻址单元,有时会称作“文件块”或“I/O块”。

    14.2 缓冲区和缓冲区头

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

    struct buffer_head{

      unsigned long  b_state;  //缓冲区状态标志

      struct buffer_head  *b_this_page;  //页面中的缓冲区

      struct page   *b_page;  //存储缓冲区的页面

      sector_t  b_blocknr;  //起始块号

      size_t  b_size;  //映像的大小

      char  *b_data;  //页面内的数据指针

      struct block_device  *b_bdev;  //相关联的块设备

      bh_end_io_t  *b_end_io;  //I/O完成方法

      void   *b_pravate;  //io完成方法

      struct list_head  b_assoc_buffers;  //相关的映射链表

      struct address_space  *b_assoc_map;  //相关的地址空间

      atomic_t  b_count;  //缓冲区使用计数

    };

    b_state域表示缓冲区的状态。合法的标志存放在bh_state_bits枚举中。

    状态标志-----意义

    BH_Uptodate  该缓冲区包含可用数据

    BH_Dirty  该缓冲区是脏的(缓冲区中的内容比磁盘内容新,需要写回磁盘)

    BH_Lock  该缓冲区正在被I/O操作使用,被锁定以防被并发访问

    BH_Req  该缓冲区有I/O请求操作

    BH_Mapped  该缓冲区是映射磁盘块的可用缓冲区

    BH_New  缓冲区是通过get_block()刚刚映射的,尚且不能访问

    BH_Async_Read  该缓冲区正通过end_buffer_async_read()被异步I/O读操作使用

    BH_Async_Write  该缓冲区正通过end_buffer_async_write()被异步I/O写操作使用

    BH_Delay  该缓冲区尚未和磁盘块关联

    BH_Boundary  该缓冲区处于连续块区的边界----下一个块不再连续

    BH_Write_EIO  该缓冲区在写的时候遇到I/O错误

    BH_Ordered  顺序写

    BH_Eopnotsupp  该缓冲区发生“不被支持”错误

    BH_Unwritten  该缓冲区在硬盘上的空间已被申请但是没有实际的数据写出

    BH_Quiet  此缓冲区禁止错误

    bh_state_bits列表还包含了一个特殊标志----BH_PrivateStart,该标志不是可用状态标志,使用它是为了指明可被其它代码使用的起始位。块I/O层不会使用BH_PrivateStart或更高的位。

    b_count域表示缓冲区的使用计数,可通过两个定义在文件linux/buffer_head.h中的内联函数对此域进行增减。get_bh、set_bh,参数为struct buffer_head*bh。

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

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

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

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

    14.3 bio结构体

    目前内核中块I/O操作的基本容器由bio结构体表示,该结构体代表了正在现场的(活动的)以片断(segment)链表形式组织的块I/O操作。一个片段是一小块连续的内存缓冲区。这样的话,就不需要保证单个缓冲区一定要连续。

    所以通过片段来描述缓冲区,即使一个缓冲区分散在内存的多个位置上,bio结构体也能对内核保证I/O操作的执行。像这样的向量I/O就是所谓的聚散I/O。

    struct bio{

      sector_t  bi_sector;  //磁盘上相关的扇区

      struct bio  *bi_next;  //请求链表

      struct block_device  *bi_bdev;  //相关的块设备

      unsigned long  bi_flags;  //状态和命令标志

      unsigned long  bi_rw;  //读还是写

      unsigned short  bi_vcnt;  //bio_vecs偏移的个数

      unsigned short  bi_idx;  //bio_io_vect的当前索引

      unsigned short  bi_phys_segments;  //结合后的片段数目

      unsigned int  bi_size;  //I/O计数  

      unsigned int  bi_seg_front_size;  //第一个可合并的段大小

      unsigned int  bi_seg_back_size;  //最后一个可合并的段大小

      unsigned int  bi_max_vecs;  //bio_vecs数目上限

      unsigned int  bi_comp_cpu;  //结束CPU

      atomic_t  bi_cnt;  //使用计数

      struct bio_vec  *bi_io_vec;  //bio_vecs链表

      bio_end_io_t  *bi_end_io;  //I/O完成方法

      void  *bi_private;  //拥有者的私有方法

      bio_destructor_t  *bi_destructor;  //撤销方法

      struct bio_vec  bi_inline_vecs[0];  //内嵌bio向量

    };

    使用bio结构体的目的主要是代表正在现场执行的I/O操作,所以该结构体中的主要域都是用来管理相关信息的,其中最重要的几个域是bi_io_vecs、bi_vcnt和bi_idx。

    14.3.1 I/O向量

    bi_io_vec域指向一个bio_vec结构体数组,该结构体链表包含了一个特定I/O操作所需要使用到的所有片段。每个bio_vec结构都是一个形式为<page,offset,len>的向量,它描述的是一个特定的片段:片段所在的物理页、块在物理页中的偏移位置、从给定偏移量开始的块长度。整个bio_io_vec结构体数组表示了一个完整的缓冲区。bio_vec结构定义在linux/bio.h文件中:

    struct bio_vec{

      struct page *bv_page;  //指向这个缓冲区所驻留的物理页

      unsigned int bv_len;  //这个缓冲区以字节为单位的大小

      unsigned int bv_offset;  //缓冲区所驻留的页中以字节为单位的偏移量

    };

    在每个给定的块I/O操作中,bi_vcnt域用来描述bi_io_vec所指向的bio_vec数组中的向量数目。当块I/O操作执行完毕后,bi_idx域指向数组的当前索引。

    每一个块I/O请求都通过一个bio结构体表示。每个请求包含一个或多个块,这些块存储在bio_vec结构体数组中。这些结构体描述了每个片段在物理页中的实际位置,并且像向量一样被组织在一起。I/O操作的第一个片段由bi_io_vec指向,其它片段在其后依次防止,共有bi_vcnt个片段。当块I/O层开始执行请求、需要使用各个片段时,bi_idx域会不断更新,从而总指向当前片段。

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

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

    void bio_get(struct bio*bio)  //增加使用计数

    void bio_put(struct bio*bio  //减少使用计数

    最后说明bi_private域,这是一个属于拥有者(也就是创建者)的私有域,只有创建了bio结构的拥有者可以读写该域。

    14.3.2 新老方法对比

    缓冲区头和新的bio结构体之间存在显著差别。bio结构体代表的是I/O操作,它可以包括内存中的一个或多个页了人另一方面,buffer_head结构体代表的是一个缓冲区,它描述的仅仅是磁盘中的一个块。因为缓冲区头关联的是单独页中的单独磁盘块,所以它可能会引起不必要的分隔,将请求按块为单位划分,只能靠以后再重新组合。由于bio结构体是轻量级的,它描述的块可以不需要连续的存储区,并且不需要分隔I/O操作。

    使用bio结构体代替buffer_head结构体还有以下好处:

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

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

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

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

    但是还是需要缓冲区头这个概念,毕竟它还负责描述磁盘块到页面的映射。bio结构体不包含任何和缓冲区相关的状态信息-----它仅仅是一个矢量数组,描述一个或多个单独块I/O操作的数据片段和相关信息。在当前设置中,当bio结构体描述当前正在使用的I/O操作时,buffer_head结构体仍然需要包含缓冲区信息。内核通过这两种结构分别保存各自的信息,可以保证每种结构所含的信息量尽可能少。

    14.4 请求队列

    块设备将它们挂起的块I/O请求保存在请求队列中,该队列由reques_queue结构体表示,定义在文件linux/blkdev.h中,包含一个双向请求链表以及相关控制信息。通过内核中像文件系统这样的高层的代码将请求加入到队列中。请求队列只要不为空,队列对应的块设备驱动程序就会从队列头获取请求,然后将其送入对应的块设备上去。请求队列表中的每一项都是一个单独的请求,由request结构体表示。

    队列中的请求由结构体request表示,因为一个请求可能要操作多个连续的磁盘块,所以每个请求可以由多个bio结构体组成。注意,虽然磁盘上的块必须连续,但是在内存中这些块并不一定要连续----每个bio结构体都可以描述多个片段(片段是内存中的连续的小区域),而每个请求也可以包含多个bio结构体。

    14.5 I/O调度程序

     如果简单地以内核产生的请求的次序直接将请求发向块设备的话,性能会很差。磁盘寻址是整个计算机中最慢的操作之一,每次寻址(定位磁盘磁头到特定块上的某个位置)需要花费不少时间,所以尽量缩短寻址时间无疑提高系统性能。

    为了优化寻址操作,内核既不会简单地按请求接受次序,也不会立即将其提交给磁盘。相反,它会在提交前,先执行名为合并排序的预操作,这种预操作可以极大地提高系统的整体性能。在内核中负责提交I/O请求的子系统称为I/O调度程序。

    I/O调度程序将磁盘I/O资源分配给系统中所有挂起的块I/O请求。这种资源分配是通过将请求队列中挂起的请求合并和排序来完成的。

    进程调度程序的作用是将处理器资源分配给系统中的运行进程。I/O调度程序虚拟块设备给多个磁盘请求,以便降低磁盘寻址时间,确保磁盘性能地最优化。

    14.5.1 I/O调度程序的工作

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

    I/O调度程序通过两种方法减少磁盘寻址时间:合并与排序。合并指将两个或多个请求结合成一个请求。通过合并请求,I/O调度程序将多次请求的开销压缩成一次请求的开销。更重要的是,请求合并后只需要传递给磁盘一条寻址命令,就可以访问到请求合并前必须多次寻址才能访问完的磁盘区域了,因此合并请求显然能减少系统开销和磁盘寻址次数。

    如果存在一个请求,它要操作的磁盘扇区位置与当前请求比较接近,那么是不是该让这两个请求在请求队列上也相邻呢?事实上,I/O调度程序的确是这样处理上述情况的,整个请求队列将按扇区增长方向有序排列。使所有请求按硬盘上扇区的排列顺序有序排列(尽可能的)的目的不仅是为了缩短单独一次请求的寻址时间,更重要的优化在于,通过保持磁盘头以直线方向移动,缩短了所有请求的磁盘寻址时间。该排序算法类似于电梯调度(A-->B.....A--->B),所以也称为电梯调度。

    以下是Linux中实际使用的I/O调度程序。

    14.5.2 Linus电梯

    Linus电梯能执行合并与排序预处理。当有新的请求加入队列时,它首先会检查其它每一个挂起的请求是否可以和新请求合并(合并尝试)。Linus电梯I/O调度程序可以执行向前和向后合并,合并类型描述的是请求向前面还是向后面,这一点和已有请求相连。

    如果合并尝试失败,那么就需要寻找可能的插入点(新请求在队列中的位置必须符合请求以扇区方向有序排列序的原则)。如果找到,新请求将被插入到该点;如果没有合适的位置,那么新请求就被加入到队列尾部。另外,如果发现队列中有驻留时间过长的请求,那么新请求也将被加入到队列尾部,即使插入后还要排序。这样做是为了避免由于访问相近磁盘位置的请求太多,从而造成访问磁盘其他位置的请求难以得到执行机会这一问题。

    总而言之,当一个请求加入到队列中时,有可能发生四种操作,它们依次是:

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

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

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

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

    14.5.3 最终期限I/O调度程序

    最终期限I/O调度程序是为了解决Linus电梯所带来的饥饿问题而提出的。处于减少磁盘寻址时间的考虑,对某个磁盘区域上的繁重操作,无疑会使得磁盘其它位置上的操作请求永远得不到机会运行。

    更糟糕的是,普通的请求饥饿还会带来写-饥饿-读(writes-starving-reads)这种特殊问题。写操作通常是在内核有空时才将请求提交给磁盘的,写操作完全和提交它的应用程序异步执行;读操作则恰恰相反,通常当应用程序提交一个读请求时,应用程序会发生堵塞直到读请求被满足,也就是说,读操作是和提交它的应用程序同步执行的。

    读请求反应时间对性能影响很大。应用程序一般等待读取完成后才能运行其它程序。

    2.6版本内核引入了最后期限I/O调度程序来减少请求饥饿现像。

    减少请求饥饿必须以降低全局吞吐量为代价。Linus电梯调度程序虽然也做了这样的折中,但却忽略了请求的驻留时间,会发生同样不可取的饥饿问题。

    最后期限I/O调度程序中,每个请求都有一个超时时间,默认情况下,读请求的超时时间是500ms,写请求的超时时间是5s。最后期限I/O调度请求类似于Linus电梯,也以磁盘物理位置为次序维护请求队列,这个队列称为排序队列。当一个新请求递交给排序队列时,最后期限I/O调度程序在执行合并和插入请求时类似于Linus电梯,但是最后期限I/O调度程序同时也会以请求类型为依据将它们插入到额外队列中。读请求按次序被插入到特定的读FIFO队列中,写请求被插入到特定的写FIFO队列中。

    对于普通操作来说,最后期限I/O调度程序将请求从排序队列的头部取下,再推入到派发队列中,派发队列然后将请求提交给磁盘驱动,从而保证了最小化的请求寻址。

    如果在写FIFO队列头,或读FIFO队列头的请求超时(当前时间超过了请求指定的超时时间),那么最后期限I/O调度程序便从FIFO队列中提取请求进行服务。

    读请求FIFO队列  |

    写请求FIFO队列  |     ------>   派发队列------> 磁盘

    排序队列              |

    最后期限I/O调度算法并不能严格保证请求的响应时间,但是通常情况下,可以在请求超时或超时前提交和执行,以防止请求饥饿现象的发生。

    由于读请求给定的超时时间要比写请求短很多,所以最后期限I/O调度器也确保了写请求不会因为堵塞读请求而使读请求发生饥饿。

    14.5.4 预测I/O调度程序

    虽然最后期限I/O调度程序为降低读操作响应时间做了许多工作,但是它同时也降低了系统吞吐量。

    假设一个系统处于很繁重的写操作期间,每次提交读请求,I/O调度程序都会迅速处理读请求,所以磁盘首先为读操作进行寻址,执行读操作,然后返回再寻址进行写操作,并对每个读请求都重复这个过程。损害了系统全局吞吐量。

    预测(anticipatory)I/O调度程序的目标就是在保持良好的读响应的同时也能提供良好的全局吞吐量。

    预测I/O调度的基础是最后期限I/O调度,所以有很多相同之处。预测I/O调度也实现了三个队列(加上一个派发队列),并为每个请求设置了超时时间,这点与最后期限I/O调度程序一样。预测I/O调度程序最主要的改进是它增加了预测启发能力。

    预测I/O调度程序试图减少在进行读I/O操作期间,处理新到的读请求所带来的寻址数量。和最后期限I/O调度程序一样,读请求通常会在超时前得到处理,但是预测I/O调度程序的不同之处在于,请求提交后并不直接返回处理其它请求,而是会有意空闲片刻(实际空闲时间可以设置,默认为6ms)。这个ms,对应用程序来说是个提交其它读请求的好机会----任何对相邻磁盘位置操作的请求都会立刻得到处理。在等待时间结束后,预测I/O调度程序重新返回原来的位置,继续执行以前剩下的请求。

    如果等待可以减少读请求所带来的向后再向前(back-and-forth)寻址操作,那么完全值得花一些时间来等待更多的请求;如果一个相邻的I/O请求在等待期到来,那么I/O调度程序可以节省两次寻址操作。如果存在很多的访问同样区域的读请求到来,那么片刻等待将无疑会避免大量的寻址操作。

    预测I/O调度程序是Linux系统缺省的I/O调度程序,对大多数工作负荷来说都执行良好,对服务器也是理想的。

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

    完全公正的排队I/O调度程序(CFQ)是为专有工作负荷设计的。

    CFQ I/O调度程序把进入的I/O请求放入特定的队列中,这种队列是根据引起I/O请求的进程组织的。例如,来自foo进程的I/O请求进入foo队列,而来自bar进程的I/O请求进入bar队列。在每个队列中,刚进入的请求与相邻请求合并在一起,并进行插入分类。队列由此按扇区方式分类,这与其它I/O调度程序队列类似。 CFQ I/O调度程序的差异在于每一个提交I/O的进程都有自己的队列。

    CFQ I/O调度程序以时间片轮转调度队列,从每个队列中选取请求数(默认值为4,可以进行配置),然后进行下一轮调度。这就在进程级提供了公平,确保每个进程接受公平的磁盘带宽片断。

    预定的工作负荷是多媒体,主要推荐给桌面工作负荷使用。

    14.5.6 空操作的I/O调度程序

    空操作(noop)I/O调度程序,不做多少事情。空操作I/O调度程序不进行排序,或者也不进行什么其它形式的预寻址操作。

    不过空操作I/O调度程序需要执行合并。

    空操作I/O调度用于块设备中的随机访问设备,比如内闪卡。如果RAM的块设备没有寻址的负担,也就没必要对进入的请求进行排序。

  • 相关阅读:
    绑定方法、非绑定方法与静态方法
    封装、隐藏和property装饰器
    自己动手写中文分词解析器完整教程,并对出现的问题进行探讨和解决(附完整c#代码和相关dll文件、txt文件下载)
    SASS -- 基本认识
    网易新闻页面信息抓取 -- htmlagilitypack搭配scrapysharp
    爬虫技术(四)-- 简单爬虫抓取示例(附c#代码)
    c# -- 读取文件夹中的所有文件(备忘)
    爬虫技术(五)-- 模拟简单浏览器(附c#代码)
    爬虫技术(六)-- 使用HtmlAgilityPack获取页面链接(附c#代码及插件下载)
    关于引用mshtml的问题
  • 原文地址:https://www.cnblogs.com/cjj-ggboy/p/12368238.html
Copyright © 2011-2022 走看看