linux块设备的IO调度算法和回写机制

**************************************************************************************

參考:

《Linux内核设计与实现》 

http://laokaddk.blog.51cto.com/368606/699028/

http://www.cnblogs.com/zhenjing/archive/2012/06/20/linux_writeback.html

**************************************************************************************

1 Linux块IO请求

块设备中最小的可寻址单元是扇区。扇区的大小通常是2的整数倍。最常见的大小是512个字节。扇区的大小是设备的物理属性,扇区是全部块设备的基本单元,块设备无法对照它还小的单元进行寻址和操作,只是很多块设备可以一次就传输多个扇区。从软件角度来讲。最小的逻辑可寻址单元却是块,块是文件系统的一种抽象-----仅仅能基于块来訪问文件系统。尽管物理磁盘寻址是依照扇区级进行的,可是内核运行的全部磁盘操作都是依照块进行的。

前边已经说过。扇区是设备的最小可寻址单元。所以块不能比扇区还小,仅仅能数倍于扇区大小。

另外内核还要求块大小是2的整数倍,=并且不能超过一个页的长度,所以大小的终于要求是,必须是扇区大小的2的整数倍。并且要小于页面大小。所以通常块大小是512字节。1k或4k。

当一个块被调入内存时,它要存储在一个缓冲区中,每一个缓冲区与一个块相应,它相当于是磁盘块在内存中的表示。

另外,因为内核在处理数据时须要一些相关的控制信息,所以每一个缓冲区都有一个叫做buffer_head的描写叙述符来表示,被称为缓冲区头,在linux/buffer_head.h中定义。


这个结构体在内核中扮演一个描写叙述符的角色。说明从缓冲区到块的映射关系。

使用缓冲区头作为I/O操作有它的弊端。这里不细说,你明确就好。我们仅仅需知道如今的内核採用了一种新型。灵活并且轻量级的容器---bio结构体。
      bio结构体定义在linux/bio.h中,该结构体代表了正在现场的(活动)以片断(segment)链表形式组织的块I/O操作。一个片断是一小块连续的内存缓冲区。这种话,就不须要保证单个缓冲区一定要连续,全部通过片断来描写叙述缓冲区,即使一个缓冲区分散在内存的多个位置上,bio结构体也能保证I/O操作的运行。以下给出bio结构体和各个域的描写叙述,例如以下:

struct bio {
	        sector_t             bi_sector;         /* associated sector on disk */
	        struct bio           *bi_next;          /* list of requests */
	        struct block_device  *bi_bdev;          /* associated block device */
	        unsigned long        bi_flags;          /* status and command flags */
	        unsigned long        bi_rw;             /* read or write? */
	        unsigned short       bi_vcnt;           /* number of bio_vecs off */
	        unsigned short       bi_idx;            /* current index in bi_io_vec */
	        unsigned short       bi_phys_segments;  /* number of segments after coalescing */
	        unsigned short       bi_hw_segments;    /* number of segments after remapping */
	        unsigned int         bi_size;           /* I/O count */
	        unsigned int         bi_hw_front_size;  /* size of the first mergeable segment */
	        unsigned int         bi_hw_back_size;   /* size of the last mergeable segment */
                unsigned int         bi_max_vecs;       /* maximum bio_vecs possible */
                struct bio_vec       *bi_io_vec;        /* bio_vec list */
	        bio_end_io_t         *bi_end_io;        /* I/O completion method */
	        atomic_t             bi_cnt;            /* usage counter */
	        void                 *bi_private;       /* owner-private method */
	        bio_destructor_t     *bi_destructor;    /* destructor method */
	};

      使用bio结构体的目的主要是代表正在现场运行的I/O操作,全部该结构体中的主要域都是用来管理相关信息的。当中最重要的几个域是bi_io_vecs,bi_vcnt和bi_idx.它们之间的关系例如以下图所看到的:

      
      我在前边已经给出了struct bio的结构体。以下给出struct bio_vec的描写叙述:

struct bio_vec {
	   struct page     *bv_page;   /* pointer to the physical page on which this buffer resides */ 
	   unsigned int    bv_len;     /* the length in bytes of this buffer */ 
	   unsigned int    bv_offset;   /* the byte offset within the page where the buffer resides */ 
	};


      以下来分析以上上面的那个图,我们说:每一个块I/O请求都通过一个bio结构体表示。

每一个请求包括一个或多个块。这些块存储在bio_vec结构体数组中,这些结构体描写叙述了每一个片断在物理页中的实际位置,而且像向量一样地组织在一起,IO操作的第一个片断由b_io_vec结构体所指向,其它的片断在其后依次放置,共同拥有bi_vcnt个片断。当块IO開始运行请求,须要使用各个片段时,bi_idx域会不断更新,从而总指向当前片断。bi_idx域指向数组中的当前bio_vec片段,块IO层通过它跟踪块IO操作的完毕进度。

