页缓存page cache和地址空间address_space

注:本文分析基于linux-4.18.0-193.14.2.el8_2内核版本,即CentOS 8.2

1 page cache

page cache用于在内存中缓存磁盘文件,几乎所有文件的读写都依赖于page cache,除非使用直接IO的方式。它是文件系统cache中占比最大的部分,也是对文件系统读写性能提升最大的部分,而地址空间address_space则是page cache的核心管理结构。

2 struct address_space主要成员变量

struct address_space {
	struct inode		*host;		//pagecache的所有者,inode或者block device
	struct radix_tree_root	i_pages;	/* cached pages */
	atomic_t		i_mmap_writable;/* count VM_SHARED mappings */
	struct rb_root_cached	i_mmap;		/* tree of private and shared mappings */
	struct rw_semaphore	i_mmap_rwsem;	/* protect tree, count, list */
	/* Protected by the i_pages lock */
	unsigned long		nrpages;	//包含的页面数量
	/* number of shadow or DAX exceptional entries */
	unsigned long		nrexceptional;
	pgoff_t			writeback_index;/* writeback starts here */
	const struct address_space_operations *a_ops;	/* methods */
	unsigned long		flags;		/* error bits */
	spinlock_t		private_lock;	/* for use by the address_space */
	gfp_t			gfp_mask;	/* implicit gfp mask for allocations */
	struct list_head	private_list;	//一般用来放metadata buffers,但ext4不使用该变量
	void			*private_data;	//同上
	...
};

3 创建page cache

page cache的创建一般发生在应用程序读取文件时,会在内存中查找缓存,如果找到就直接返回,没找到就会创建一个缓存页,然后从磁盘中读取该文件填充该缓存页。

我们从read系统调用出发,大概看下这个过程,

const struct file_operations ext4_file_operations = {
	.llseek		= ext4_llseek,
	.read_iter	= ext4_file_read_iter,
	.write_iter	= ext4_file_write_iter,
	.mmap		= ext4_file_mmap,
	.open		= ext4_file_open,
	...
};

