linux 按需预读策略追踪(源码参考centos6.0内核源码)

来源:互联网 发布:数据分析师有哪些证书 编辑:程序博客网 时间:2024/05/17 04:57

1   按需预读

我理解的按需预读和传统预读根本不同的地方,在于传统预读是按照请求来触发预取,而按需预读是按照访问的页面是否存在来触发预取。这也正是按需这两个字的含义。当访问页面不存在时,按需预读才开始触发预读。

同时为了更好的解决并发顺序流的访问,引入了顺序流的概念。这个顺序流是空间连续,而非时间连续。在每个顺序流中都会选出一个位置,当IO访问到该页面时,便会触发预读。这样每个顺序流都有属于自己独立的预取记录信息。

在page cache层,每一个file结构里会记录着一个窗口的信息。如果此时对一个文件open多次,会有多个窗口信息。当然每一个打开的file,有可能会有并发顺序流的访问。对于我们CBD层来说是不是只有一个窗口就足够了呢?

1.1 缓存命中率

1.2 数据结构

struct file_ra_state {

  pgoff_t start;     /* where readahead started */

  unsigned intsize;    /* # of readahead pages */

  unsigned intasync_size; /* do asynchronous readaheadwhen

               there are only # of pages ahead */

  unsigned intra_pages;     /* Maximum readahead window*/

  unsigned intmmap_miss;   /* Cache miss stat for mmapaccesses */

  loff_tprev_pos;   /* Cache last read() position*/

};

在非按需预读中,采用的是两个窗口。一个是current window,用来记录上次预读的信息。一个是预读窗口。当IO在当前窗口中访问时,不会触发预读。一旦IO进入预读窗口,便开始进行预读操作(将current window记录ahead window的信息,然后将ahead window右移)。

在按需预读中,只保留了一个readahead window。Ahead window的功能被变量async_size来取代。在简化后的数据结构中, start 和size 构成一个预读窗口,记录了最近一次预取请求的位置和大小; async_size 指示了异步预取的位置提前量,即这个流的前方还剩余多少未访问的预取页面时启动下一次预取I/O;prev_pos记录了最后的读位置,它可用于简单的顺序性测试。

1.3 算法框架

在非按需预读中,原预读例程page_cache_readahead(),除了进行正常的顺序性匹配及预读之外,还需要判断和处理多种异常情况,包括:预取功能是否被禁用;磁盘设备是否太忙而不宜立即进行异步预取;数据是否已经被缓存不需要预取;下一个异步预取的时机是否已经到来,等等。上述逻辑交织在一起,使预取算法变得相当复杂。这对代码的理解、BUG 的修复和功能的扩展非常不利。

按需预读把预取算法在逻辑结构上拆分成两大部分:监控/触发部分和匹配/执行部分。监控部分嵌入在do_generic_file_read()等读请求响应例程中,并在访问每个文件页面之前,检查目标页面是否满足预取的触发条件;匹配部分位ondemand_readahead(),在逻辑上由一组独立的判决模块组成,每个模块匹配并处理一种访问模式。



1.3.1 预取触发条件

非按需预读是按照读请求来进行预读操作,基于IO请求的预读。可以说根本就没有触发条件。这样有很多弊端:

a、    CPU资源的浪费,例如:当一个读请求发现自己此时不适合预读(没有到达ahead window),便放弃此次预读机会。

b、    预读缓存命中CPU资源浪费:因为没有经过任何判断,就开始预读。有可能此次要预读的页面已经在缓存中了。这时也是对CPU资源的一种浪费,因为需要在缓存中查找相应的页面。像那种读小文件密集型的IO,此时cpu浪费较为明显。因为小文件不足以让预读算法进入INCACHE的状态。INCACHE的设定需要连续vm_max_cache_hit(256)个页面缓存命中才可以设置。

c、     预读缓存命中,在走出缓存区时。并没有开始进入预读流程,只是设置了CACHE_MISS标志位,清空INCACHE标志位。只有在第二次IO访问时,才会开始进入预读流程。这样在第一次没有命中缓存时会生成一个读4K数据的小IO。

d、    只有当IO进入ahead window时,开始进行异步预读。这里只能描述一个区域,而不能精确的说明到底何时开始进行异步预读。按需预读是按照页面来进行预读的,通过async_size可以精确的控制何时进行预读操作。

预读触发条件:

1. 缓存缺失页面(cache miss page):此时需要立即进行磁盘I/O 加载当前页面,与此同时应用程序将被临时挂起等待I/O。此时应当调用预取例程,确定即将被访问的临近页面,进行同步预读。

2. 预取标记页面( PG_readaheadpage):此标记是在上一次预取I/O 中设置的,见到它意味着进行下一个预取I/O 的时机已经来到。因而应立即调用预取例程,进行异步预读。相对于前一触发条件,这一触发条件的设置有助于减少应用程序的I/O 阻塞时间。PG_readahead 标记必须在它成功触发预读的同时清除,以避免重复触发的发生。

当一次预读操作开始,当预读的page到达nr_to_read – async_size时,会给该page设置PG_readahead标记。

1.3.2 自适应的预取大小

在三种情况下会初始化预读窗口,当然这都是在访问缓存页面不存在的前提下还需要满足以下三个条件之一。

a、 offset == 0

