2015年(100)
分类: LINUX
2015-08-13 13:02:55
Linux的预读架构如图所示,Linux内核会将它最近访问过的文件页面缓存在内存中一段时间,这个文件缓存被称为pagecache。如图1所示。一般的read()操作发生在应用程序提供的缓冲区与pagecache之间。而预读算法则负责填充这个pagecache。应用程序的读缓存一般都比较小,比如文件拷贝命令cp的读写粒度就是4KB;内核的预读算法则会以它认为更合适的大小进行预读 I/O,比如16-128KB。
图1 以pagecache为中心的读和预读
我们分析如下情况,用户程序调用read函数对设备进行读操作,文件系统会调用相应的方法:generic_file_aio_read() -->do_generic_file_read(),在do_generic_file_read()函数中,使用find_get_page()从cache中查找该page。如果没有找到,则调用page_cache_sync_readahead进行适当的预读,预读之后一般page就可以找到了。
页面标志位:PG_readahead,它是“请作异步预读”的一个提示。在每次进行新预读时,算法都会选择其中的一个新页面并标记之。预读规则为:
◆当读到缺失页面(missing page),进行同步预读;
◆当读到预读页面(PG_readahead page),进行异步预读;
◆在读取方向上剩余页数为async_size时,进行异步预读。
我们来看以下例子:
当进程打开一个文件,想要读取第一页,而该页不在缓存中,这时内核采用同步预读page_cache_sync_readahead()读取了若干页(图中为4页),并将预读窗口中的第二页标记为PG_readahead。
进程现在继续读取接下来的各页,当读取到有PG_readahead标志的第二页时,内核触发一个异步预读操作page_cache_async_readahead(),在后台读取若干页。因为这时候缓存中还有两页可用,不必匆忙读取,所以不需要一个同步预读操作。
现在将重复这种做法。由于异步预读又将预读窗口中的一页标记为PG_readahead,在进程遇到该页时,又进行一次异步预读,以此类推。
以上说到预读若干页,那么预读窗口的最优长度到底是几页呢?预读粒度太小的话,达不到应有的性能提升效果;预读太多,又有可能载入太多程序不需要的页面,造成资源浪费。为此,Linux内核设置了一个file_ra_state数据结构,关联到每个file实例,记录每个文件的预读状态。
struct file_ra_state{
pgoff_tstart; /*预读的起始位置 */
unsigned intsize; /*预读的页数,即预读窗口长度 */
unsigned intasync_size; /* 阈值,在读取方向上剩余页数为该值时,启动异步预读*/
unsigned intra_pages; /*预读窗口最大长度 */
intmmap_miss; /*Cache miss stat for mmap accesses */
loff_tprev_pos; /*前一次读取时,最后的访问位置*/
};
用于实现预读的函数之间的关联如下图所示:
ondemand_readahead()函数在确定预读窗口长度之后,调用ra_submit(),ra_submit()是对函数__do_page_cache_readahead()的封装,于是预读的技术性问题是由后者完成的。
get_init_ra_size()和get_next_ra_size()是辅助ondemand_readahead()判断需要读入多少当前不需要的页的辅助函数。其中get_init_ra_size()根据进程请求的页数为一个文件确定最初的预读窗口长度,而get_next_ra_size()为后来的读取计算长度,即此时已经有一个先前的预读窗口存在,函数根据前一个预读窗口长度计算新的预读窗口长度。两个函数都会确保预读窗口长度不超过特定于文件的上限值。该上限值通常设置为VM_MAX_READAHEAD * 1024 /PAGE_CACHE_SIZE,在页长度为4K的系统上,相当于32页。两个函数的结果如下图所示:
不难看出,初始值一般是最近的2的整数幂的值,而新的窗口长度是原长度的两倍。
何时构建一个新的预读窗口?何时是顺序读取?ondemand_readahead()函数进行如下判断:
(1)当前偏移量在前一个预读窗口末尾,或在触发阈值的点上时,内核识别为顺序读取,使用get_next_ra_size()为后来的读取计算长度;
(2)若是随机读,则直接调用__do_page_cache_readahead()进行读取,而不破坏当前的预读状态;
(3)若是遇到预读标志,则使用get_next_ra_size()来计算新的预读窗口长度;
(4)如果以上情况都不是,内核则判定为对文件的第一次读取,这时用get_init_ra_size()建立一个新的预读窗口。