SYSCALL_DEFINE3(read ->											//read系统调用入口
	ksys_read ->
		vfs_read ->												//vfs层接口
			__vfs_read ->										//对于ext4文件系统,没有read函数,只有read_iter
				new_sync_read ->
					call_read_iter ->
						file->f_op->read_iter ->				//调用对应文件系统的read_iter函数
							ext4_file_read_iter -> 
								generic_file_read_iter ->
									generic_file_buffered_read  //查找pagecache或者创建新的pagecache

从系统调用入口,进而调用对应文件系统的read_iter函数,然后来到generic_file_buffered_read,在这里就会进行pagecache的查找,以及创建。
主要流程:

  • 以文件的f_mapping为参数,通过find_get_page查找page cache
  • 查找到page cache且数据是最新的,就通过copy_page_to_iter,将数据拷贝到用户空间
  • 没找到,就通过page_cache_alloc分配一个新页面,并将其加入page cache和LRU链表
  • 然后调用对应的readpage函数,从磁盘中读入文件数据
  • 最后还是通过copy_page_to_iter,将数据拷贝到用户空间
static const struct address_space_operations ext4_aops = {
	.readpage		= ext4_readpage,
	.readpages		= ext4_readpages,
	.writepage		= ext4_writepage,
	.writepages		= ext4_writepages,
	.direct_IO		= ext4_direct_IO,
	.releasepage		= ext4_releasepage,
	...
};

static ssize_t generic_file_buffered_read(struct kiocb *iocb,
		struct iov_iter *iter, ssize_t written)
{
	struct file *filp = iocb->ki_filp;
	struct address_space *mapping = filp->f_mapping;
	struct inode *inode = mapping->host;
	struct file_ra_state *ra = &filp->f_ra; //获取预读设置
	loff_t *ppos = &iocb->ki_pos;
	...
	for (;;) {
		...
		//根据地址空间查询是否有对应的pagecache
		page = find_get_page(mapping, index);
		if (!page) {
			if (iocb->ki_flags & IOCB_NOWAIT)
				goto would_block;
			//如果没有查找到pagecache,则尝试执行同步预读
			page_cache_sync_readahead(mapping, ra, filp, index, last_index - index);
			//预读后,再次查找,如果没有开启预读则依旧返回空
			page = find_get_page(mapping, index);
			if (unlikely(page == NULL))
				goto no_cached_page;
		}
		//如果设置了预读
		if (PageReadahead(page)) {
			//开始异步预读
			page_cache_async_readahead(mapping, ra, filp, page, index, last_index - index);
		}
		...
page_ok:
		...
		//顺序读时只在第一次时设置accessed标志
		if (prev_index != index || offset != prev_offset)
			mark_page_accessed(page);
		prev_index = index;

		//拷贝数据到用户地址空间
		ret = copy_page_to_iter(page, offset, nr, iter);
		offset += ret;
		index += offset >> PAGE_SHIFT;
		offset &= ~PAGE_MASK;
		prev_offset = offset;

		put_page(page);
		written += ret;
		//数据读取完毕,则跳出循环,否则再接着读
		if (!iov_iter_count(iter))
			goto out;
		...
		continue;
		...
readpage:
		ClearPageError(page);
		//调用readpage函数,读取对应文件系统的文件到页面中,对于ext4调用的是ext4_readpage
		error = mapping->a_ops->readpage(filp, page);
		...
		goto page_ok;
		...
no_cached_page:
		//没有找到pagecache,分配一个page页面,读取磁盘数据并放到pagecache中
		page = page_cache_alloc(mapping);
		...
		//将新建的page加入pagecache,同时也加入对应的LRU链表
		error = add_to_page_cache_lru(page, mapping, index,
				mapping_gfp_constraint(mapping, GFP_KERNEL));
		...
		goto readpage;
	}
	...
	return written ? written : error;
}

新缓存页创建后,通过add_to_page_cache_lru将其加入pagecache的基树中,同时也会加入对应的LRU链表

int add_to_page_cache_lru(struct page *page, struct address_space *mapping,
				pgoff_t offset, gfp_t gfp_mask)
{
	void *shadow = NULL;
	int ret;

	__SetPageLocked(page);
	//加入pagecache中
	ret = __add_to_page_cache_locked(page, mapping, offset, gfp_mask, &shadow);
	if (unlikely(ret))
		__ClearPageLocked(page);
	else {
		WARN_ON_ONCE(PageActive(page));
		if (!(gfp_mask & __GFP_WRITE) && shadow)
			workingset_refault(page, shadow);
		//加入LRU链表
		lru_cache_add(page);
	}
	return ret;
}

static int __add_to_page_cache_locked(struct page *page,
				      struct address_space *mapping,
				      pgoff_t offset, gfp_t gfp_mask,
				      void **shadowp)
{
	...
	get_page(page);
	//设置page的地址空间及偏移地址
	page->mapping = mapping;
	page->index = offset;

	xa_lock_irq(&mapping->i_pages);
	//插入页面到pagecache
	error = page_cache_tree_insert(mapping, page, shadowp);
	radix_tree_preload_end();
	...
	return 0;
	...
}

4 删除page cache

page cache的删除主要发生在内存回收时,有可能是用户主动调用sync类函数,或者内存不足,系统主动发起内存回收。

对于主动回收,我们考虑通过echo 1 > /proc/sys/vm/drop_caches的情况——vm内核参数之缓存回收drop_caches,我们这次看下针对page cache的部分,
大概逻辑就是,

  • 通过遍历超级块上的inode对象
  • 根据i_mapping指针找到对应的地址空间
  • 进而通过page cache基树找到对应的page
  • 然后将其从page cache中删除
  • 并调用对应文件系统的releasepage函数释放资源
  • 之后再将页面从活动LRU链表移动到非活动LRU链表
  • 最后是inode自身相关的处理,我们这里不关注
drop_pagecache_sb ->									//遍历该超级块的所有inode,并尝试释放
	invalidate_mapping_pages ->							//尝试释放该inode对应的page cache
		pagevec_lookup_entries							//根据地址空间查询该inode关联的所有page cache
		invalidate_inode_page ->						//尝试释放查询到的page cache页面
			invalidate_complete_page ->
				remove_mapping ->
					__remove_mapping ->
						__delete_from_page_cache ->
							page_cache_tree_delete   	//将该页面从page cache的基树中删除
						mapping->a_ops->freepage ->		//调用对应文件系统的releasepage函数,释放资源,比如buffer_head
							ext4_releasepage			//对于ext4调用的是ext4_releasepage
		deactivate_file_page							//把页面从活动LRU链表移动到非活动LRU链表
		pagevec_release ->								//释放页面
			__pagevec_release ->
				lru_add_drain							//先将各个pagevec刷到对应LRU链表
				release_pages							//然后针对上面inode的page cache减少页面引用计数,并回收0引用页面
	iput												//回收inode

而对于内存不足时系统回收page cache,我们以kswapd进程为例,它主要是以LRU链表为依据进行回收,page cache也在LRU链表中,具体可参考——kswapd进程工作原理(三)——回收LRU链表

5 结构关系

以读/home/test.c文件为例

  • 应用程序打开test.c文件,并通过fd文件描述符获得file结构体
  • file结构体中f_inode指向文件对应的inode结构
  • file结构体中f_path的dentry指向文件对应的dentry对象
  • dentry对象中的d_inode也指向该文件对应的inode结构
  • file结构体的f_mapping指向inode的i_mapping
  • inode的i_mapping则指向inode内嵌的address_space结构体
  • 并且address_space结构体也有一个变量——host,指向对应的inode结构
  • address_space结构体的i_pages指向page cache基树
  • page cache基树的结点中slot指针数组指向page对象
  • 此时count=2,即说名test.c文件在内存中有两个高速缓存页,page地址保存在slot指针数组中
  • page结构体中的mapping指向file结构体的f_mapping

所以从进程角度来看,是通过fd->file->address_space->radix_tree->page的逻辑,找到文件对应的page cache。
在这里插入图片描述

  • 4
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值