但该域更关键的数据是切割bio结构体。bi_cnt域记录bio结构体的使用计数,假设为0,则应该销毁该bio结构体,并释放它占用的内存。

通过以下两个函数管理使用计数:

void bio_get(struct bio *bio);
void bio_put(struct bio *bio);
      最后一个域是bi_private域。这是一个属于拥有者的私有域。谁创建了bio结构,谁就能够读写该域。
      块设备将它们挂起的块IO请求保存在请求队列中。该队列有request_queue结构体体表示,定义在文件linux/blkdev.h中,包括一个双向请求链表以及相关控制信息。通过内核中想文件系统这样高层的代码将请求增加到队列中。请求队列仅仅要不为空,队列相应的块设备驱动程序就会从队列头获取请求,然后将其送入相应的块设备上去请求队列表中的每一项都是一个单独的请求,有reques结构体体表示。队列中的请求由结构体request表示,定义在文件linux/blkdev.h表示。由于一个请求可能要操作多个连续的磁盘块,全部每一个请求可有由多个bio结构体组成。注意,尽管磁盘上的块必须连续,可是在内存中的这些块并不一定要连续----每一个bio结构都能够描写叙述多个片段,而每一个请求也能够包括多个bio结构体。


      好了。我们明确了块IO请求,以下的就是IO调度了。每次寻址的操作就是定位磁盘磁头到特定块上的某个位置,为了优化寻址操作,内核既不会简单地按请求接收次序。也不会马上将其提交给磁盘,相反。它会在提交前,先运行名为合并与排序的预操作。这样的预操作能够极大地提高系统的总体性能。

2 Linux内核块设备IO子系统

Linux IO调度程序是块设备I/O子系统的主要组件,它介于通用块层和块设备驱动程序之间。例如以下图所看到的。当Linux内核组件要读写数据时,并不是一有请求便马上运行,而是将请求放入请求(输入)队列,并推迟运行。为什么如此设计?原因在于Linux须要应对的最核心的块设备是磁盘。

磁盘的寻道时间严重制约磁盘性能。若想提高磁盘IO性能必须想尽办法降低磁盘寻道次数。

块设备I/O子系统最核心的任务也就是提高块设备的总体性能,为此Linux实现了四种IO调度算法,算法基本思想就是通过合并和排序IO请求队列中的请求大大减少所需的磁盘寻道时间,从而提供总体IO性能。

2.6内核实现了四种IO调度算法。分别为预期(Anticipatory)算法、最后期限(Deadline)算法、全然公平对列(CFQ)算法以及NOOP算法(No Operation)。用户可在内核引导时指定一种I/O调度算法,也可在执行时通过 sysfs 文件系统/sys/block/sda/queue/scheduler改变块设备的I/O调度算法(cat可查看当前使用IO调度算法)。默认的IO调度程序是"预測"IO调度程序。

“Noop”算法

最简单的 I/O调度算法。

该算法仅适当合并用户请求。并不排序请求:新的请求通常被插在调度队列的开头或末尾。下一个要处理的请求总是队列中的第一个请求。这样的算法是为不须要寻道的块设备设计的,如SSD。

“CFQ”算法

"CFQ(全然公平队列)”算法的主要目标是在触发I/O请求的全部进程中确保磁盘I/O带宽的公平分配。为了达到这个目标,算法使用很多个排序队列——缺省为64——它们存放了不同进程发出的请求。当算法处理一个请求时。内核调用一个散列函数将当前进程的线程组标识符(PID);然后,算法将一个新的请求插人该队列的末尾。因此,同一个进程发出的请求通常被插入同样的队列中。

算法本质上採用轮询方式扫描I/O输入队列。选择第一个非空队列。依次调度不同队列中特定个数(公平)的请求,然后将这些请求移动到调度队列的末尾

“最后期限”算法

除了调度队列外,“最后期限”算法还使用了四个队列。当中的两个排序队列分别包括读请求和写请求。当中的请求是依据起始扇区号排序的。另外两个最后期限队列包括同样的读和写请求,但这是依据它们的“最后期限”排序的。引人这些队列是为了避免请求饿死。由于电梯策略(以前的调度算法)优先处理与上一个所处理的请求近期的请求,因而就会对某个请求忽略非常长一段时间。这时就会发生这样的情况。

请求的最后期限本质上就是一个超时定时器。当请求被传给电梯算法时開始计时。缺省情况下,读请求的超时时间是500ms,写请求的超时时间是5s——读请求优先于写请求,由于读请求通常堵塞发出请求的进程。最后期限保证了调度程序照应等待非常长一段时间的那个请求,即使它位于排序队列的末尾。


