Linux 内存管理:匿名内存映射简析

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 匿名内存映射的典型场景

本文以 ARMv7 + Linux 4.14.x 为上下文,分析匿名内存映射的典型场景下,代码实现的概要细节。

2.1 只读内存匿名映射过程

用户空间分配内存,而后进行读操作,映射为 zero page:

size_t size = 1 * 1024 * 1024;
char *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, 
		MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
char c = ptr[0]; // 读操作触发 page fault
munmap(ptr, size);

语句 char c = ptr[0]; 会触发硬件 page fault,从而进入读操作匿名内存页的映射过程。ARMv7 架构的处理流程如下:

do_DataAbort()
	...
	do_page_fault()
		__do_page_fault()
			handle_mm_fault()
				__handle_mm_fault()
					handle_pte_fault()
static int handle_pte_fault(struct vm_fault *vmf)
{
	...
	if (unlikely(pmd_none(*vmf->pmd))) {
		/*
		 * Leave __pte_alloc() until later: because vm_ops->fault may
		 * want to allocate huge page, and if we expose page table
		 * for an instant, it will be difficult to retract from
		 * concurrent faults and from rmap lookups.
		 */
		vmf->pte = NULL;
	} else {
		...
	}

	if (!vmf->pte) { /* @vmf 的 PTE 页表项 还未建立,即还未分配物理页面 */
		if (vma_is_anonymous(vmf->vma))
			return do_anonymous_page(vmf); /* 匿名映射 */
		else
			return do_fault(vmf);
	}

	...
}

static int do_anonymous_page(struct vm_fault *vmf)
{
	...
	/* Use the zero-page for reads */
	if (!(vmf->flags & FAULT_FLAG_WRITE) &&
			!mm_forbids_zeropage(vma->vm_mm)) {
		entry = pte_mkspecial(pfn_pte(my_zero_pfn(vmf->address),
						vma->vm_page_prot));
		...
		goto setpte;
	}

	...

setpte:
	set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);
	...
}

上面的代码中,my_zero_pfn() 选择将读地址映射到 zero page。

从硬件 MMU 可以得知,当前的 page fault 是由读还是写引起的,从而去决定是否设置 FAULT_FLAG_WRITE 标志位。如 ARMv7 架构下,发生 page fault 时,从寄存器 DFSR(Data Fault Status Register) 获知是读还是写:

c7be8abe-20b1-4e03-89a8-523be13cf0a0

75226da1-7f00-4177-a07a-32c9f8f7b4f0

2.2 只写内存匿名映射过程

用户空间分配内存,而后进行写操作,新分配物理内存并映射:

size_t size = 1 * 1024 * 1024;
char *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, 
		MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
ptr[0] = 0xAA; // 写操作触发 page fault
munmap(ptr, size);

语句 ptr[0] = 0xAA; 会触发硬件 page fault,从而进入写操作匿名内存页的映射过程。ARMv7 架构的处理流程如下:

do_DataAbort()
	...
	do_page_fault()

static int __kprobes
do_page_fault(unsigned long addr, unsigned int fsr, struct pt_regs *regs)
{
	...
	unsigned int flags = FAULT_FLAG_ALLOW_RETRY | FAULT_FLAG_KILLABLE;

	...

	/* 从 DFSR 寄存器得知是写操作引发的 page fault,则设置 FAULT_FLAG_WRITE 标志位 */
	if (fsr & FSR_WRITE)
		flags |= FAULT_FLAG_WRITE;

	...

	fault = __do_page_fault(mm, addr, fsr, flags, tsk);

	...
}

__do_page_fault()
	handle_mm_fault()
		__handle_mm_fault()
			handle_pte_fault()
				do_anonymous_page()
static int do_anonymous_page(struct vm_fault *vmf)
{
	...
	struct page *page;
	...

	...

	/* 分配新内存页面 */
	page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
	if (!page)
		goto oom;

	...

	__SetPageUptodate(page);

	entry = mk_pte(page, vma->vm_page_prot); /* 构建 pte */
	if (vma->vm_flags & VM_WRITE)
		entry = pte_mkwrite(pte_mkdirty(entry)); /* 设置页面可写 */
	...

setpte:
	set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry); /* 填充 PTE 页表项 */
	...
}

2.3 COW 匿名映射过程

2.3.1 先读后写内存匿名映射过程

用户空间分配内存,先读而后进行写操作,将引发写时拷贝(COW):

size_t size = 1 * 1024 * 1024;
char *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, 
		MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
char c = ptr[0]; // 读操作触发 page fault,过程见 2.1 只读内存匿名映射过程
ptr[0] = 0xAA; // 对只读内存进行写操作,触发 COW
munmap(ptr, size);

语句 char c = ptr[0]; 触发 page fault,将虚拟地址映射到只读的 zero page,过程见 2.1 只读内存匿名映射过程,这里不再赘述。语句 ptr[0] = 0xAA; 写只读内存,将触发写时拷贝(COW):

do_DataAbort()
	...
	do_page_fault()
		__do_page_fault()
			handle_mm_fault()
				__handle_mm_fault()
					handle_pte_fault()

static int handle_pte_fault(struct vm_fault *vmf)
{
	pte_t entry;

	if (unlikely(pmd_none(*vmf->pmd))) {
		...
	} else {
		...
		vmf->pte = pte_offset_map(vmf->pmd, vmf->address);
		vmf->orig_pte = *vmf->pte;
		...
	}

	...

	vmf->ptl = pte_lockptr(vmf->vma->vm_mm, vmf->pmd);
	spin_lock(vmf->ptl);
	entry = vmf->orig_pte;
	if (unlikely(!pte_same(*vmf->pte, entry)))
		goto unlock;
	if (vmf->flags & FAULT_FLAG_WRITE) { /* 请求发起写操作 */
		if (!pte_write(entry)) /* 但页面不允许写 */
			return do_wp_page(vmf);/* 做写时拷贝(COW: Copy-On-Write) */
		entry = pte_mkdirty(entry);
	}
	entry = pte_mkyoung(entry);
	...
unlock:
	pte_unmap_unlock(vmf->pte, vmf->ptl);
	return 0;
}