b、 req_size > max

c、 发现自己是连续读(这种页面缺失,有可能是内存压力过大,回收掉了已经缓存上来的页面)

初始化话预读窗口大小(以下记为size),伪代码如下:

    newsize = readsize;

if (newsize <= max / 32)

    newsize = newsize * 4;

else if (newsize <= max / 4)

    newsize = newsize * 2;

else

    newsize = max;

size = newsize

当接下来连续访问中,会不断扩大窗口,伪代码如下:

    if (size < max / 16)

       newsize = 4 * size;

    else

       newsize = 2 * size;

    min(newsize, max);

1.3.3 异步预取的提前量

设置异步预取的提前量规则如下:

1、 提前量async_size通常设置为最大可能的值:aysnc_size= size;

2、 对非超大读的首次预取,等到下一个读请求时才启动异步预读。对于文件访问来说,元数据一般都在文件首部。这种推迟异步预读的方式,可以不必要的增加程序的IO阻塞时间。

1.3.4 预读抖动

在非按需预取中,在内存压力很大时,预读上来的缓存数据有可能在没有被访问之前被回收掉。被回收掉,没有做任何的补救操作。这样会造成大量的小IO,从而导致大量的磁盘寻到操作。

而按需预取框架,因为加入了预取触发条件。在没有命中缓存的情况下,自动开始新一轮的预取。随着I/O的继续,窗口会不断放大,直到设置的窗口最大值。

1.3.5 非对齐读和重试读

对于非对齐读,在非按需预读算法中是不能很好处理的。对于(128,128,128,128)这样的访问序列。传统的预读算法会认为这是一个非连续的读请求,归类为随机读。然而在按需算法中时按照访问的页面来触发预读的,当该页面已经存在,便不会再发起预读操作。

一组重试读的请求页面为:[0, 1000] [4, 1000] [8, 1000]。传统的预读算法无法理解这样的访问模式。这些读请求会被当作随机的超大读,并触发如下的预读请求:[0, 32) + [32, 64), [4, 36) + [36, 68), [8, 40) + [40, 72), . . .注意它们是互相重叠的,虽然预读请求的大小是ra_pages(=32) 个页面,但是其中大部分页面都是预读缓存命中,实际的I/O 大小仅有4 个页面。

同样因为按需预读是按照访问页面是否存在来触发的,当发现访问页面已经存在时。便不会触发异步预读,很好的解决了这个问题。

1.4 顺序流的预取算法

1.4.1 被动检测

蓝色:数据被访问过

绿色:设置PG_readahead标记为的页面

粉色:被预读上来缓存的数据,没有被访问过

灰色:没有在缓存中的数据。



1.4.2 主动检测

主动性检测顺序流,当检测到一个没有被缓存的页面属于一个顺序流时有两种情况。第一种便是顺序流刚开始读,因为顺序流的第一次读会被当做随机读。第二种情况,因为缓存压力,被缓存的数据已经被回收了。因为通常的文件访问,如果预读功能是打开的话,那么缓存的功能也是会打开的。因而最近被访问过的页面就会被缓存。而在顺序访问中,文件地址空间中的前一个页面就是最近被访问过的页面。


1.4.3 交织的顺序流

1.4.4 矩阵的列向扫描(跨步读)

在这种需要矩阵运算的计算项目中,往往会读取一列。这种读取也叫做跨不读,每次读取得数据之间间隔相等。众所周知的优化方法便是进行异步跨读,即探知跨步读的步距和读大小。按此规律推算未来若干个跨不读的位置,然后预先加载之。但是这种跨步预读不能帮助增加对性能至关重要的I/O尺寸。

在按需预读中,因为是按页面来预读的。这种异步预读就转换为了N路顺序读。大大提高了性能。

1.4.5 随机加顺序并发访问

此种情况和3.4.3小节提到的类似,只是这次打破顺序读的是一个个的随机读。这样会产生两个很坏的结果:

1、 窗口总是被关闭,导致预取大小一直增长不上去。长生小IO。

2、 会导致缓存命中。例如(1,2,3,4,5,6)这个访问流被一个随机读打乱成(1,2,3,128,4,5,6)。在访问1到3这几个页面时已经预读了1到4,和5到13。在访问4到6这三个页面时,重新启动异步预读,会预读4到7,8到16这几个页面。但是4到13已经在缓存中了。此时只会发起一个只有3个页面的读操作。

按需预读就会避免这个问题。按需预读会识别出这是一个连续读,读取范围为1到6和一个随机读,读取页面128。

1.4.6 稀疏的顺序读

1.4.7 局部随机的顺序读

把文件分拆成小块,并按顺序把它们分配给服务进程池中的一个工作进程。由于并发调度中的不确定性,每个文件块的实际被服务时间可能是同步或乱序的。nfs就属于类似的情况。而且对于nfs来说。按需预读还解决了重试读的问题。

1.4.8 高密度的随机读

当一个随机读结束后,有可能很快就会有第二个随机读(开始位置紧接着第一个随机读的末尾)。这样就形成了一个连续读,会留下PG_readahead标记位。因为是高密度的随机读,所以会有大量设置PG_readahead标记位的页。这些页被命中的概率也越大,会发起异步预读。这样会大大改善I/O性能。

原创粉丝点击