Linux 内存管理之address_space

文章目录

  • 简介
  • 一、address_space
    • [1.1 address_space](#1.1 address_space)
    • [1.2 struct page](#1.2 struct page)
    • [1.3 基数树](#1.3 基数树)
      • [1.3.1 radix_tree_root](#1.3.1 radix_tree_root)
      • [1.3.2 radix_tree_node](#1.3.2 radix_tree_node)
  • 二、find_get_page
    • [2.1 find_get_page用到场景](#2.1 find_get_page用到场景)
      • [2.1.1 generic_file_buffered_read](#2.1.1 generic_file_buffered_read)
      • [2.1.2 filemap_fault](#2.1.2 filemap_fault)
        • [2.1.2.1 file mmap](#2.1.2.1 file mmap)
        • [2.1.2.2 file filemap_fault](#2.1.2.2 file filemap_fault)
      • [2.1.3 do_read_cache_page](#2.1.3 do_read_cache_page)
    • [2.2 pagecache_get_page](#2.2 pagecache_get_page)
      • [2.2.1 __page_cache_alloc](#2.2.1 __page_cache_alloc)
      • [2.2.2 add_to_page_cache_lru](#2.2.2 add_to_page_cache_lru)
  • 三、相应数据结构关系图

简介

这篇文章介绍了page cache:Linux 内存管理之page cache,接下来介绍 page cache 的管理数据结构 address_space。

一、address_space

1.1 address_space

在Linux内核中,struct address_space是用于管理文件系统中的文件页缓存(page cache)和内存映射(Memory Mapping)文件的数据结构,主要用于文件系统和块设备的缓存管理。

一个address_space管理了一个文件在内存中缓存的所有pages,这部分缓存也是页高速缓存。address_space将属于同一文件的page联系起来,将这些page的操作方法与文件所属的文件系统联系起来。

大量子系统(文件系统、页交换、同步、缓存)都围绕地址空间的概念展开。因而,这个概念可以认为是内核最根本的抽象机制之一,以重要性而论,该抽象可跻身于传统抽象如进程、文件之列。

c 复制代码
// v4.15/source/include/linux/fs.h

struct address_space {
	struct inode		*host;		/* owner: inode, block_device */
	struct radix_tree_root	page_tree;	/* radix tree of all pages */
	spinlock_t		tree_lock;	/* and lock protecting it */
	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 tree_lock together with the radix tree */
	unsigned long		nrpages;	/* number of total pages */
	/* 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;	/* for use by the address_space */
	void			*private_data;	/* ditto */
	errseq_t		wb_err;
} __attribute__((aligned(sizeof(long)))) __randomize_layout;

address_space 作为文件(owner)与内存页的中间层,管理文件对应的所有缓存页。在page cache中,page cache中的每个page都有对应的文件,这个文件就是这个page的owner,address_space将属于同一owner的pages联系起来。

(1)struct inode *host:指向所属的 inode 或 block_device,标识缓存的所有者(如文件或块设备)。

(2)struct radix_tree_root page_tree:基数树,存储所有缓存的物理页(struct page),用于快速按偏移查找。通过 page_tree(基数树)根据文件偏移快速定位缓存页。

Linux 4.20 以后的内核版本使用 xarray 管理 page cache。

(3)atomic_t i_mmap_writable:统计共享内存映射(VM_SHARED)的数量。

(4)struct rb_root_cached i_mmap:红黑树,管理所有内存映射(包括私有和共享的 VMA)。通过 i_mmap 红黑树快速找到所有映射该文件的进程(用于内存回收等)。

(5)unsigned long nrpages:当前缓存的页数,受 tree_lock 保护。

(6)pgoff_t writeback_index:标记下次回写(Writeback)的起始文件偏移。

(7)const struct address_space_operations *a_ops:定义文件系统对页缓存的操作(如 readpage、writepage)。

1.2 struct page

c 复制代码
struct page {
			struct address_space *mapping;
			pgoff_t index;		/* Our offset within mapping. */
}

mapping:指向所属文件的 address_space。

index:表示该页在文件中的逻辑偏移(如文件第3页对应 index=2)。

与地址空间所管理的区域之间的关联,是通过以下两个成员建立的:一个指向inode实例(类型为struct inode)的指针指定了后备存储器,一个基数树的根(page_tree)列出了地址空间中所有的物理内存页。

1.3 基数树

内核使用了基数树来管理与一个地址空间相关的所有页。

基数树最广泛的用途是在内存管理代码中。用于跟踪后备存储的address_space结构包含一个基数树,用于跟踪与该映射关联的核心页面。除此之外,该树还允许内存管理代码快速查找脏页或正在写回的页等功能。

从大量数据的集合(页缓存)中快速获取单个数据元素(页),Linux也采用了基数树这种结构来管理页缓存中包含的页。

1.3.1 radix_tree_root

根据address_space的定义,我们很清楚radix_tree_root结构是每个基数树的的根结点:

c 复制代码
/* root tags are stored in gfp_mask, shifted by __GFP_BITS_SHIFT */
struct radix_tree_root {
	unsigned int		height;
	gfp_t			gfp_mask;
	struct radix_tree_node	__rcu *rnode;
};

(1)height指定了树的高度,即根结点之下结点的层次数目。根据该信息和每个结点的项数,内

核可以快速计算给定树中数据项的最大数目。如果没有足够的空间容纳新数据,可以据此对

树进行扩展。

(2)gfp_mask指定了从哪个内存域分配内存。

(3)rnode是一个指针,指向树的第一个结点。该结点的数据类型是radix_tree_node。

1.3.2 radix_tree_node

基数树的结点基本上由以下数据结构表示:

c 复制代码
#define RADIX_TREE_MAP_SHIFT	(CONFIG_BASE_SMALL ? 4 : 6)

#define RADIX_TREE_MAP_SIZE	(1UL << RADIX_TREE_MAP_SHIFT)
#define RADIX_TREE_MAP_MASK	(RADIX_TREE_MAP_SIZE-1)

#define RADIX_TREE_TAG_LONGS	\
	((RADIX_TREE_MAP_SIZE + BITS_PER_LONG - 1) / BITS_PER_LONG)

struct radix_tree_node {
	unsigned int	height;		/* Height from the bottom */
	unsigned int	count;
	union {
		struct radix_tree_node *parent;	/* Used when ascending tree */
		struct rcu_head	rcu_head;	/* Used when freeing node */
	};
	void __rcu	*slots[RADIX_TREE_MAP_SIZE];
	unsigned long	tags[RADIX_TREE_MAX_TAGS][RADIX_TREE_TAG_LONGS];
};

(1)slots

是一个void指针的数组,根据结点所在的层次,指向数据或其他结点。

每个树结点都可以进一步指向64个结点(或叶子),根据radix_tree_node中的slots数组可以

推断。该定义的直接后果是,每个结点中的数组长度都只能为2的幂。另外,基数树结点的大小只能在编译时定义(当然,树中结点的最大数目可以在运行时修改)。

(2)count

保存了该结点中已经使用的数组项的数目。各数组项从头开始填充,未使用的项

为NULL指针。

(3)tags

linux中radix tree的每个slot除了存放指针,还存放着标志page和磁盘文件同步状态的tag。如果page cache中一个page在内存中被修改后没有同步到磁盘,就说这个page是dirty的,此时tag就是PAGE_CACHE_DIRTY。如果正在同步,tag就是PAGE_CACHE_WRITEBACK。

包括地址空间和页树,但这些并不能让内核直接区分映射的干净页和脏页。在某些时候这种区分是本质性的,例如在将页回写到后备存储器、永久修改底层块设备上的数据时。早期的内核版本在address_space中提供了额外的链表,来列出脏页和干净页。原则上,内核当然可以扫描整个树,并过滤出具备适当状态的页,但这显然非常耗时。为此,基数树的每个结点都包含了额外的标记信息,用于指定结点中的每个页是否具有标记中指定的属性。例如,内核对带有脏页的结点使用了一个标记。在扫描脏页期间,没有该标记的结点即可跳过。这种方案是在简单、统一的数据结构(不需要显式的链表来保存不同状态的页)与快速搜索具备特定性质的页的方案之间的一个折中。

内核可以据此快速判断某个区域中是否有dirty page或正在write back的page,而无须扫描该区域中的所有pages。

当前支持如下三种标记:

c 复制代码
#define RADIX_TREE_MAX_TAGS 3
c 复制代码
/*
 * Radix-tree tags, for tagging dirty and writeback pages within the pagecache
 * radix trees
 */
#define PAGECACHE_TAG_DIRTY	0
#define PAGECACHE_TAG_WRITEBACK	1
#define PAGECACHE_TAG_TOWRITE	2

(1)PAGECACHE_TAG_DIRTY:用于标记脏页,即已经被修改但尚未同步到存储介质上的页面。

(2)PAGECACHE_TAG_WRITEBACK:用于标记正在进行写回操作的页,即已经触发了将脏页写回到存储介质的操作,但尚未完成。

(3)PAGECACHE_TAG_TOWRITE:用于标记待写入页,即需要将脏页写回到存储介质的页面。

Tags是一个二维数组,RADIX_TREE_MAX_TAGS为3代表的是3种tag类型,RADIX_TREE_TAG_LONGS定义slot数占用的long类型长度个数。加入这个值为64,也就是3*64的数组,这样每一行代表一种tag的集合,假如tag[0]代表PAGE_CACHE_DIRTY,假如我们查到当前节点的tags[0]值为1,那么它的子树节点就存在PAGE_CACHE_DIRTY节点,在我们查找PG_dirty的页面时,就不用遍历整个树了,可以提高查找效率。

标记信息保存在一个二维数组中(tags),它是radix_tree_node的一部分。数组的第一维区分不同的标记,而第二维包含了足够数量的unsigned long,使得对该结点中可能组织的每个页,都能分配到一个比特位。

这些标签可以在页缓存的基数树中的节点上进行设置和查询,以便标记和跟踪页面的状态。通过使用这些标签,可以更有效地管理页缓存中的脏页和写回页,以提高文件系统的性能和数据一致性。

radix_tree_tag_set用于对一个特定的页设置一个标志:

c 复制代码
void *radix_tree_tag_set(struct radix_tree_root *root,
			unsigned long index, unsigned int tag);

内核在位串中操作对应的位置,并将该比特位设置为1。在完成后,将自上而下扫描树,更新所有结点中的信息。为查找所有具备特定标记的页,内核仍然必须扫描整个树,但该操作现在可以被加速,首先可以过滤出至少有一页设置了该标志的所有子树。另外,这个操作还可以进一步加速,内核实际上无须逐比特位检查,只需要检查存储该标记的unsigned long中,是否有某个不为0即可。

Tag 与 Page Flags 的关系:

Radix Tree Tag Page Flag 用途
PAGECACHE_TAG_DIRTY PG_dirty 标识页内容已修改
PAGECACHE_TAG_WRITEBACK PG_writeback 标识页正在回写磁盘

为什么需要双重标记?

Radix Tree Tag:快速扫描大范围页状态(如 find_get_pages_tag())

Page Flag:原子操作单个页状态(如 TestClearPageDirty())

radix tree加上tag标记为了管理的方便,内核可以据此快速判断某个区域中是否有dirty page或正在write back的page,而无须扫描该区域中的所有pages。

二、find_get_page

c 复制代码
// v4.15/source/include/linux/pagemap.h

/**
 * find_get_page - find and get a page reference
 * @mapping: the address_space to search
 * @offset: the page index
 *
 * Looks up the page cache slot at @mapping & @offset.  If there is a
 * page cache page, it is returned with an increased refcount.
 *
 * Otherwise, %NULL is returned.
 */
static inline struct page *find_get_page(struct address_space *mapping,
					pgoff_t offset)
{
	return pagecache_get_page(mapping, offset, 0, 0);
}

find_get_page() 这个函数是用于查找并获取页缓存(page cache)中某个页面的引用,用于在给定的地址空间(address_space)中,根据页面偏移量(offset)查找对应的缓存页。

mapping的获取:

c 复制代码
	struct address_space *mapping = file->f_mapping
c 复制代码
	struct inode *inode = file_inode(file);
	struct address_space *mapping = inode->i_mapping;

2.1 find_get_page用到场景

2.1.1 generic_file_buffered_read

c 复制代码
/**
 * generic_file_read_iter - generic filesystem read routine
 * @iocb:	kernel I/O control block
 * @iter:	destination for the data read
 *
 * This is the "read_iter()" routine for all filesystems
 * that can use the page cache directly.
 */
ssize_t
generic_file_read_iter(struct kiocb *iocb, struct iov_iter *iter)
{
	generic_file_buffered_read(iocb, iter, retval);
}
c 复制代码
/**
 * generic_file_buffered_read - generic file read routine
 * @iocb:	the iocb to read
 * @iter:	data destination
 * @written:	already copied
 *
 * This is a generic file read routine, and uses the
 * mapping->a_ops->readpage() function for the actual low-level stuff.
 *
 * This is really ugly. But the goto's actually try to clarify some
 * of the logic when it comes to error handling etc.
 */

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;
  for (;;) {
    struct page *page;
    pgoff_t end_index;
    loff_t isize;
    page = find_get_page(mapping, index);
    if (!page) {
      if (iocb->ki_flags & IOCB_NOWAIT)
        goto would_block;
      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);
    }
    /*
     * Ok, we have the page, and it's up-to-date, so
     * now we can copy it to user space...
     */
    ret = copy_page_to_iter(page, offset, nr, iter);
    }
 
 	......
	 no_cached_page:
			/*
			 * Ok, it wasn't cached, so we need to create a new
			 * page..
			 */
			page = page_cache_alloc(mapping);
			if (!page) {
				error = -ENOMEM;
				goto out;
			}
			error = add_to_page_cache_lru(page, mapping, index,
					mapping_gfp_constraint(mapping, GFP_KERNEL));
			if (error) {
				put_page(page);
				if (error == -EEXIST) {
					error = 0;
					goto find_page;
				}
				goto out;
			}
			goto readpage;
		}
}

这段代码是 Linux 内核中 generic_file_buffered_read() 函数的简化版,主要用于实现缓冲文件读取(buffered I/O)。

(1)内核首先尝试从页缓存中直接读取数据(find_get_page),避免磁盘 I/O。

如果页不存在(!page),触发同步预读(page_cache_sync_readahead),预加载后续数据到缓存。

同步预读:当缓存未命中时,立即加载当前页和后续若干页。

如果同步预读(page_cache_sync_readahead)还是没有找到,则 跳往 no_cached_page,调用 page_cache_alloc分配一个新的page。

(2)如果第一次找缓存页就找到了,我们还是要判断,是不是应该继续预读;如果需要,就调用 page_cache_async_readahead 发起一个异步预读。

如果发现页标记了 PageReadahead,说明访问模式是顺序读取,后台异步预读更多数据(page_cache_async_readahead)。

page_cache_sync/async_readahead

(1)page_cache_sync_readahead:同步预读,在缓存未命中(需要读取新数据)时触发,直接提交读请求。同步执行,可能阻塞当前操作(等待 I/O)。

关键逻辑:

直接处理缓存未命中的请求。

根据访问模式(随机 / 顺序)选择不同的预读策略。

触发时机:缓存未命中(cache miss)时触发,即需要读取的数据不在页缓存中。

执行方式:同步执行,直接提交读请求,可能阻塞当前操作等待 I/O 完成。

预读目标:足当前读取请求,并预读少量后续数据。

应用场景:顺序读取或随机读取的初始阶段。

(2)page_cache_async_readahead:异步预读,当已预读的页面被使用(且标记了PG_readahead)时触发,提前加载更多数据,不阻塞当前操作。异步执行,不阻塞当前操作。

关键逻辑:

当页面的 PG_readahead 标记被触发时,表明应用正在快速消耗预读数据,需启动新一轮预读。

检查页面状态和系统负载,避免在不合适的时机预读。

触发时机:当已预读的页面被使用(且设置了 PG_readahead 标记)时触发。

执行方式:异步执行,不阻塞当前操作,在后台预读后续数据。

预读目标:预测未来可能需要的数据,提前加载到页缓存中。

应用场景:顺序读取的持续阶段(如大文件连续读取)。

这两个函数通过 ondemand_readahead 函数实现预读窗口的动态调整:

(1)顺序访问模式:

初始读取时,sync_readahead 触发,预读少量数据(如 1-2 页)。

当应用快速消耗这些预读页时,async_readahead 被触发,预读窗口按指数级增长(如 2 → 4 → 8 页)。

(2)随机访问模式:

sync_readahead 检测到随机访问标志(FMODE_RANDOM),仅预读当前请求所需的数据,不扩展预读窗口。

async_readahead 几乎不会被触发,避免无效预读。

2.1.2 filemap_fault

2.1.2.1 file mmap

(1)建立起虚拟地址内存到文件的映射关系。

c 复制代码
SYSCALL_DEFINE6(mmap_pgoff
	-->vm_mmap_pgoff()
		-->do_mmap_pgoff()
			-->do_mmap()
				-->get_unmapped_area()
				{
					get_area = current->mm->get_unmapped_area;
					if (file) {
						if (file->f_op->get_unmapped_area)
							get_area = file->f_op->get_unmapped_area;
				}
			 -->mmap_region()

调用 get_unmapped_area 找到一个没有映射的虚拟地址区域;

调用 mmap_region 映射这个虚拟地址区域。

(2)用户态缺页异常

一旦开始访问虚拟内存的某个地址,如果我们发现,并没有对应的物理页,那就触发缺页中断,调用 do_page_fault。

c 复制代码
do_page_fault()
	-->__do_page_fault()
		-->handle_mm_fault()
			-->__handle_mm_fault()
				-->handle_pte_fault()

handle_pte_fault有三种情况:

do_anonymous_page:匿名页的映射

do_fault:文件映射

do_swap_page:swap映射

这里我们只看 do_fault:文件映射。

c 复制代码
do_fault()
	-->__do_fault()
	{
		vma->vm_ops->fault(vmf);
	}

mmap 映射文件的时候,对于 ext4 文件系统,vm_ops 指向了 ext4_file_vm_ops,也就是调用了 ext4_filemap_fault。

c 复制代码
static const struct vm_operations_struct ext4_file_vm_ops = {
  .fault    = ext4_filemap_fault,
  .map_pages  = filemap_map_pages,
  .page_mkwrite   = ext4_page_mkwrite,
};

int ext4_filemap_fault(struct vm_fault *vmf)
{
  struct inode *inode = file_inode(vmf->vma->vm_file);
......
  err = filemap_fault(vmf);
......
  return err;
}
2.1.2.2 file filemap_fault
c 复制代码
/**
 * filemap_fault - read in file data for page fault handling
 * @vmf:	struct vm_fault containing details of the fault
 *
 * filemap_fault() is invoked via the vma operations vector for a
 * mapped memory region to read in file data during a page fault.
 *
 * The goto's are kind of ugly, but this streamlines the normal case of having
 * it in the page cache, and handles the special cases reasonably without
 * having a lot of duplicated code.
 *
 * vma->vm_mm->mmap_sem must be held on entry.
 *
 * If our return value has VM_FAULT_RETRY set, it's because
 * lock_page_or_retry() returned 0.
 * The mmap_sem has usually been released in this case.
 * See __lock_page_or_retry() for the exception.
 *
 * If our return value does not have VM_FAULT_RETRY set, the mmap_sem
 * has not been released.
 *
 * We never return with VM_FAULT_RETRY and a bit from VM_FAULT_ERROR set.
 */
 int filemap_fault(struct vm_fault *vmf)
{
  int error;
  struct file *file = vmf->vma->vm_file;
  struct address_space *mapping = file->f_mapping;
  struct inode *inode = mapping->host;
  pgoff_t offset = vmf->pgoff;
  struct page *page;
  int ret = 0;
......
  page = find_get_page(mapping, offset);
  if (likely(page) && !(vmf->flags & FAULT_FLAG_TRIED)) {
    do_async_mmap_readahead(vmf->vma, ra, file, page, offset);
  } else if (!page) {
    goto no_cached_page;
  }
......
  vmf->page = page;
  return ret | VM_FAULT_LOCKED;
no_cached_page:
  error = page_cache_read(file, offset, vmf->gfp_mask);
......
}

这段代码是 Linux 内核中处理文件内存映射(mmap)缺页异常的核心函数 filemap_fault(),属于虚拟内存子系统(VFS)的关键路径。它的作用是在进程访问文件映射区域触发 缺页异常(page fault) 时,将文件数据加载到物理内存。

(1)find_get_page

find_get_page 优先从页缓存查找目标页,避免磁盘 I/O。当首次命中页缓存时,调用do_async_mmap_readahead,后台异步预读后续文件内容(基于局部性原理),减少后续缺页异常的开销。

如果没有找到跳到 no_cached_page ,调用 page_cache_read。

do_async_mmap_readahead最常见的场景是处理文件支持的页面的缺页异常 (handle_mm_fault -> filemap_fault)。

(2)page_cache_read

c 复制代码
/**
 * page_cache_read - adds requested page to the page cache if not already there
 * @file:	file to read
 * @offset:	page index
 * @gfp_mask:	memory allocation flags
 *
 * This adds the requested page to the page cache if it isn't already there,
 * and schedules an I/O to read in its contents from disk.
 */
static int page_cache_read(struct file *file, pgoff_t offset, gfp_t gfp_mask)
{
	struct address_space *mapping = file->f_mapping;
	struct page *page;
	int ret;

	do {
		page = __page_cache_alloc(gfp_mask);
		if (!page)
			return -ENOMEM;

		ret = add_to_page_cache_lru(page, mapping, offset, gfp_mask & GFP_KERNEL);
		if (ret == 0)
			ret = mapping->a_ops->readpage(file, page);
		else if (ret == -EEXIST)
			ret = 0; /* losing race to add is OK */

		put_page(page);

	} while (ret == AOP_TRUNCATED_PAGE);

	return ret;
}

__page_cache_alloc:从内核内存分配器获取一个干净的页框。

add_to_page_cache_lru:将页插入页缓存和 LRU 链表。

然后在 address_space 中调用 address_space_operations 的 readpage 函数,将文件内容读到内存中。

struct address_space_operations 对于 ext4 文件系统的定义如下所示。这么说来,上面的 readpage 调用的其实是 ext4_readpage。

c 复制代码
static const struct address_space_operations ext4_aops = {
  .readpage    = ext4_readpage,
  .readpages    = ext4_readpages,
......
};

2.1.3 do_read_cache_page

暂无。

2.2 pagecache_get_page

c 复制代码
// v4.15/source/mm/filemap.c

/**
 * pagecache_get_page - find and get a page reference
 * @mapping: the address_space to search
 * @offset: the page index
 * @fgp_flags: PCG flags
 * @gfp_mask: gfp mask to use for the page cache data page allocation
 *
 * Looks up the page cache slot at @mapping & @offset.
 *
 * PCG flags modify how the page is returned.
 *
 * @fgp_flags can be:
 *
 * - FGP_ACCESSED: the page will be marked accessed
 * - FGP_LOCK: Page is return locked
 * - FGP_CREAT: If page is not present then a new page is allocated using
 *   @gfp_mask and added to the page cache and the VM's LRU
 *   list. The page is returned locked and with an increased
 *   refcount. Otherwise, NULL is returned.
 *
 * If FGP_LOCK or FGP_CREAT are specified then the function may sleep even
 * if the GFP flags specified for FGP_CREAT are atomic.
 *
 * If there is a page cache page, it is returned with an increased refcount.
 */
struct page *pagecache_get_page(struct address_space *mapping, pgoff_t offset,
	int fgp_flags, gfp_t gfp_mask)
{
	struct page *page;

repeat:
	page = find_get_entry(mapping, offset);
	if (radix_tree_exceptional_entry(page))
		page = NULL;
	if (!page)
		goto no_page;

	if (fgp_flags & FGP_LOCK) {
		if (fgp_flags & FGP_NOWAIT) {
			if (!trylock_page(page)) {
				put_page(page);
				return NULL;
			}
		} else {
			lock_page(page);
		}

		/* Has the page been truncated? */
		if (unlikely(page->mapping != mapping)) {
			unlock_page(page);
			put_page(page);
			goto repeat;
		}
		VM_BUG_ON_PAGE(page->index != offset, page);
	}

	if (page && (fgp_flags & FGP_ACCESSED))
		mark_page_accessed(page);

no_page:
	if (!page && (fgp_flags & FGP_CREAT)) {
		int err;
		if ((fgp_flags & FGP_WRITE) && mapping_cap_account_dirty(mapping))
			gfp_mask |= __GFP_WRITE;
		if (fgp_flags & FGP_NOFS)
			gfp_mask &= ~__GFP_FS;

		page = __page_cache_alloc(gfp_mask);
		if (!page)
			return NULL;

		if (WARN_ON_ONCE(!(fgp_flags & FGP_LOCK)))
			fgp_flags |= FGP_LOCK;

		/* Init accessed so avoid atomic mark_page_accessed later */
		if (fgp_flags & FGP_ACCESSED)
			__SetPageReferenced(page);

		err = add_to_page_cache_lru(page, mapping, offset,
				gfp_mask & GFP_RECLAIM_MASK);
		if (unlikely(err)) {
			put_page(page);
			page = NULL;
			if (err == -EEXIST)
				goto repeat;
		}
	}

	return page;
}
EXPORT_SYMBOL(pagecache_get_page);
c 复制代码
// v4.15/source/include/linux/pagemap.h

#define FGP_ACCESSED		0x00000001
#define FGP_LOCK		0x00000002
#define FGP_CREAT		0x00000004
#define FGP_WRITE		0x00000008
#define FGP_NOFS		0x00000010
#define FGP_NOWAIT		0x00000020

(1)页面查找阶段

c 复制代码
repeat:
page = find_get_entry(mapping, offset);  // 查找页缓存条目
if (radix_tree_exceptional_entry(page))  // 特殊条目处理
    page = NULL;
if (!page)
    goto no_page;  // 未找到跳转

(2)页面存在时的处理

c 复制代码
// 锁定处理
if (fgp_flags & FGP_LOCK) {
    if (fgp_flags & FGP_NOWAIT) {
        if (!trylock_page(page)) {  // 非阻塞尝试锁
            put_page(page);
            return NULL;
        }
    } else {
        lock_page(page);  // 阻塞锁定
    }
    
    // 检查页面截断
    if (unlikely(page->mapping != mapping)) {
        unlock_page(page);
        put_page(page);
        goto repeat;  // 重新查找
    }
    VM_BUG_ON_PAGE(page->index != offset, page);  // 调试检查
}

// 标记访问状态
if (page && (fgp_flags & FGP_ACCESSED))
    mark_page_accessed(page);  // 更新访问状态

当页面被访问时,内核调用 mark_page_accessed(),mark_page_accessed() 函数负责管理页面的活跃状态标记(PG_referenced 和 PG_active)。

标记页面为"被访问过",参与 LRU 链表的活跃度管理。

关键行为:

若页面首次被访问,设置 PG_referenced 标志。

对应状态转换:inactive,unreferenced → inactive,referenced。

若页面已被访问过(PG_referenced 已设置)且可回收,则将其激活(移到 Active List)。

对应状态转换:inactive,referenced → active,unreferenced。

(3)页面不存在时的创建处理

c 复制代码
no_page:
if (!page && (fgp_flags & FGP_CREAT)) {
    // 调整分配标志
    if ((fgp_flags & FGP_WRITE) && mapping_cap_account_dirty(mapping))
        gfp_mask |= __GFP_WRITE;  // 可写页面优化
    if (fgp_flags & FGP_NOFS)
        gfp_mask &= ~__GFP_FS;    // 禁止文件系统操作

    page = __page_cache_alloc(gfp_mask);  // 分配新页
    if (!page)
        return NULL;
    
    // 安全保护:确保新页被锁定
    if (WARN_ON_ONCE(!(fgp_flags & FGP_LOCK)))
        fgp_flags |= FGP_LOCK;
    
    // 预标记访问状态
    if (fgp_flags & FGP_ACCESSED)
        __SetPageReferenced(page);
    
    // 添加到页缓存
    err = add_to_page_cache_lru(page, mapping, offset, 
                               gfp_mask & GFP_RECLAIM_MASK);
    if (unlikely(err)) {
        put_page(page);
        page = NULL;
        if (err == -EEXIST)  // 处理竞争条件
            goto repeat;     // 页面已被其他线程创建
    }
}

如果在page cache中未找到,就会触发page fault,然后调用__page_cache_alloc在内存中分配若干物理页面,最后将数据从磁盘对应位置复制到内存;

调用__page_cache_alloc分配一个新的page,然后调用add_to_page_cache_lru将其加入到缓存page cache 和 LRU链表里面。

2.2.1 __page_cache_alloc

c 复制代码
#ifdef CONFIG_NUMA
struct page *__page_cache_alloc(gfp_t gfp)
{
	int n;
	struct page *page;

	if (cpuset_do_page_mem_spread()) {
		unsigned int cpuset_mems_cookie;
		do {
			cpuset_mems_cookie = read_mems_allowed_begin();
			n = cpuset_mem_spread_node();
			page = __alloc_pages_node(n, gfp, 0);
		} while (!page && read_mems_allowed_retry(cpuset_mems_cookie));

		return page;
	}
	return alloc_pages(gfp, 0);
}
EXPORT_SYMBOL(__page_cache_alloc);
#endif

调用__alloc_pages_node/alloc_pages 分配新的页。

2.2.2 add_to_page_cache_lru

c 复制代码
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);
	ret = __add_to_page_cache_locked(page, mapping, offset,
					 gfp_mask, &shadow);
	if (unlikely(ret))
		__ClearPageLocked(page);
	else {
		/*
		 * The page might have been evicted from cache only
		 * recently, in which case it should be activated like
		 * any other repeatedly accessed page.
		 * The exception is pages getting rewritten; evicting other
		 * data from the working set, only to cache data that will
		 * get overwritten with something else, is a waste of memory.
		 */
		if (!(gfp_mask & __GFP_WRITE) &&
		    shadow && workingset_refault(shadow)) {
			SetPageActive(page);
			workingset_activation(page);
		} else
			ClearPageActive(page);
		lru_cache_add(page);
	}
	return ret;
}
EXPORT_SYMBOL_GPL(add_to_page_cache_lru);

add_to_page_cache_lru 是 Linux 内存管理核心函数,负责将新分配的页面添加到页缓存(page cache)并纳入 LRU链表管理。

这个函数就说明 page cache 在两个数据结构里面管理:

(1)address_space里的基数树(高版本变为xarray)

c 复制代码
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->mapping = mapping;
	page->index = offset;
	......
	page_cache_tree_insert(mapping, page, shadowp);
	.......
}

__add_to_page_cache_locked 执行实际的基数树插入操作。

(2)LRU链表

c 复制代码
/**
 * lru_cache_add - add a page to a page list
 * @page: the page to be added to the LRU.
 *
 * Queue the page for addition to the LRU via pagevec. The decision on whether
 * to add the page to the [in]active [file|anon] list is deferred until the
 * pagevec is drained. This gives a chance for the caller of lru_cache_add()
 * have the page added to the active list using mark_page_accessed().
 */
void lru_cache_add(struct page *page)
{
	VM_BUG_ON_PAGE(PageActive(page) && PageUnevictable(page), page);
	VM_BUG_ON_PAGE(PageLRU(page), page);
	__lru_cache_add(page);
}
c 复制代码
static void __lru_cache_add(struct page *page)
{
	struct pagevec *pvec = &get_cpu_var(lru_add_pvec);

	get_page(page);
	if (!pagevec_add(pvec, page) || PageCompound(page))
		__pagevec_lru_add(pvec);
	put_cpu_var(lru_add_pvec);
}

将page cache添加到LRU链表。

三、相应数据结构关系图

1、每个adrres_space对象对应一颗搜索树。他们之间的联系是通过address_space对象中的page_tree字段指向该address_space对象对应的基树。

c 复制代码
struct address_space {
	......
	struct radix_tree_root	page_tree;	/* radix tree of all pages */
	......
}

2、一个inode节点对应一个address_space对象,其中inode节点对象的i_mapping和i_data字段指向相应的 address_space对象,而address_space对象的host字段指向对应的inode节点对象。

c 复制代码
struct inode {
	......
	struct address_space	*i_mapping;
	......
	struct address_space	i_data;
	......	
}
c 复制代码
struct address_space {
	struct inode		*host;		/* owner: inode, block_device */
	......
}

索引节点的i_mapping字段总是指向索引节点的数据页所有者的address_space对象,address_space对象的host字段指向其所有者的索引节点对象。

因此,如果页属于一个文件(保存在ext4文件系统中),那么页的所有者就是文件的索引节点,而且相应的address_space对象存放在VFS索引节点对象的i_data字段中。索引节点的i_mapping字段指向同一个索引节点的i_data字段,而address_space对象的host字段也只想索引节点。

3、一般情况下一个inode节点对象对应的文件或者是块设备都会包含多个页面的内容,所以一个inode对象对应多个page描述符。同一个文件拥有的所有page描述符都可以在该文件对应的基树中找到。

c 复制代码
struct page {
	/* First double word block */
	......
	struct address_space *mapping;	/* If low bit clear, points to
					 * inode address_space, or NULL.
					 * If page mapped as anonymous
					 * memory, low bit is set, and
					 * it points to anon_vma object:
					 * see PAGE_MAPPING_ANON below.
	......

mapping指定了页帧所在的地址空间address_space。地址空间用于将文件的内容(数据)与装载数据的内存区关联起来。通过一个小技巧,mapping不仅能够保存一个指针,而且还能包含一些额外的信息,用于判断页是否属于未关联到地址空间的某个匿名内存区。如果将mapping置为1,则该指针并不指向address_space的实例,而是指向另一个数据结构(anon_vma),该结构对实现匿名页的逆向映射很重要。对该指针的双重使用是可能的,因为address_space实例总是对齐到sizeof(long)。因此在Linux支持的所有计算机上,指向该实例的指针最低位总是0。该指针如果指向address_space实例,则可以直接使用。如果使用了技巧将最低位设置为1,内核可使用下列操作恢复来恢复指针:

anon_vma = (struct anon_vma *) (mapping -PAGE_MAPPING_ANON)

它们之间的关系如下图:

相关推荐
花嫁代二娃13 分钟前
Linux:环境变量
linux
秋说3 小时前
【PTA数据结构 | C语言版】一元多项式求导
c语言·数据结构·算法
暮鹤筠4 小时前
[C语言初阶]操作符
c语言·开发语言
l1x1n05 小时前
Vim 编辑器常用操作详解(新手快速上手指南)
linux·编辑器·vim
ajassi20007 小时前
开源 python 应用 开发(三)python语法介绍
linux·python·开源·自动化
o不ok!7 小时前
Linux面试问题-软件测试
linux·运维·服务器
DaxiaLeeSuper7 小时前
Prometheus+Grafana+node_exporter监控linux服务器资源的方案
linux·grafana·prometheus
尽兴-8 小时前
如何将多个.sql文件合并成一个:Windows和Linux/Mac详细指南
linux·数据库·windows·sql·macos
kfepiza8 小时前
Netplan 中 bridges、bonds、ethernets、vlans 之间的关系 笔记250711
linux·tcp/ip·shell
小小不董9 小时前
深入理解oracle ADG和RAC
linux·服务器·数据库·oracle·dba