static int do_wp_page(struct vm_fault *vmf)
	__releases(vmf->ptl)
{
	...
	return wp_page_copy(vmf);
}

2.3.2 父子进程写 COW 匿名映射过程

size_t size = 1 * 1024 * 1024;
char *ptr = mmap(NULL, size, PROT_READ | PROT_WRITE, 
		MAP_PRIVATE | MAP_ANONYMOUS, 0, 0);
// (1) 写操作触发 page fault,分配内存页面
ptr[0] = 'A';
fork();
// (2.1) 父进程写触发 page fault,
// (2.2) 
ptr[0] = 'B';
  • 父进程写触发 COW

假定父进程先执行 ptr[0] = 'B';,在写入过程中,会触发 page fault 并进行 COW:

static int do_wp_page(struct vm_fault *vmf)
	__releases(vmf->ptl)
{
	struct vm_area_struct *vma = vmf->vma;

	vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
	...

	/*
	 * Ok, we need to copy. Oh, well..
	 */
	get_page(vmf->page);

	pte_unmap_unlock(vmf->pte, vmf->ptl);
	return wp_page_copy(vmf);
}

static int wp_page_copy(struct vm_fault *vmf)
{
	...
	if (is_zero_pfn(pte_pfn(vmf->orig_pte))) { /* 旧页面 是 zero page, 直接分配清 0 的新页面即可, 无需拷贝 */
		...
	}  else { /* 旧页面 非 zero page, 分配新页面, 并拷贝旧页面内容 */
		/* 为(父进程)分配新页面 */
		new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma,
				vmf->address);
		if (!new_page)
			goto oom;
		/* 拷贝 (父进程) 旧页面内容 到 新分配的页面 */
		cow_user_page(new_page, old_page, vmf->address, vma);
	}

	...

	/*
	 * Re-check the pte - we dropped the lock
	 */
	vmf->pte = pte_offset_map_lock(mm, vmf->pmd, vmf->address, &vmf->ptl);
	if (likely(pte_same(*vmf->pte, vmf->orig_pte))) {
		...
		flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
		/* 为(父进程)构建新的 PTE 页表项内容 */
		entry = mk_pte(new_page, vma->vm_page_prot);
		entry = maybe_mkwrite(pte_mkdirty(entry), vma);
		...
		/* 用新构建的 PTE 填充 PTE 页表项 */
		set_pte_at_notify(mm, vmf->address, vmf->pte, entry)
		...
		/* Free the old page.. */
		new_page = old_page;
		page_copied = 1;
	}  else {
		...
	}

	...
	return page_copied ? VM_FAULT_WRITE : 0;
	...
}

从上面的分析看到,为父进程创建了新的页面(假定父进程先执行ptr[0] = 'B';),然后拷贝旧页面内容,并填充了 PTE 页表项。

  • 子进程写触发 COW

假定子进程后执行 ptr[0] = 'B';,在写入过程中,会触发 page fault 并进行 COW:

static int do_wp_page(struct vm_fault *vmf)
	__releases(vmf->ptl)
{
	struct vm_area_struct *vma = vmf->vma;

	vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
	...

	/*
	 * Take out anonymous pages first, anonymous shared vmas are
	 * not dirty accountable.
	 */
	if (PageAnon(vmf->page) && !PageKsm(vmf->page)) {
		...
		if (!trylock_page(vmf->page)) {
			...
		}
		if (reuse_swap_page(vmf->page, &total_map_swapcount)) {
			...
			/* 子进程 重用 fork 时 父进程 的 旧页面 */
			wp_page_reuse(vmf);
			return VM_FAULT_WRITE;
		}
	}  else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
					(VM_WRITE|VM_SHARED))) {
		...
	}
}

static inline void wp_page_reuse(struct vm_fault *vmf)
	__releases(vmf->ptl)
{
	struct vm_area_struct *vma = vmf->vma;
	struct page *page = vmf->page;
	pte_t entry;
	/*
	 * Clear the pages cpupid information as the existing
	 * information potentially belongs to a now completely
	 * unrelated process.
	 */
	if (page)
		page_cpupid_xchg_last(page, (1 << LAST_CPUPID_SHIFT) - 1);

	flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));
	entry = pte_mkyoung(vmf->orig_pte);
	entry = maybe_mkwrite(pte_mkdirty(entry), vma);
	if (ptep_set_access_flags(vma, vmf->address, vmf->pte, entry, 1))
		update_mmu_cache(vma, vmf->address, vmf->pte);
	pte_unmap_unlock(vmf->pte, vmf->ptl);
}

上面分析假定父进程先执行语句 ptr[0] = 'B';,此时父进程新建页面,而子进程通过 wp_page_reuse() 重用旧页面。对于子进程先执行语句 ptr[0] = 'B'; 的情形,不过父子进程互换一下角色,即为 fork 后先写的子进程分配新页面,拷贝旧页面内容,而父进程重用旧页面。还有一点值得注意的是,如果没有前面的 ptr[0] = 'A'; 语句,则在创建新页面时无需进行拷贝操作,只需要创建清 0 的新页面即可,详见代码 is_zero_pfn() 判定处。

posted @ 2026-01-05 01:12  JiMoKuangXiangQu  阅读(28)  评论(0)    收藏  举报