Linux内核的文件预读2,Linux技术文章,Linux系列教程,Linux

来源:百度文库 编辑:神马文学网 时间:2024/04/28 08:09:02
Linux内核的文件预读
www.firnow.com    时间 : 2008-09-19  作者:佚名   编辑:本站 点击:  202 [ 评论 ]
-
-
 
图6 重试读(retried reads)Linux 2.6.22无法理解这种情况,于是把它误判为随机读。这里的问题在于“读请求”并不代表读取操作实实在在的发生了。预读的决策依据应为后者而非前者。最新发布的2.6.23对此作了改进。新的算法以当前读取的页面状态为主要决策依据,并为此新增了一个页面标志位:PG_readahead,它是“请作异步预读”的一个提示。在每次进行新预读时,算法都会选择其中的一个新页面并标记之。预读规则相应的改为:
◆当读到缺失页面(missing page),进行同步预读;
◆当读到预读页面(PG_readahead page),进行异步预读。
这样一来,ahead预读窗口就不需要了:它实际上是把预读大小和提前量两者作了不必要的绑定。新的标记机制允许我们灵活而精确地控制预读的提前量,这有助于将来引入对笔记本省电模式的支持。
 
图7 Linux 2.6.23预读算法的工作动态另一个越来越突出的问题来自于交织读(interleaved read)。这一读模式常见于多媒体/多线程应用。当在一个打开的文件中同时进行多个流(stream)的读取时,它们的读取请求会相互交织在一起,在内核看来好像是很多的随机读。更严重的是,目前的内核只能在一个打开的文件描述符中跟踪一个流的预读状态。因而即使内核对两个流进行预读,它们会相互覆盖和破坏对方的预读状态信息。对此,我们将在即将发布的2.6.24中作一定改进,利用页面和pagecache所提供的状态信息来支持多个流的交织读。
预读建议 相关链接:  -------------------------------
琢磨ULK2时的一些个人理解。参考了WFG的这篇文章:
http://os.51cto.com/art/200711/60574.htm
如果觉得有必要,以后会写写其他版本预读算法的实现分析及改进逻辑。 一 为什么需要预读 1 I/O合并 2 延迟隐藏
参见上述链接。 二 预读算法初步设计 设read系统调用的内核实现函数为do_generic_read。如果不考虑预读,直观上讲,其实现用伪码表示,应该是如下形式:
do_genric_read()
{
    for(read系统调用需读取的所有页){
        if(页不在pagecache){
            分配page加入pagecache;
            锁住page;
            启动对page的I/O传送;
         }
        等待当前页I/O传送完成;
        copy当前页数据到用户空间;
    }
}在上述表示中,我们将核心流程进行简化:只在读取新页时启动I/O;如果page已经在pagecache里时,说明它要么包含有效数据,要么在page新加入到pagecache时,已经启动I/O。在不考虑I/O错误时,这种简化是可行的。 如果考虑预读,有两种形式的预读:同步预读与异步预读。
每个read系统调用需要读取的所有页面是连续的,它们应被一次性同步预读。称为“同步”,是因为read需同步等待这些页面I/O完成。当read与前一个read在文件内的位置是顺序的,说明它正在顺序读取文件,此时应进行异步预读:启动对read“最后一个页面之后”的一组连续页面的预读,为下一个顺序read提前准备数据,从而形成流水线预读。称为“异步”,是因为后续read的I/O启动与数据处理是异步进行的。 特别友情提醒:本文以后出现的“read”都是指read系统调用。 检测顺序读还有一个特例。当文件被打开后的第一次读,并且读的是文件首部时,我们善意推定,后续的read会是顺序的,因此需进行异步预读。如果不满足上述顺序性条件,就判定为随机读。任何一个随机读都将终止当前的顺序序列,从而终止预读行为。 利用这两种预读,尽最大可能隐藏I/O延迟。其实现用伪码表示,变成如下形式: do_genric_read()
{
    generic_read_ahead(read需读取的所有页)/* 启动同步预读 */
    if(顺序读)
        /* 启动异步预读*/
        generic_read_ahead(read最后一页之后的一组连续页面);
     /* 等待同步预读完成 */
    for(read需读取的所有页){
        等待当前页I/O传送完成;
        copy当前页数据到用户空间;
    }
}注意:do_genric_read并不需要等待异步预读I/O传送完成。 generic_read_ahead(预读的所有页)
{
    for(预读的所有页){
      if(页不在pagecache){
      分配page加入pagecache;
      锁住page;
      启动对page数据的I/O传送;
     }
  }
}注意:generic_read_ahead只是启动页I/O传送,但并不等待I/O完成。
异步预读的“一组连续页面”在读文件开始时可设置一个初值,如“read需读取的所有页”的2倍。在顺序读时,其额度可不断加大(例如加倍),直至某一个上限,后面还会讲到这一点。 上述实现的特点是:
1 同步预读的页面肯定会被用到,不会浪费;异步预读的页面不一定被用到,如果后续读不是顺序的,就可能部分或全部被浪费掉。
2 同步预读是为当前read服务的,因此有较小的预读组;而异步预读是为顺序读形成流水线服务的,应该有较大的预读组。
3 我们一开始就启动所有可能的预读,这是一种有“确定预读时机”的方案。 三 上述实现中的问题 我们使用预读,是希望程序在处理一批数据时,硬盘能在后台把下一批数据给CPU事先准备好,以便CPU和硬盘能流水线作业。流水线预读的所达到的理想状态是:当read向“同步预读”的页面发出请求时,页面已经由前一个read的“异步预读”读入内存,因此read无须等待I/O传送。 考虑这样的情形: 假设read“同步预读”的第一页被锁。如果它是由前一个read(或更之前的read)的“异步预读”启动I/O传送的, 说明I/O传送尚未完成。显然,对比理想状态,I/O传送速度要慢于进程运行速度。这可能是I/O系统负担太重,也可能是进程处理太快。无论哪种情形,此时再启动其他预读I/O(它与前一个read发出的I/O请求不太可能合并),不但于事无补,很可能加重I/O负担,使得I/O传送速度更慢。但在实现中,read的同步预读是无条件地启动所有页的I/O传送(如果它不是被前一个read异步预读启动的话),因此可能导致系统性能问题。 再考虑这样的情形: 假设read是顺序的,其“异步预读”的第一页未锁,说明I/O传送已经完成。如果此页是由“前一个read”异步预读启动I/O,为达到流水线预读的理想状态,此时启动下一轮预读是可行的。但如果此页是由“前一个read之前的read”启动I/O,显然,I/O传送速度要快于进程运行(可能是进程计算量太大),预读的页面数量相对很充足了,此时,启动其他预读I/O,会使得预读页面过早过多占据pagecache空间,而且一旦后续读并非顺序读,会导致I/O处理时间与pagecache空间的浪费。但在实现中,只要read为顺序读,其“异步预读”也是无条件的,同样会导致系统性能问题。 四 提出新的设计 结合上面的分析,要使预读发挥最大的效益,必须对要预读的页考虑两个因素:其I/O传送是否已经由前一个read或更前的read启动;它是否仍处于I/O传送中。前者需要记录“预读历史”,后者需要跟踪页面加锁状态。记录“预读历史”后,还能明确知道新的预读究竟从何处开始。read需根据两个因素的组合特性,决定是否启动预读I/O。我们考虑在预读中记录预读历史,然后将“确定预读时机”转化为“动态预读时机”:在处理每个页的读操作时,执行“与此页状态相关”的预读:由这个预读根据页加锁状态及预读历史,动态决定是否启动预读I/O。其基本原则是:当页是由“以前的预读”启动I/O并且加锁时,不要启动新的预读;当页是由“最近一次”预读启动I/O,并且页未锁时,应该启动新的预读。需要强调的是:“以前的预读”不但包括“最近一次预读”,但还包括更早的预读。 我们还应注意到一点,当预读与页绑定在一起时,我们无法象原实现一样,独立启动针对顺序读的异步预读,而预读函数无法自己判断顺序读。所以,应当给预读函数传递一个表示顺序读的标志。伪码如下: do_genric_read()
{
    int reada_ok=0;        /* 表示是否顺序读的标志 */
    if(顺序读)reada_ok=1;
    for(read需读取的所有页){
        if(页不在pagecache){
          分配page加入pagecache;
          锁住page;
          启动对page的I/O传送;
       }
       generic_read_ahead(page,reada_ok,file);/*与此页状态相关的预读,page指当前页 */
       等待当前页I/O传送完成;
       copy当前页数据到用户空间;
   }
}generic_read_ahead(page,reada_ok,file)
{
    根据reada_ok标志,page加锁状态及预读历史,确定是否启动预读I/O;
}五 详细分析generic_read_ahead的设计 从直观上看,我们希望上述实现在循环读取页的过程中,某一次读取会触发同步预读,另一次读取会触发异步预读,其他读取不会触发预读。这样,就与原实现在语义上等价。(真实的情形不一定是这样,后面会看到例子) 1 记录预读历史 先考虑如何记录预读历史。我们称最近一次预读的页面集合为一个预读组,显然,记录预读组是必须的,我们将预读组记录在file对象中。按照前面的分析,当顺序读命中预读组时,如果页未锁,正是推进下一次预读的最佳时机:需要启动新一轮预读I/O,在紧跟预读组尾部的位置设置新的预读组,以包含新的预读页面。如果页被锁,则不推进预读。因此,每一个后续的顺序读都可能产生一个新的预读组,那么,是否需要记录所有这些预读组呢?我们注意到一点,触发建立新预读组的读操作,其读取的页包含在前一个预读组中,因此,后续的顺序读可能仍访问前一个预读组,但不可能访问前一个预读组之前的页。因此,对顺序读,只需记录最近两个预读组即可。我们将最近两个预读组合并成预读窗口,作为总的预读历史记录在file对象中。当然,如果前面的reads只预读过一次,仅有一个预读组时,则预读窗口即为预读组。显然,当预读往前推进,设置新的预读组时,预读窗口也在同步推进。需要强调的是,因为需要区分最近一次预读和以前的预读,所以预读窗口不能完全替代预读组。预读组和预读窗口的设置,与每次预读的大小息息相关,我们将表示“下一个预读的大小”的字段f_ramax也记录在file对象中。 2 启动预读时机 有了预读组与预读窗口的定义,关于预读启动时机的基本原则可初步描述为:当读取的页被锁并命中预读窗口时,不要启动新的预读;当读取的页未锁并命中预读组时,应该启动新的预读。在这个原则的约束下,在read循环读取页面时,究竟何时启动一个预读,使它能对应“同步预读”?当读是顺序时,也即reada_ok为1时,何时启动另一个预读,使它能对应“异步预读”? 考虑这样的情形。在read读取的页面中,前一部分页面在预读窗口中,正等待I/O传送完成。考察在预读窗口外的第一页,因为它没有被前面的预读启动I/O,很可能不在pagecache中,直到本次读取页面的操作,将页加入pagecache并启动I/O,此时页面被锁,需要等待I/O操作完成。而其后需读取的页面,当然也很可能都不在pagechache中。因此,此时是启动“同步预读”的最佳时机。需要注意的是,当页在预读窗口外,而页未锁时,说明页早已在pagecache中(例如被其他进程装入内存),此时,无须启动同步预读,因为不用等待I/O操作完成,“启动同步预读”的时间不会被隐藏。 启动了同步预读后,会设置新的预读组,包含其后需读取的所有页面。在read继续往前读取页面时,发现某页未锁(此页必然在预读组中),如果read是顺序读,此时,应触发新的预读,即“异步预读”。当然,如果read不是顺序读,就不应触发异步预读。 文章出处:飞诺网(www.firnow.com):http://dev.firnow.com/course/6_system/linux/Linuxjs/2008919/143679_2.html