文件的预读

1.3.5 文件的预读

文件预读的内容我是把ULK-3一书中的内容全盘拷贝下来了。如果大家感兴趣可以根据源代码深入了解一下。

 

很多磁盘的访问都是顺序的。普通文件以相邻扇区成组存放在磁盘上,因此很少移动磁头就可以快速检索到文件。当程序读或拷贝一个文件时,它通常从第一个字节到最后一个字节顺序地访问文件。因此,在处理进程对同一文件的一系列读请求时,可以从磁盘上很多相邻的扇区读取。

 

预读(read-ahead)是一种技术,这种技术在于在实际请求前读普通文件或块设备文件的几个相邻的数据页。在大多数情况下,预读能极大地提高磁盘的性能,因为预读使磁盘控制器处理较少的命令,其中的每条命令都涉及一大组相邻的扇区。此外,预读还能提高系统的响应能力。顺序读取文件的进程通常不需要等待请求的数据,因为请求的数据已经在RAM中了。

 

但是,预读对于随机访问的文件是没有用的;在这种情况下,预读反而是有害的,因为它用无用的信息浪费了页高速缓存的空间。因此,当内核确定出最近所进行的I/O访问与前一次I/O访问不是顺序的时就减少或停止预读。

 

文件的预读需要更复杂的算法,这是由于以下几个原因:

 

l  由于数据是逐页进行读取的,因此预读算法不必考虑页内偏移量,只要考虑所访问的页在文件内部的位置就可以了。

l  只要进程持续地顺序访问一个文件,预读就会逐渐增加。

l  当前的访问与上一次访问不是顺序的时(随机访问),预读就会逐渐减少乃至禁止。

l  当一个进程重复地访问同一页(即只使用文件的很小一部分)时,或者当几乎所有的页都已在页高速缓存内时,预读就必须停止。

l  低级I/O设备驱动程序必须在合适的时候激活,这样当将来进程需要时,页已传输完毕。

 

如果请求的第一页紧跟上次访问所请求的最后一页,那么相对于上次的文件访问,内核把文件的这次访问看作是顺序的。

 

当访问给定文件时,预读算法使用两个页面集,各自对应文件的一个连续区域。这两个页面集分别叫做当前窗(current window)和预读窗(ahead window)。

 

当前窗内的页是进程请求的页和内核预读的页,且位于页高速缓存内(当前窗内的页不必是最新的,因为I/O数据传输仍可能在运行中)。当前窗包含进程顺序访问的最后一页,且可能有内核预读但进程未请求的页。

 

预读窗内的页紧接着当前窗内的页,它们是内核正在预读的页。预读窗内的页都不是进程请求的,但内核假定进程会迟早请求。

 

当内核认为是顺序访问而且第一页在当前窗内时,它就检查是否建立了预读窗。如果没有,内核创建一个预读窗并触发相应页的读操作。理想情况下,进程继续从当前窗请求页,同时预读窗的页则正在传送。当进程请求的页在预读窗,那么预读窗就成为当前窗。

 

预读算法使用的主要数据结构是file_ra_state描述符,它的字段见表所示:每个文件对象在它的f_ra字段中存放这样的一个描述符。

类型

字段

说明

unsigned long

start

当前窗内第一页的索引

unsigned long

size

当前窗内的页数(当临时禁止预读时为-10表示当前窗空)

unsigned long

flags

控制预读的一些标志

unsigned long

cache_hit

连续高速缓存命中数(进程请求的页同时又在页高速缓存内)

unsigned long

prev_page

进程请求的最后一页的索引

unsigned long

ahead_start

预读窗内第一页的索引

unsigned long

ahead_size

预读窗的页数(0表示预读窗口空)

unsigned long

ra_pages

预读窗的最大页数(0表示预读窗永久禁止)

unsigned long

mmap_hit

预读命中计数器(用于内存映射文件)

unsigned long

mmap_miss

预读失败计数器(用干内存映射文件)

当一个文件被打开时,在它的file_ra_state描述符中,除了prev_pagera_pages这两个字段,其他的所有字段都置为0prev_page字段用来存放进程在上一次读操作中所请求页的最后一页的索引。它的初值是-1ra_pages字段表示当前窗的最大页数,即对该文件允许的最大预读量。该字段的初始值(缺省值)存放在该文件所在块设备的backing_dev_info描述符中。一个应用可以修改一个打开文件的ra_pages字段从而调整预读算法;具体的实现方法是调用posix_fadvise()系统调用,并传给它命令POSIX_FADV_NORMAL(设最大预读量为缺省值,通常是32页)、POSIX_FADV_SEQUENTIAL(设最大预读量为缺省值的两倍)和POSIX_FADV_RANDOM(最大预读量为0,从而永久禁止预读)。

 

flags字段内有两个重要的字段RA_FLAG_MISSRA_FLAG_INCACHE。如果已被预读的页不在页高速缓存内(可能的原因是内核为了释放内存而加以收回了),则第一个标志置位,这时候下一个要创建的预读窗大小将被缩小。当内核确定进程请求的最后256页都在页高速缓存内时(连续高速缓存命中数存放在ra->cache_hit字段中),第二个标志置位,这时内核认为所有的页都已在页高速缓存内,进而关闭预读。

 

