Readahead预读机制
由于内存的速度比磁盘速度快很多,如果每一次访问文件数据都要从磁盘读取一次数据,就会导致非常严重的时延。因此Linux为了提高性能,通过page cache机制,将多个用户数据缓存在内存当中,从而避免多次再磁盘读取。Readahead预读机制正是将用户数据缓存到内存的方法之一。
Readahead机制的介绍
Readahead预读机制是Linux针对顺序读的性能优化机制。它的核心思想是当用户访问连续多个page的时候,一次性将多个连续的页从磁盘读取到内存中,从而避免多次与磁盘交互,降低性能。
Readahead机制的源码分析
预读行为的在generic_file_buffered_read
函数做了具体实现,如下:
static ssize_t generic_file_buffered_read(struct kiocb *iocb,
struct iov_iter *iter, ssize_t written)
{
...
for (;;) {
page = find_get_page(mapping, index); // 访问特定index的page
if (!page) { // 如果是NULL,即在page cache找不到
page_cache_sync_readahead(mapping,
ra, filp,
index, last_index - index); // 进行同步预读
page = find_get_page(mapping, index); // 预读以后再获取一次
}
if (PageReadahead(page)) { // 如果读取出来的page包含Readahead的特殊标志
page_cache_async_readahead(mapping,
ra, filp, page,
index, last_index - index); // 进行一次异步预读
}
}
...
}
从上面可以知道,当find_get_page
函数无法在page cache中找到该index对应的page的实例,就会调用page_cache_sync_readahead
函数进行同步预读,预读完的数据就会加入到page cache中,再一次调用find_get_page
函数就可以获取出来。
page index=1的页以及异步预读出来的最后一个页会包含特殊标志,这个标志可以通过PageReadahead
函数进行判断,从而判断是否需要调用page_cache_async_readahead
进行异步预读。
同步预读以及异步预读都会调用同一个函数ondemand_readahead
,只是输入参数不一样,如下所示:
void page_cache_sync_readahead(struct address_space *mapping,
struct file_ra_state *ra, struct file *filp,
pgoff_t offset, unsigned long req_size)
{
if (!ra->ra_pages)
return;
if (blk_cgroup_congested())
return;
if (filp && (filp->f_mode & FMODE_RANDOM)) {
force_page_cache_readahead(mapping, filp, offset, req_size);
return;
}
ondemand_readahead(mapping, ra, filp, false, offset, req_size);
}
page_cache_async_readahead(struct address_space *mapping,
struct file_ra_state *ra, struct file *filp,
struct page *page, pgoff_t offset,
unsigned long req_size)
{
if (!ra->ra_pages)
return;
if (PageWriteback(page))
return;
ClearPageReadahead(page); // 如前面所述,异步预读的最后一个页会特殊的标志,再一次触发异步预读后在这里清除标志
if (inode_read_congested(mapping->host))
return;
if (blk_cgroup_congested())
return;
ondemand_readahead(mapping, ra, filp, true, offset, req_size);
}
根据代码,同步预读和异步预读传入ondemand_readahead
函数的第四个参数不一样,一个是False一个是True。为了搞清楚差异,继续分析ondemand_readahead
函数的源码。
static unsigned long
ondemand_readahead(struct address_space *mapping,
struct file_ra_state *ra, struct file *filp,
bool hit_readahead_marker, pgoff_t offset,
unsigned long req_size)
{
struct backing_dev_info *bdi = inode_to_bdi(mapping->host);
unsigned long max_pages = ra->ra_pages; // 一般值是32
unsigned long add_pages;
pgoff_t prev_offset;
/*
* 根据条件,计算本次预读最大预读取多少个页,一般情况下是max_pages=32个页
*/
if (req_size > max_pages && bdi->io_pages > max_pages)
max_pages = min(req_size, bdi->io_pages);
/*
* offset即page index,如果page index=0,表示这是文件第一个页,跳转到initial_readahead进行处理
*/
if (!offset)
goto initial_readahead;
/*
* 默认情况下是 ra->start=0, ra->size=0, ra->async_size=0 ra->prev_pos=0
* 但是经过第一次预读后,上面三个值会出现变化
*/
if ((offset == (ra->start + ra->size - ra->async_size) ||
offset == (ra->start + ra->size))) {
ra->start += ra->size;
ra->size = get_next_ra_size(ra, max_pages);
ra->async_size = ra->size;
goto readit;
}
/*
* 异步预读的时候会进入这个判断,更新ra的值,然后预读特定的范围的页
* 异步预读的调用表示Readahead出来的页连续命中
*/
if (hit_readahead_marker) {
pgoff_t start;
rcu_read_lock();
// 这个函数用于找到offset + 1开始到offset + 1 + max_pages这个范围内,第一个不在page cache的页的index
start = page_cache_next_miss(mapping, offset + 1, max_pages);
rcu_read_unlock();
if (!start || start - offset > max_pages)
return 0;
ra->start = start;
ra->size = start - offset; /* old async_size */
ra->size += req_size;
/*
* 由于连续命中,get_next_ra_size会加倍上次的预读页数
* 第一次预读了4个页
* 第二次命中以后,预读8个页
* 第三次命中以后,预读16个页
* 第四次命中以后,预读32个页,达到默认情况下最大的读取页数
* 第五次、第六次、第N次命中都是预读32个页
* */
ra->size = get_next_ra_size(ra, max_pages);
ra->async_size = ra->size;
goto readit;
}
if (req_size > max_pages)
goto initial_readahead;
prev_offset = (unsigned long long)ra->prev_pos >> PAGE_SHIFT;
if (offset - prev_offset <= 1UL)
goto initial_readahead;
if (try_context_readahead(mapping, ra, offset, req_size, max_pages))
goto readit;
// 这个函数执行具体的从磁盘读取的流程
return __do_page_cache_readahead(mapping, filp, offset, req_size, 0);
initial_readahead:
ra->start = offset;
/* get_init_ra_size初始化第一次预读的页的个数,一般情况下第一次预读是4个页 */
ra->size = get_init_ra_size(req_size, max_pages);
ra->async_size = ra->size > req_size ? ra->size - req_size : ra->size;
readit:
if (offset == ra->start && ra->size == ra->async_size) {
add_pages = get_next_ra_size(ra, max_pages);
if (ra->size + add_pages <= max_pages) {
ra->async_size = add_pages;
ra->size += add_pages;
} else {
ra->size = max_pages;
ra->async_size = max_pages >> 1;
}
}
/*
* 经过一点处理以后,会调用__do_page_cache_readahead函数,执行具体的从磁盘读取的流程
* 区别在于它是基于ra->start ra->async_size等信息进行读取
* */
return ra_submit(ra, mapping, filp);
}
当第一个页(page index=0)传入函数时,跳到initial_readahead
部分,初始化ra->start
、ra->size
以及ra->async_size
等信息,然后调用ra_submit
进行读取。
当第一个页以外传入函数时,需要根据hit_readahead_marker
判断同步预读还是异步预读,同步则根据offset
和req_size
进行预读,如果是异步则通过ra->start
以及ra->async_size
进行预读。
ondemand_readahead
函数的核心是__do_page_cache_readahead
函数,它会根据传入的参数,从磁盘读取特定范围的数据:
unsigned int __do_page_cache_readahead(struct address_space *mapping,
struct file *filp, pgoff_t offset, unsigned long nr_to_read,
unsigned long lookahead_size)
{
struct inode *inode = mapping->host;
struct page *page;
unsigned long end_index;
LIST_HEAD(page_pool); // 将要读取的页存入到这个list当中
int page_idx;
unsigned int nr_pages = 0;
loff_t isize = i_size_read(inode); // 得到文件的大小
gfp_t gfp_mask = readahead_gfp_mask(mapping);
end_index = ((isize - 1) >> PAGE_SHIFT); // 根据文件大小计算得到最后一个页的index
for (page_idx = 0; page_idx < nr_to_read; page_idx++) {
pgoff_t page_offset = offset + page_idx; // 计算得到page index
if (page_offset > end_index) // 超过了文件的尺寸就break,停止读取
break;
rcu_read_lock();
// 查看是否在page cache,如果已经在了cache中,再判断是否为脏,要不要进行读取
page = radix_tree_lookup(&mapping->i_pages, page_offset);
rcu_read_unlock();
if (page && !radix_tree_exceptional_entry(page)) {
if (nr_pages)
read_pages(mapping, filp, &page_pool, nr_pages, gfp_mask);
nr_pages = 0;
continue;
}
// 如果不存在,则创建一个page cache结构
page = __page_cache_alloc(gfp_mask);
if (!page)
break;
// 设定page cache的index
page->index = page_offset;
// 加入到list当中
list_add(&page->lru, &page_pool);
// !!! 注意计算值,给这一个页加上Readahead的标志
if (page_idx == nr_to_read - lookahead_size)
SetPageReadahead(page);
nr_pages++;
}
/*
* 如果nr_pages大于0,则表示有页要进行读取
* 执行read_pages从磁盘进行读取
*/
if (nr_pages)
read_pages(mapping, filp, &page_pool, nr_pages, gfp_mask);
out:
return nr_pages;
}
Readahead机制的实例分析
下面通过一个顺序读的例子说明Linux预读机制的执行:
用户需要连续访问某个文件连续32个页(page index=0~31)的数据,那么它在预读机制下的访问行为是:
用户访问第一个页,page index=0,触发同步预读机制,一次性从磁盘读取4个页,即第1~4个页(page index=0~3)
用户访问第二个页,page index=1,含有特殊标志,触发异步预读机制,一次性从磁盘读取8个页,即第5~12个页(page index=4~11)
用户访问第三个页,page index=2,命中,直接返回给用户
用户访问第四个页,page index=3,命中,直接返回给用户
用户访问第五个页,page index=4,含有特殊标志,触发异步预读机制,一次性从磁盘读取16个页,即第13~28个页(page index=12~27)
用户访问第六个页,page index=5,命中,直接返回给用户
用户访问第七个页,page index=6,命中,直接返回给用户
用户访问第八个页,page index=7,命中,直接返回给用户
用户访问第九个页,page index=8,命中,直接返回给用户
用户访问第十个页,page index=9,命中,直接返回给用户
用户访问第十一个页,page index=10,命中,直接返回给用户
用户访问第十二个页,page index=11,命中,直接返回给用户
用户访问第十三个页,page index=12,含有特殊标志,触发异步预读机制,一次性从磁盘读取32个页,即第12~42个页(page index=13~43),但是由于page index=12~27的页上一次预读就将页读入了page cache,因此会跳过,实际上只会从磁盘读取page index=28~43页。
…以此类推
当访问第一个页时,跳到initial_readahead
部分,初始化ra->start=0
、ra->size=4
以及ra->async_size=3
,因此预读取了page index=0~3的页,并且给page index=1的页加上了标志。
访问第二个页(page index=1)时,由于有预读标志,因此进行异步预读。异步预读会增大预读页数,将预读页数由4个页增大到8个页,以次类推。