标签: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中定义。
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_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 */ };
好了,我们明白了块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调度程序。
最简单的 I/O调度算法。该算法仅适当合并用户请求,并不排序请求:新的请求通常被插在调度队列的开头或末尾,下一个要处理的请求总是队列中的第一个请求。这种算法是为不需要寻道的块设备设计的,如SSD。
"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,严重影响读性能。
标签:linux内核 内存管理 块设备 io子系统 磁盘读写
原文地址:http://blog.csdn.net/hustyangju/article/details/40507647