如何时执行预读算法?这有下列几种情形:

 

l  当内核用用户态请求来读文件数据的页时。这一事件触发page_cach_readahead()函数的调用。

l  当内核为文件内存映射分配一页时。

l  当用户态应用执行readahead()系统调用时,它会对某个文件描述符显式触发某预读活动。

l  当用户态应用使用POSIX_FADV_NOREUSEPOSIX_FADV_WILLNEED命令执行posix_fadvise()系统调用时,它会通知内核,某个范围的文件页不久将要被访问。

l  当用户态应用使用MADV_WILLNEED命令执行madvise()系统调用时,它会通知内核,某个文件内存映射区域中的给定范围的文件页不久将要被访问。

page_cache_readahead()函数

 

page_cache_readahead()函数处理没有被特殊系统调用显式触发的所有预读操作。它填写当前窗和预读窗,根据预读命中数更新当前窗和预读窗的大小,也就是根据过去对文件访问预读策略的成功程度来调整。

 

当内核必须满足对某个文件一页或多页的读请求时,函数就被调用,该函数有下面五个参数:mapping:描述页所有者的address_space对象指针

ra:包含该页的文件file_ra_state描述符指针

file:文件对象地址

offset:文件内页的偏移量

req_size:要完成当前读操作还需要读的页数(实际上,如果读操作要读的页数大于预读窗的最大尺寸,就会多次调用page_cache_readahead()函数。因此,req_size参数可能比完成读操作还需要读的页数小)

 

下图是page_cache_readahead()的流程图。该函数基本上作用于file_ra_state描述符的字段,因此,尽管流程图中的行为描述不很正规,你还是能很容易地确定函数执行的实际步骤。例如,为了检查请求页是否与刚读的页相同,函数检查ra->prev_page字段的值和offset参数的值是否一致(见前面的表)。

文件的预读_第1张图片

当进程第一次访问一个文件,并且其第一个请求页是文件中偏移量为0的页时,函数假定进程要进行顺序访问。那么,函数从第一页创建一个新的当前窗。初始当前窗的长度(总是为2的幂)与进程第一个读操作所请求的页数有一定的联系。请求页数越大,当前窗越大,一直到最大值,最大值存放在ra->ra_pages字段。反之,当进程第一次访问文件,但其第一个请求页在文件中的偏移量不为0时,函数假定进程不是执行顺序读。那么,函数暂时禁止预读(ra->size字段设为-1)。但是当预读暂时被禁止而函数又认为需要顺序访问时,将建立一个新的当前窗。

 

如果预读窗不存在,一旦函数认为在当前窗内进程执行了顺序读,则预读窗将被建立。预读窗总是从当前窗的最后一页开始。但它的长度与当前窗的长度相关:如果RA_FLAG_MISS标志置位,则预读窗长度是当前窗长度减2,小于4时设为4;否则,预读窗长度是当前窗长度的4倍或2倍。如果进程继续顺序访问文件,最终预读窗成为新的当前窗,新的预读窗被创建。这样,随着进程顺序地读文件,预读会大大地增强。

 

一旦函数认识到对文件的访问相对于上一次不是顺序的,当前窗与预读窗就被清空,预读被暂时禁止。当进程的读操作相对于上一次文件访问为顺序时,预读将重新开始。

每次page_cache_readahead()创建一个新窗,它就开始对所包含页的读操作。为了读一大组页,函数page_cache_readahead()调用blockable_page_cache_readahead()。为减少内核开销,后面这个函数采用下面灵活的方法:

l  如果服务于块设备的请求队列是读拥塞的,就不进行读操作。

l  将要读的页与页高速缓存进行比较,如果该页已在页高速缓存内,跳过即可。

l  在从磁盘进行读之前,读请求所需的全部页框是一次性分配的。如果不能一次性得到全部页框,预读操作就只在可以得到的页上进行。而且把预读推迟至所有页框都得到时再进行并没有多大意义。

l  只要可能,通过使用多段bio描述符向通用块层发出读操作。这通过address_space对象专用的readpages方法实现(假如已定义);如果没有定义,就通过反复调用readpage方法来实现。readpage方法在前面“从文件中读取数据”一节中对于单段情形有详细描述,但稍作修改就可以很容易地将它用于多段情形。

 

handle_ra_miss()函数

在某些情况下,预读策略似乎不是十分有效,内核就必须修正预读参数。图中展示了两种情形:请求页在当前窗或预读窗表明它已经被预先读入了;或者还没有,则调用blockable_page_cache_readahead()来读入。在这两种情形下,函数do_generic_file_read()应该在第4d步中就在页高速缓存中找到了该页,如果没有,就表示该页框已被收回算法从高速缓存中删除。在这种情形下,do_generic_file_read()调用handle_ra_miss()函数,这个函数会通过将RA_FLAG_MISS标志置位与RA_FLAG_INCACHE标志清0来调整预读算法。

 

 

 

你可能感兴趣的:(数据结构,算法,cache,File,Random,磁盘)