当算法要补充调度队列时,首先确定下一个请求的数据方向。

假设同一时候要调度读和写两个请求,算法会选择“读”方向,除非该“写”方向已经被放弃非常多次了(为了避免写请求饿死)。

接下来,算法检查与被选择方向相关的最后期限队列:假设队列中的第一个请求的最后期限已用完,那么算法将该请求移到调度队列的末尾。同一时候,它也会移动该过期的请求后面的一组来自排序队列的同样扇区号的请求。假设将要移动的请求在磁盘上物理相邻,那么这一批队列的长度会非常长,否则就非常短。

最后。假设没有请求超时,算法对来自于排序队列的最后一个请求连带之后的一组同样扇区的请求进行调度。当指针到达排序队列的末尾时,搜索又从头開始(“单方向算法”)。

“预期”算法

“预期”算法是Linux提供的最复杂的一种1/O调度算法。

基本上,它是“最后期限”算法的一个演变,借用了“最后期限”算法的基本机制:两个最后期限队列和两个排序队列。I/O调度程序在读和写请求之间交互扫描排序队列,只是更倾向于读请求。

扫描基本上是连续的,除非有某个请求超时。读请求的缺省超时时间是125ms,写请求的缺省超时时间是250ms。

可是,该算法还遵循一些附加的启示式准则:

有些情况下,算法可能在排序队列当前位置之后选择一个请求,从而强制磁头从后搜索。

这样的情况通常发生在这个请求之后的搜索距离小于在排序队列当前位置之后对该请求搜索距离的一半时。

算法统计系统中每一个进程触发的I/O操作的种类。当刚刚调度了由某个进程p发出的一个读请求之后。算法立即检查排序队列中的下一个请求是否来自同一个进程p。

假设是,立即调度下一个请求。否则。查看关于该进程p的统计信息:假设确定进程p可能非常快发出还有一个读请求。那么就延迟一小段时间(缺省大约为7ms)。

因此,算法预測进程p发出的读请求与刚被调度的请求在磁盘上可能是“近邻”。

3 Linux 回写机制

无论怎样优化块设备调度算法。也不可能解决磁盘IO和CPU速度严重不匹配的问题,为此Linux引入了页快速缓存。页快速缓存最開始是为内存管理而设计的,在2.6内核中,各种基于页的数据管理都纳入页快速缓存。因此块设备的IO缓冲区也属于页快速缓存。这些和使用者无关。是内核开发人员须要关心的。对于开发人员,须要知道的是:全部文件的IO操作都是“读写缓存”。

对于读操作,仅仅有当数据不在缓存时才须要IO操作。

对于写操作,一定须要IO操作,但内核把数据写到快速缓存后write系统调用立刻返回,内核採用特定的写进程统一回写dirty的缓存页。

即内核对读写是分别对待的:“同步读,异步写”。

Linux的写回由特定进程依照特定算法来进行回写。在2.6.32之前的内核。pdflush后台回写例程负责完毕这个工作。啥时回写呢? 以下两种情况下。脏页会被写会到磁盘:

1.在空暇内存低于一个特定的阈值时,内核必须将脏页写回磁盘,以便释放内存。


2.当脏页在内存中驻留超过一定的阈值时,内核必须将超时的脏页写会磁盘。以确保脏页不会无限期地驻留在内存中。

回写開始后,pdflush会持续写数据。直到满足下面两个条件:

1.已经有指定的最小数目的页被写回到磁盘。
2.空暇内存页已经回升。超过了阈值dirty_background_ration。

系统管理员能够在/proc/sys/vm中设置回写相关的參数,也能够通过sysctl系统调用来设置它们。下表给出了能够设置的量:

回写机制看上去非常完美。在实际工作中却未必。

一个问题是:pdflush线程数据是可变的(2-8),但面对的是全部块设备的数据。当某个块设备非常慢。必定堵塞其它块设备的回写。

为此2.6.32引入新的pdflush线程模型,每一个块设备拥有独立的pdflush线程,设备之间的回写不再互相干扰。

回写缺陷

看上去如此完美的回写机制,实际中有2个缺陷:1) 回写不及时引发丢数据(sync|fsync);2) 回写期间读IO性能极差,尤其是大数据量时。

Linux:2.6.16 
内存:32G
測试过程:限速情况下持续追加写入磁盘,速度20-10MB/s, 
实測曲线:

结论:pdflush回写期间极其消耗磁盘IO,严重影响读性能。



原文地址:https://www.cnblogs.com/jhcelue/p/6926061.html