VFS源码分析-Readahead预读机制

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->startra->size以及ra->async_size等信息,然后调用ra_submit进行读取。

当第一个页以外传入函数时,需要根据hit_readahead_marker判断同步预读还是异步预读,同步则根据offsetreq_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=0ra->size=4以及ra->async_size=3,因此预读取了page index=0~3的页,并且给page index=1的页加上了标志。

访问第二个页(page index=1)时,由于有预读标志,因此进行异步预读。异步预读会增大预读页数,将预读页数由4个页增大到8个页,以次类推。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值