重写-linux内存管理缺页异常分析(下)


上一节我们讲到下面这几种情况的处理,但是没有继续往下看,现在我们继续:

  1. 巨型页缺页:hugetlb_fault
  2. 匿名页缺页:do_anonymous_page
  3. 文件页缺页:do_fault
  4. 页在交换分区中:do_swap_page
  5. 也在其他内存节点中:do_numa_page
  6. 写时复制:do_wp_page

1. hugetlb_fault

2. do_anonymous_page

触发匿名页的缺页异常的情况:

  1. 函数的局部变量比较大,或者函数调用的层次比较深,导致当前栈不够用,需要扩大栈。
  2. 进程调用 malloc ,从堆申请了内存块,只分配了虚拟内存区域,还没有映射到物理页,第一次访问时触发缺页异常。
  3. 进程直接调用 mmap ,创建匿名的内存映射,只分配了虚拟内存区域,还没有映射到物理页,第一次访问时触发缺页异常。

然后看代码:

static vm_fault_t do_anonymous_page(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	struct page *page;
	vm_fault_t ret = 0;
	pte_t entry;

	if (vma->vm_flags & VM_SHARED)//如果是共享的匿名映射(正常的共享映射是文件映射的)
		return VM_FAULT_SIGBUS;

	/*
	 * Use pte_alloc() instead of pte_alloc_map().  We can't run
	 * pte_offset_map() on pmds where a huge pmd might be created
	 * from a different thread.
	 *
	 * pte_alloc_map() is safe to use under mmap_write_lock(mm) or when
	 * parallel threads are excluded by other means.
	 *
	 * Here we only have mmap_read_lock(mm).
	 */
	if (pte_alloc(vma->vm_mm, vmf->pmd))//如果直接页表不存在,那么分配页表。
		return VM_FAULT_OOM;

	/* See the comment in pte_alloc_one_map() */
	if (unlikely(pmd_trans_unstable(vmf->pmd)))
		return 0;

	/* 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));//生成特殊的页表项,映射到专用的零页
		vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
				vmf->address, &vmf->ptl);//在直接页表中查找虚拟地址对应的表项,并且锁住页表
		if (!pte_none(*vmf->pte)) {//如果页表项不是空表项,说明其他cpu在处理,需要等待他完成
			update_mmu_tlb(vma, vmf->address, vmf->pte);
			goto unlock;
		}
		ret = check_stable_address_space(vma->vm_mm);//检查给定的内存是否从用户拷贝过来的
		if (ret)
			goto unlock;//从用户拷贝过来的内存不稳定,不用处理
		//userfaultfd(用户页错误文件描述符)作用是解决QEMU/KVM虚拟机动态迁移的问题,我们不看
		if (userfaultfd_missing(vma)) {
			pte_unmap_unlock(vmf->pte, vmf->ptl);
			return handle_userfault(vmf, VM_UFFD_MISSING);
		}
		goto setpte;//跳转到标号 setpte 去设置页表项
	}

	/* Allocate our own private page. */
	if (unlikely(anon_vma_prepare(vma)))//初始化vma中的anon_vma_chain和anon_vma
		goto oom;
	//分配物理页,优先从高端内存区域分配,并且用零初始化。
	page = alloc_zeroed_user_highpage_movable(vma, vmf->address);
	if (!page)
		goto oom;

	if (mem_cgroup_charge(page, vma->vm_mm, GFP_KERNEL))//cgroup相关,不看
		goto oom_free_page;
	cgroup_throttle_swaprate(page, GFP_KERNEL);

	/*
	 * The memory barrier inside __SetPageUptodate makes sure that
	 * preceding stores to the page contents become visible before
	 * the set_pte_at() write.
	 */
	__SetPageUptodate(page);//设置页描述符的标志位 PG_uptodate ,表示物理页包含有效的数据

	entry = mk_pte(page, vma->vm_page_prot);//使用页帧号和访问权限生成页表项。
	entry = pte_sw_mkyoung(entry);//直接返回entry,不知道有什么用
	if (vma->vm_flags & VM_WRITE)//如果虚拟内存区域有写权限,
		//设置页表项的脏标志位和写权限,脏标志位表示页的数据被修改过
		entry = pte_mkwrite(pte_mkdirty(entry));
	//在直接页表中查找虚拟地址对应的表项,并且锁住页表。
	vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
			&vmf->ptl);
	if (!pte_none(*vmf->pte)) {//如果页表项不是空表项,说明其他处理器可能正在修改同一个页表项,
		update_mmu_cache(vma, vmf->address, vmf->pte);
		goto release;//处理器只需要等着使用其他处理器设置的页表项,没必要继续处理页错误异常。
	}

	ret = check_stable_address_space(vma->vm_mm);//检查给定的内存是否从用户拷贝过来的
	if (ret)
		goto release;//从用户拷贝过来的内存不稳定,不用处理

	//userfaultfd(用户页错误文件描述符)作用是解决QEMU/KVM虚拟机动态迁移的问题,我们不看
	if (userfaultfd_missing(vma)) {
		pte_unmap_unlock(vmf->pte, vmf->ptl);
		put_page(page);
		return handle_userfault(vmf, VM_UFFD_MISSING);
	}

	inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);//task或者mm的rss_stat的匿名页数量加一
	page_add_new_anon_rmap(page, vma, vmf->address, false);//建立物理页到虚拟页的反向映射
	lru_cache_add_inactive_or_unevictable(page, vma);//把物理页添加到活动LRU链表或者不可回收LRU链表 
setpte:
	set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);//设置页表项

	/* No need to invalidate - it was non-present before */
	update_mmu_cache(vma, vmf->address, vmf->pte);//更新处理器的页表缓存
unlock:
	pte_unmap_unlock(vmf->pte, vmf->ptl);//释放页表的锁
	return ret;
release:
	put_page(page);
	goto unlock;
oom_free_page:
	put_page(page);
oom:
	return VM_FAULT_OOM;
}

do_anonymous_page首先判断一下匿名页是否是共享的,如果是共享的匿名映射,但是虚拟内存区域没有提供虚拟内存操作集合
就返回错误;然后判断一下pte页表是否存在,如果直接页表不存在,那么分配页表;接下来判读缺页异常是由读操作触发的还是写操作触发的,如果是读操作触发的,生成特殊的页表项,映射到专用的零页,设置页表项后返回;如果是写操作触发的,需要初始化vma中的anon_vma_chain和anon_vma,分配物理页用于匿名映射,调用mk_pte函数生成页表项,设置页表项的脏标志位和写权限,设置页表项后返回。

3. do_fault

触发文件页的缺页异常的情况:

  1. 启动程序的时候,内核为程序的代码段和数据段创建私有的文件映射,映射到进程的虚拟地址空间,第一次访问的时候触发文件页的缺页异常。
  2. 进程使用mmap创建文件映射,把文件的一个区间映射到进程的虚拟地址空间,第一次访问的时候触发文件页的缺页异常。
    然后看代码:
static vm_fault_t do_fault(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	struct mm_struct *vm_mm = vma->vm_mm;
	vm_fault_t ret;

	if (!vma->vm_ops->fault) {//如果虚拟内存区域没有提供处理页错误异常的方法,返回错误
		if (unlikely(!pmd_present(*vmf->pmd)))
			ret = VM_FAULT_SIGBUS;
		else {
			vmf->pte = pte_offset_map_lock(vmf->vma->vm_mm,
						       vmf->pmd,
						       vmf->address,
						       &vmf->ptl);
			/*
			 * Make sure this is not a temporary clearing of pte
			 * by holding ptl and checking again. A R/M/W update
			 * of pte involves: take ptl, clearing the pte so that
			 * we don't have concurrent modification by hardware
			 * followed by an update.
			 */
			if (unlikely(pte_none(*vmf->pte)))
				ret = VM_FAULT_SIGBUS;
			else
				ret = VM_FAULT_NOPAGE;

			pte_unmap_unlock(vmf->pte, vmf->ptl);
		}
	} 
	else if (!(vmf->flags & FAULT_FLAG_WRITE))//如果缺页异常是由读文件页触发的
		ret = do_read_fault(vmf);//处理读文件页错误
	else if (!(vma->vm_flags & VM_SHARED))//如果缺页异常是由写私有文件页触发的
		ret = do_cow_fault(vmf);//处理写私有文件页错误,执行写时复制
	else//缺页异常是由写共享文件页触发的
		ret = do_shared_fault(vmf);//处理写共享文件页错误

	if (vmf->prealloc_pte) {//如果预分配的分页未使用
		pte_free(vm_mm, vmf->prealloc_pte);//释放预分配的页表项
		vmf->prealloc_pte = NULL;
	}
	return ret;
}

do_fault如果没有提供处理页错误异常的方法,直接返回错误;如果缺页异常是由读文件页触发的,调用 do_read_fault处理读文件页错误;如果缺页异常是由写私有文件页触发的,调用do_cow_fault处理写私有文件页错误,执行写时复制;否则缺页异常就是由写共享文件页触发的,调用do_shared_fault处理写共享文件页错误。最后查看vmf->prealloc_pte,如果没有开启预分配,则释放预分配的页表项。我们主要看读文件错误do_read_fault、写私有文件错误do_cow_fault和写共享文件错误do_shared_fault。

3.1 do_read_fault

处理读文件页错误的方法如下:

  1. 把文件页从存储设备上的文件系统读到文件的页缓存(每个文件有一个缓存,因为以页为单位,所以称为页缓存)中。
  2. 设置进程的页表项,把虚拟页映射到文件的页缓存中的物理页。
static vm_fault_t do_read_fault(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	vm_fault_t ret = 0;

	//调用map_pages映射到物理页,如果需要预读
	if (vma->vm_ops->map_pages && fault_around_bytes >> PAGE_SHIFT > 1) {
		ret = do_fault_around(vmf);//预先的文件页也映射到物理页
		if (ret)
			return ret;
	}

	ret = __do_fault(vmf);//读文件到物理页缓存中
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
		return ret;

	ret |= finish_fault(vmf);//设置页表项
	unlock_page(vmf->page);
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
		put_page(vmf->page);
	return ret;
}

do_read_fault先调用vma中的map_pages 方法,把文件页映射到物理内存上,然后判断如果需要预读,则调用do_fault_around把预读的文件页也映射到物理内存上;然后调用__do_fault函数读文件到物理页缓存中;最后调用finish_fault设置页表项。其中do_fault_around主要是调用map_pages 方法把预读的文件页映射到物理内存上,__do_fault则是调用vma中的falut方法读文件到物理页缓存中,finish_fault设置页表项我们继续分析:

vm_fault_t finish_fault(struct vm_fault *vmf)
{
	struct page *page;
	vm_fault_t ret = 0;

	//如果异常是写操作导致的,并且vma不是共享的
	if ((vmf->flags & FAULT_FLAG_WRITE) &&	
	    !(vmf->vma->vm_flags & VM_SHARED))
		page = vmf->cow_page;//page指向vmf->cow_page
	else
		page = vmf->page;//page指向vmf->page

	/*
	 * check even for read faults because we might have lost our CoWed
	 * page
	 */
	if (!(vmf->vma->vm_flags & VM_SHARED))//如果是读操作导致的,
		ret = check_stable_address_space(vmf->vma->vm_mm);//检查给定的内存是否从用户拷贝过来的
	if (!ret)
		ret = alloc_set_pte(vmf, page);//设置页表项的主要工作函数
	if (vmf->pte)
		pte_unmap_unlock(vmf->pte, vmf->ptl);
	return ret;
}

finish_fault 负责设置页表项,把主要工作委托给函数 alloc_set_pte :

vm_fault_t alloc_set_pte(struct vm_fault *vmf, struct page *page)
{
	struct vm_area_struct *vma = vmf->vma;
	bool write = vmf->flags & FAULT_FLAG_WRITE;
	pte_t entry;
	vm_fault_t ret;

	if (pmd_none(*vmf->pmd) && PageTransCompound(page)) {
		ret = do_set_pmd(vmf, page);
		if (ret != VM_FAULT_FALLBACK)
			return ret;
	}

	if (!vmf->pte) {//如果直接页表不存在
		ret = pte_alloc_one_map(vmf);//查找页表项
		if (ret)
			return ret;
	}

	/* Re-check under ptl */
	if (unlikely(!pte_none(*vmf->pte))) {//如果在锁住页表以后发现页表项不是空表项
		update_mmu_tlb(vma, vmf->address, vmf->pte);//说明其他处理器修改了同一页表项,那么当前处理器放弃处理
		return VM_FAULT_NOPAGE;
	}

	flush_icache_page(vma, page);//从指令缓存中冲刷页
	entry = mk_pte(page, vma->vm_page_prot);//使用页帧号和访问权限生成页表项的值
	entry = pte_sw_mkyoung(entry);//直接返回entry,不知道有什么用
	if (write)
		entry = maybe_mkwrite(pte_mkdirty(entry), vma);//设置页表项的脏标志位和写权限位
	/* copy-on-write page */
	if (write && !(vma->vm_flags & VM_SHARED)) {//如果写私有文件页
		inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);//task或者mm的rss_stat的匿名页数量加一
		page_add_new_anon_rmap(page, vma, vmf->address, false);//建立物理页到虚拟页的反向映射
		lru_cache_add_inactive_or_unevictable(page, vma);//把物理页添加到活动LRU链表或者不可回收LRU链表 
	} else {//写私有文件页除外
		inc_mm_counter_fast(vma->vm_mm, mm_counter_file(page));//task或者mm的rss_stat的某个种类页数量加一
		page_add_file_rmap(page, false);//添加pte映射到文件页面
	}
	set_pte_at(vma->vm_mm, vmf->address, vmf->pte, entry);设置页表项,把刚刚申请的物理地址写入到页表

	/* no need to invalidate: a not-present page won't be cached */
	update_mmu_cache(vma, vmf->address, vmf->pte);//更新处理器的页表缓存

	return 0;
}

alloc_set_pte如果直接页表不存在需要调用pte_alloc_one_map查找页表项;如果在锁住页表以后发现页表项不是空表项,说明其他处理器修改了同一页表项,那么当前处理器不用处理,仅仅更新一下TLB就行;然后调用flush_icache_page从指令缓存中冲刷页,使用页帧号和访问权限生成页表项的值;如果是写操作导致的缺页异常,则需要设置页表项的脏标志位和写权限位;如果是写私有文件页,需要建立物理页到虚拟页的反向映射,把物理页添加到活动LRU链表或者不可回收LRU链表;如果不是写私有文件,就是写共享文件或者写匿名页或者读操作,则需要添加pte映射到文件页面;最后调用set_pte_at函数完成页表项的设置,更新处理器的页表缓存。

3.2 do_cow_fault

处理写私有文件页错误会执行写诗复制,方法如下,

  1. 把文件页从存储设备上的文件系统读到文件的页缓存中。
  2. 执行写时复制,为文件的页缓存中的物理页创建一个副本,这个副本是进程的私有匿名页,和文件脱离关系,修改副本不会导致文件变化。
  3. 设置进程的页表项,把虚拟页映射到副本。
static vm_fault_t do_cow_fault(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	vm_fault_t ret;

	if (unlikely(anon_vma_prepare(vma)))//初始化vma中的anon_vma_chain和anon_vma
		return VM_FAULT_OOM;
	//因为后面需要执行写时复制,所以预先为副本分配一个物理页
	vmf->cow_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma, vmf->address);
	if (!vmf->cow_page)
		return VM_FAULT_OOM;

	if (mem_cgroup_charge(vmf->cow_page, vma->vm_mm, GFP_KERNEL)) {
		put_page(vmf->cow_page);
		return VM_FAULT_OOM;
	}
	cgroup_throttle_swaprate(vmf->cow_page, GFP_KERNEL);

	ret = __do_fault(vmf);//把文件页读到文件的页缓存中
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
		goto uncharge_out;
	if (ret & VM_FAULT_DONE_COW)
		return ret;

	//把文件的页缓存中物理页的数据复制到副本物理页
	copy_user_highpage(vmf->cow_page, vmf->page, vmf->address, vma);
	__SetPageUptodate(vmf->cow_page);//设置副本页描述符的标志位PG_uptodate,表示物理页包含有效的数据

	ret |= finish_fault(vmf);//设置页表项,把虚拟页映射到副本物理页
	unlock_page(vmf->page);
	put_page(vmf->page);
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
		goto uncharge_out;
	return ret;
uncharge_out:
	put_page(vmf->cow_page);
	return ret;
}

do_cow_fault首先调用anon_vma_prepare初始化vma中的anon_vma_chain和anon_vma,因为后面需要执行写时复制,所以调用alloc_page_vma预先为副本分配一个物理页,然后调用__do_fault把文件页读到文件的页缓存中,接着调用copy_user_highpage把文件的页缓存中物理页的数据复制到副本物理页,同时设置副本页描述符的标志位PG_uptodate,表示物理页包含有效的数据,最后调用finish_fault设置页表项。

3.3do_shared_fault

处理写共享文件页错误的方法如下:

  1. 把文件页从存储设备上的文件系统读到文件的页缓存中。
  2. 设置进程的页表项,把虚拟页映射到文件的页缓存中的物理页。
static vm_fault_t do_shared_fault(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	vm_fault_t ret, tmp;

	ret = __do_fault(vmf);//把文件页读到文件的页缓存中
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE | VM_FAULT_RETRY)))
		return ret;

	/*
	 * Check if the backing address space wants to know that the page is
	 * about to become writable
	 */
	if (vma->vm_ops->page_mkwrite) {//如果page_mkwrite存在
		unlock_page(vmf->page);
		tmp = do_page_mkwrite(vmf);//调用page_mkwrite方法通知文件系统“页即将变成可写的”
		if (unlikely(!tmp ||s
				(tmp & (VM_FAULT_ERROR | VM_FAULT_NOPAGE)))) {
			put_page(vmf->page);
			return tmp;
		}
	}

	ret |= finish_fault(vmf);//设置页表项,把虚拟页映射到文件的页缓存中的物理页。
	if (unlikely(ret & (VM_FAULT_ERROR | VM_FAULT_NOPAGE |
					VM_FAULT_RETRY))) {
		unlock_page(vmf->page);
		put_page(vmf->page);
		return ret;
	}

	ret |= fault_dirty_shared_page(vmf);//设置页的脏标志位,表示页的数据被修改
	return ret;
}

do_shared_fault首先调用__do_fault把文件页读到文件的页缓存中;接着判断page_mkwrite方法是否存在,如果存在则通过do_page_mkwrite函数调用page_mkwrite方法通知文件系统“页即将变成可写的”,然后通过finish_fault函数设置页表项,最后调用fault_dirty_shared_page设置页的脏标志位,表示页的数据被修改,在fault_dirty_shared_page函数中会判断,如果page_mkwrite方法不存在,则仅仅更新文件的修改时间,不会设置页的脏标志位。

4. do_swap_page

函数 do_swap_page 的执行流程如下:

  1. 调用函数 pte_to_swp_entry,把页表项转换成交换项,交换项包含了交换区的索引和偏移。
  2. 调用函数 lookup_swap_cache,在交换缓存中根据交换区的偏移查找页。
  3. 调用函数 swap_readpage或者swapin_readahead,从交换区换入页面
vm_fault_t do_swap_page(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	struct page *page = NULL, *swapcache;
	swp_entry_t entry;
	pte_t pte;
	int locked;
	int exclusive = 0;
	vm_fault_t ret = 0;
	void *shadow = NULL;

	//比较pte页表项的内容和orig_pte,如果不相同则返回作物
	if (!pte_unmap_same(vma->vm_mm, vmf->pmd, vmf->pte, vmf->orig_pte))
		goto out;

	entry = pte_to_swp_entry(vmf->orig_pte);//通过物理地址找到entry
	if (unlikely(non_swap_entry(entry))) {//如果是非迁移类型的swap_entry,返回各种错误
		if (is_migration_entry(entry)) {
			migration_entry_wait(vma->vm_mm, vmf->pmd,
					     vmf->address);
		} else if (is_device_private_entry(entry)) {
			vmf->page = device_private_entry_to_page(entry);
			ret = vmf->page->pgmap->ops->migrate_to_ram(vmf);
		} else if (is_hwpoison_entry(entry)) {
			ret = VM_FAULT_HWPOISON;
		} else {
			print_bad_pte(vma, vmf->address, vmf->orig_pte, NULL);
			ret = VM_FAULT_SIGBUS;
		}
		goto out;
	}


	delayacct_set_flag(DELAYACCT_PF_SWAPIN);//设置current状态为正在swapin
	page = lookup_swap_cache(entry, vma, vmf->address);//在交换缓存中查找页
	swapcache = page;

	if (!page) {//如果页不在交换缓存中
		struct swap_info_struct *si = swp_swap_info(entry);

		if (data_race(si->flags & SWP_SYNCHRONOUS_IO) &&
		    __swap_count(entry) == 1) {//如果是高效同步IO并且数量是1
			/* skip swapcache */
			page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma,
							vmf->address);//分配一个物理页
			if (page) {
				int err;

				__SetPageLocked(page);
				__SetPageSwapBacked(page);
				set_page_private(page, entry.val);

				/* Tell memcg to use swap ownership records */
				SetPageSwapCache(page);
				err = mem_cgroup_charge(page, vma->vm_mm,
							GFP_KERNEL);
				ClearPageSwapCache(page);
				if (err) {
					ret = VM_FAULT_OOM;
					goto out_page;
				}

				shadow = get_shadow_from_swap_cache(entry);
				if (shadow)
					workingset_refault(page, shadow);

				lru_cache_add(page);//将页面添加到LRU列表中
				swap_readpage(page, true);//快速从交换区换入1个页
			}
		} else {//不是高效IO的情况
			page = swapin_readahead(entry, GFP_HIGHUSER_MOVABLE,
						vmf);//慢慢的从交换区换入多个页面
			swapcache = page;
		}

		if (!page) {
			/*
			 * Back out if somebody else faulted in this pte
			 * while we released the pte lock.
			 */
			vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd,
					vmf->address, &vmf->ptl);
			if (likely(pte_same(*vmf->pte, vmf->orig_pte)))
				ret = VM_FAULT_OOM;
			delayacct_clear_flag(DELAYACCT_PF_SWAPIN);
			goto unlock;
		}

		/* Had to read the page from swap area: Major fault */
		ret = VM_FAULT_MAJOR;
		count_vm_event(PGMAJFAULT);
		count_memcg_event_mm(vma->vm_mm, PGMAJFAULT);
	} else if (PageHWPoison(page)) {
		/*
		 * hwpoisoned dirty swapcache pages are kept for killing
		 * owner processes (which may be unknown at hwpoison time)
		 */
		ret = VM_FAULT_HWPOISON;
		delayacct_clear_flag(DELAYACCT_PF_SWAPIN);
		goto out_release;
	}

	locked = lock_page_or_retry(page, vma->vm_mm, vmf->flags);

	delayacct_clear_flag(DELAYACCT_PF_SWAPIN);
	if (!locked) {
		ret |= VM_FAULT_RETRY;
		goto out_release;
	}

	/*
	 * Make sure try_to_free_swap or reuse_swap_page or swapoff did not
	 * release the swapcache from under us.  The page pin, and pte_same
	 * test below, are not enough to exclude that.  Even if it is still
	 * swapcache, we need to check that the page's swap has not changed.
	 */
	if (unlikely((!PageSwapCache(page) ||
			page_private(page) != entry.val)) && swapcache)
		goto out_page;

	page = ksm_might_need_to_copy(page, vma, vmf->address);
	if (unlikely(!page)) {
		ret = VM_FAULT_OOM;
		page = swapcache;
		goto out_page;
	}

	cgroup_throttle_swaprate(page, GFP_KERNEL);//cgroup记账

	vmf->pte = pte_offset_map_lock(vma->vm_mm, vmf->pmd, vmf->address,
			&vmf->ptl);//锁住页表
	//直接页表项和锁住页表之前不同,说明其他处理器已经换入页
	if (unlikely(!pte_same(*vmf->pte, vmf->orig_pte)))
		goto out_nomap;

	if (unlikely(!PageUptodate(page))) {
		ret = VM_FAULT_SIGBUS;
		goto out_nomap;
	}

	inc_mm_counter_fast(vma->vm_mm, MM_ANONPAGES);//anonpage数加1,匿名页从swap空间交换出来,所以加1
	dec_mm_counter_fast(vma->vm_mm, MM_SWAPENTS);//swap page个数减1,
	pte = mk_pte(page, vma->vm_page_prot);//生成页表项的值
	if ((vmf->flags & FAULT_FLAG_WRITE) && reuse_swap_page(page, NULL)) {
		pte = maybe_mkwrite(pte_mkdirty(pte), vma);
		vmf->flags &= ~FAULT_FLAG_WRITE;
		ret |= VM_FAULT_WRITE;
		exclusive = RMAP_EXCLUSIVE;
	}
	flush_icache_page(vma, page);
	if (pte_swp_soft_dirty(vmf->orig_pte))
		pte = pte_mksoft_dirty(pte);
	if (pte_swp_uffd_wp(vmf->orig_pte)) {
		pte = pte_mkuffd_wp(pte);
		pte = pte_wrprotect(pte);
	}
	set_pte_at(vma->vm_mm, vmf->address, vmf->pte, pte);//将新生成的页表项的值写入页表
	arch_do_swap_page(vma->vm_mm, vma, vmf->address, pte, vmf->orig_pte);//空函数
	vmf->orig_pte = pte;//更新orig_pte

	/* ksm created a completely new copy */
	if (unlikely(page != swapcache && swapcache)) {
		page_add_new_anon_rmap(page, vma, vmf->address, false);//建立物理页到虚拟页的反向映射
		lru_cache_add_inactive_or_unevictable(page, vma);//把物理页添加到活动LRU链表或者不可回收LRU链表 
	} else {
		do_page_add_anon_rmap(page, vma, vmf->address, exclusive);//建立物理页到虚拟页的反向映射
	}

	swap_free(entry);//通过entry找到swap_info_struct并且释放他
	if (mem_cgroup_swap_full(page) ||
	    (vma->vm_flags & VM_LOCKED) || PageMlocked(page))
		try_to_free_swap(page);
	unlock_page(page);
	if (page != swapcache && swapcache) {
		unlock_page(swapcache);
		put_page(swapcache);
	}

	if (vmf->flags & FAULT_FLAG_WRITE) {
		ret |= do_wp_page(vmf);//执行的写时复制
		if (ret & VM_FAULT_ERROR)
			ret &= VM_FAULT_ERROR;
		goto out;
	}

	/* No need to invalidate - it was non-present before */
	update_mmu_cache(vma, vmf->address, vmf->pte);//保持缓存一致
unlock:
	pte_unmap_unlock(vmf->pte, vmf->ptl);//释放页表锁
out:
	return ret;
out_nomap:
	pte_unmap_unlock(vmf->pte, vmf->ptl);
out_page:
	unlock_page(page);
out_release:
	put_page(page);
	if (page != swapcache && swapcache) {
		unlock_page(swapcache);
		put_page(swapcache);
	}
	return ret;
}

do_swap_page首先比较pte页表项的内容和orig_pte,如果不相同则返回错误;调用函数pte_to_swp_entry通过物理地址找到entry,如果entry是非迁移类型的,返回各种错误;调用函数delayacct_set_flag设置current状态为正在swapin,调用函数lookup_swap_cache在交换缓存中查找页;如果找不到页,说明页不在缓存中,在判断缓存页数量,如果只有一个,并且是高效的swap IO,调用swap_readpage函数快速读入页面,如果有多个缓存页,则调用swapin_readahead稍后读入页面;接着anonpage数加1,匿名页从swap空间交换出来,所以加1,swap page个数减1,然后调用函数mk_pte生成页表项的值和调用函数set_pte_at将新生成的页表项的值写入页表;最后如果页面具有写权限,调用函数do_wp_page执行的写时复制。

5. do_numa_page

static vm_fault_t do_numa_page(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	struct page *page = NULL;
	int page_nid = NUMA_NO_NODE;
	int last_cpupid;
	int target_nid;
	bool migrated = false;
	pte_t pte, old_pte;
	bool was_writable = pte_savedwrite(vmf->orig_pte);
	int flags = 0;

	/*
	 * The "pte" at this point cannot be used safely without
	 * validation through pte_unmap_same(). It's of NUMA type but
	 * the pfn may be screwed if the read is non atomic.
	 */
	vmf->ptl = pte_lockptr(vma->vm_mm, vmf->pmd);//获取pmd锁
	spin_lock(vmf->ptl);//上锁
	//如果页表项和锁住以前的页表项不同
	if (unlikely(!pte_same(*vmf->pte, vmf->orig_pte))) {
		pte_unmap_unlock(vmf->pte, vmf->ptl);
		goto out;//说明其他处理器修改了同一页表项,那么当前处理器放弃更新页表项
	}

	//启动pte保护read-modify-write事务,防止对pte进行异步硬件修改
	old_pte = ptep_modify_prot_start(vma, vmf->address, vmf->pte);
	pte = pte_modify(old_pte, vma->vm_page_prot);//保存临时修改的pte
	pte = pte_mkyoung(pte);
	if (was_writable)
		pte = pte_mkwrite(pte);//调用mkwrite方法通知问价那系统可读变为可写
	ptep_modify_prot_commit(vma, vmf->address, vmf->pte, old_pte, pte);//更新pte后完成pte的保护
	update_mmu_cache(vma, vmf->address, vmf->pte);//刷新mmu的cache

	page = vm_normal_page(vma, vmf->address, pte);//获取与pte关联page
	if (!page) {
		pte_unmap_unlock(vmf->pte, vmf->ptl);
		return 0;
	}

	/* TODO: handle PTE-mapped THP */
	if (PageCompound(page)) {//如果是复合页
		pte_unmap_unlock(vmf->pte, vmf->ptl);
		return 0;
	}

	/*
	 * Avoid grouping on RO pages in general. RO pages shouldn't hurt as
	 * much anyway since they can be in shared cache state. This misses
	 * the case where a mapping is writable but the process never writes
	 * to it but pte_write gets cleared during protection updates and
	 * pte_dirty has unpredictable behaviour between PTE scan updates,
	 * background writeback, dirty balancing and application behaviour.
	 */
	if (!pte_write(pte))
		flags |= TNF_NO_GROUP;

	/*
	 * Flag if the page is shared between multiple address spaces. This
	 * is later used when determining whether to group tasks together
	 */
	if (page_mapcount(page) > 1 && (vma->vm_flags & VM_SHARED))
		flags |= TNF_SHARED;

	last_cpupid = page_cpupid_last(page);
	page_nid = page_to_nid(page);
	target_nid = numa_migrate_prep(page, vma, vmf->address, page_nid,
			&flags);
	pte_unmap_unlock(vmf->pte, vmf->ptl);
	if (target_nid == NUMA_NO_NODE) {
		put_page(page);
		goto out;
	}

	/* Migrate to the requested node */
	migrated = migrate_misplaced_page(page, vma, target_nid);
	if (migrated) {
		page_nid = target_nid;
		flags |= TNF_MIGRATED;
	} else
		flags |= TNF_MIGRATE_FAIL;

out:
	if (page_nid != NUMA_NO_NODE)
		task_numa_fault(last_cpupid, page_nid, 1, flags);//去到node上的页面发生PROT NONE错误。
	return 0;
}

6. do_wp_page

有两种情况会执行写时复制:

  1. 进程分叉生成子进程的时候,为了避免复制物理页,子进程和父进程以只读方式共享所有私有的匿名页和文件页。当其中一个进程试图写只读页时,触发页错误异常,页错误异常处理程序分配新的物理页,把旧的物理页的数据复制到新的物理页,然后把虚拟页映射到新的物理页。
  2. 进程创建私有的文件映射,然后读访问,触发页错误异常,异常处理程序把文件读到页缓存,然后以只读模式把虚拟页映射到文件的页缓存中的物理页。接着执行写访问,触发页错误异常,异常处理程序执行写时复制,为文件的页缓存中的物理页创建一个副本,把虚拟页映射到副本。这个副本是进程的私有匿名页,和文件脱离关系,修改副本不会导致文件变化。

函数 do_wp_page的执行流程如下:

  1. 调用函数 vm_normal_page ,从页表项得到页帧号,然后得到页帧号对应的页描述符。特殊映射不希望关联页描述符,直接使用页帧号,可能是因为页描述符不存在,也可能是因为不想使用页描述符。
  2. 如果页描述符为空,说明使用页帧号的特殊映射。 如果是共享的可写特殊映射,不需要复制物理页,调用函数 wp_pfn_shared 来设置页表项的写权限位。如果是私有的可写特殊映射,调用函数 wp_page_copy 以复制物理页,然后把虚拟页映射到新的物理页。
  3. 如果页描述符存在,说明使用页描述符的正常映射。如果是共享的可写正常映射,不需要复制物理页,调用函数 wp_page_shared 来设置页表项的写权限位;如果是私有的可写正常映射,调用函数 wp_page_copy 以复制物理页,然后把虚拟页映射到新的物理页。
static vm_fault_t do_wp_page(struct vm_fault *vmf)
	__releases(vmf->ptl)
{
	struct vm_area_struct *vma = vmf->vma;

	if (userfaultfd_pte_wp(vma, *vmf->pte)) {
		pte_unmap_unlock(vmf->pte, vmf->ptl);
		return handle_userfault(vmf, VM_UFFD_WP);
	}

	/*
	 * Userfaultfd write-protect can defer flushes. Ensure the TLB
	 * is flushed in this case before copying.
	 */
	if (unlikely(userfaultfd_wp(vmf->vma) &&
		     mm_tlb_flush_pending(vmf->vma->vm_mm)))
		flush_tlb_page(vmf->vma, vmf->address);

	//从页表项得到页帧号,然后得到页帧号对应的页描述符
	vmf->page = vm_normal_page(vma, vmf->address, vmf->orig_pte);
	if (!vmf->page) {//找不到page,说明使用页帧号的特殊映射
		/*
		 * VM_MIXEDMAP !pfn_valid() case, or VM_SOFTDIRTY clear on a
		 * VM_PFNMAP VMA.
		 *
		 * We should not cow pages in a shared writeable mapping.
		 * Just mark the pages writable and/or call ops->pfn_mkwrite.
		 */
		if ((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
				     (VM_WRITE|VM_SHARED))
			return wp_pfn_shared(vmf);//共享的可写映射的cow过程,其实是调用pfn_mkwrite通知文件系统可读变为可写

		pte_unmap_unlock(vmf->pte, vmf->ptl);
		return wp_page_copy(vmf);//私有的可写映射的cow过程,复制物理页,然后把虚拟页映射到新的物理页。
	}

	//来到这里,说明找到page结构体,也就是使用页描述符的正常映射
	if (PageAnon(vmf->page)) {
		struct page *page = vmf->page;

		/* PageKsm() doesn't necessarily raise the page refcount */
		if (PageKsm(page) || page_count(page) != 1)
			goto copy;
		if (!trylock_page(page))
			goto copy;
		if (PageKsm(page) || page_mapcount(page) != 1 || page_count(page) != 1) {
			unlock_page(page);
			goto copy;
		}
		/*
		 * Ok, we've got the only map reference, and the only
		 * page count reference, and the page is locked,
		 * it's dark out, and we're wearing sunglasses. Hit it.
		 */
		unlock_page(page);
		wp_page_reuse(vmf);
		return VM_FAULT_WRITE;
	} else if (unlikely((vma->vm_flags & (VM_WRITE|VM_SHARED)) ==
					(VM_WRITE|VM_SHARED))) {
		return wp_page_shared(vmf);//共享的可写映射cow过程,其实是调用page_mkwrite通知文件系统可读变为可写
	}
copy:
	/*
	 * Ok, we need to copy. Oh, well..
	 */
	get_page(vmf->page);

	pte_unmap_unlock(vmf->pte, vmf->ptl);
	return wp_page_copy(vmf);//私有的可写映射的cow过程,复制物理页,然后把虚拟页映射到新的物理页。
}

do_wp_page调用函数vm_normal_page从页表项得到页帧号,然后得到页帧号对应的页描述符;然后判断能否找到页,如果找不到说明使用页帧号的特殊映射,如果找到页则说明使用页描述符的正常映射。如果是特殊映射,还要判断是共享的还是私有的,如果是共享的特殊映射,调用wp_pfn_shared函数处理;如果是私有的特殊映射,则调用wp_page_copy函数处理。如果是正常映射,也是要判断是共享的还是私有的,如果是共享的正常映射,调用wp_page_shared函数处理;如果是私有的正常映射,调用wp_page_copy处理。
因为wp_pfn_shared和wp_page_shared都是共享的,所以不需要复制物理页,这两个函数仅仅调用vma的page_mkwrite方法,通知文件系统把可读修改问可写。wp_page_copy就比较复杂了,下面我们看看wp_page_copy。

6.1 wp_page_copy

static vm_fault_t wp_page_copy(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	struct mm_struct *mm = vma->vm_mm;
	struct page *old_page = vmf->page;
	struct page *new_page = NULL;
	pte_t entry;
	int page_copied = 0;
	struct mmu_notifier_range range;

	if (unlikely(anon_vma_prepare(vma)))//初始化vma中的anon_vma_chain和anon_vma
		goto oom;

	if (is_zero_pfn(pte_pfn(vmf->orig_pte))) {//如果是零页,
		new_page = alloc_zeroed_user_highpage_movable(vma,
							      vmf->address);//那么分配一个物理页,然后用零初始化
		if (!new_page)
			goto oom;
	} else {//否则就是一个物理页,
		new_page = alloc_page_vma(GFP_HIGHUSER_MOVABLE, vma,
				vmf->address);//分配一个物理页
		if (!new_page)
			goto oom;

		if (!cow_user_page(new_page, old_page, vmf)) {//把数据复制到新的物理页
			/*
			 * COW failed, if the fault was solved by other,
			 * it's fine. If not, userspace would re-fault on
			 * the same address and we will handle the fault
			 * from the second attempt.
			 */
			put_page(new_page);
			if (old_page)
				put_page(old_page);
			return 0;
		}
	}

	if (mem_cgroup_charge(new_page, mm, GFP_KERNEL))
		goto oom_free_new;
	cgroup_throttle_swaprate(new_page, GFP_KERNEL);

	__SetPageUptodate(new_page);//设置新页的标志位PG_uptodate,表示物理页包含有效的数据

	mmu_notifier_range_init(&range, MMU_NOTIFY_CLEAR, 0, vma, mm,
				vmf->address & PAGE_MASK,
				(vmf->address & PAGE_MASK) + PAGE_SIZE);
	mmu_notifier_invalidate_range_start(&range);

	//给页表上锁,锁住以后重新读页表项
	vmf->pte = pte_offset_map_lock(mm, vmf->pmd, vmf->address, &vmf->ptl);
	if (likely(pte_same(*vmf->pte, vmf->orig_pte))) {//页表项和锁住以前的页表项相同
		if (old_page) {
			if (!PageAnon(old_page)) {
				dec_mm_counter_fast(mm,
						mm_counter_file(old_page));
				inc_mm_counter_fast(mm, MM_ANONPAGES);
			}
		} else {
			inc_mm_counter_fast(mm, MM_ANONPAGES);
		}
		flush_cache_page(vma, vmf->address, pte_pfn(vmf->orig_pte));//从缓存中冲刷页
		entry = mk_pte(new_page, vma->vm_page_prot);//使用新的物理页和访问权限生成页表项的值
		entry = pte_sw_mkyoung(entry);//直接返回entry,不知道有什么用
		entry = maybe_mkwrite(pte_mkdirty(entry), vma);//设置为脏

		ptep_clear_flush_notify(vma, vmf->address, vmf->pte);//把页表项清除,并且冲刷页表缓存
		page_add_new_anon_rmap(new_page, vma, vmf->address, false);//建立新物理页到虚拟页的反向映射
		lru_cache_add_inactive_or_unevictable(new_page, vma);//把物理页添加到活动 LRU 链表或不可回收 LRU 链表中
		/*
		 * We call the notify macro here because, when using secondary
		 * mmu page tables (such as kvm shadow page tables), we want the
		 * new page to be mapped directly into the secondary page table.
		 */
		set_pte_at_notify(mm, vmf->address, vmf->pte, entry);//修改页表项
		update_mmu_cache(vma, vmf->address, vmf->pte);//更新页表缓存
		if (old_page) {
			page_remove_rmap(old_page, false);//删除旧物理页到虚拟页的反向映射
		}

		/* Free the old page.. */
		new_page = old_page;
		page_copied = 1;
	} else {//页表项和锁住以前的页表项不同
		update_mmu_tlb(vma, vmf->address, vmf->pte);
	}

	if (new_page)
		put_page(new_page);

	pte_unmap_unlock(vmf->pte, vmf->ptl);//释放页表的锁
	/*
	 * No need to double call mmu_notifier->invalidate_range() callback as
	 * the above ptep_clear_flush_notify() did already call it.
	 */
	mmu_notifier_invalidate_range_only_end(&range);
	if (old_page) {//如果页表项映射到新的物理页,并且旧的物理页被锁定在内存中,
		/*
		 * Don't let another task, with possibly unlocked vma,
		 * keep the mlocked page.
		 */
		if (page_copied && (vma->vm_flags & VM_LOCKED)) {
			lock_page(old_page);	/* LRU manipulation */
			if (PageMlocked(old_page))
				munlock_vma_page(old_page);
			unlock_page(old_page);
		}
		put_page(old_page);//把旧的物理页释放
	}
	return page_copied ? VM_FAULT_WRITE : 0;
oom_free_new:
	put_page(new_page);
oom:
	if (old_page)
		put_page(old_page);
	return VM_FAULT_OOM;
}

wp_page_copy首先调用函数anon_vma_prepare初始化vma中的anon_vma_chain和anon_vma,然后判断这个也是否为零页,如果是零页,则调用函数alloc_zeroed_user_highpage_movable分配一个物理页并且初始化为0;如果不是零页,则调用函数alloc_page_vma分配物理页,调用函数cow_user_page把数据复制到新的物理页。接着调用函数__SetPageUptodate设置新页的标志位PG_uptodate,表示物理页包含有效的数据,调用函数flush_cache_page从缓存中刷新页,调用函数mk_pte生成页表项的值,使用函数maybe_mkwrite设置为脏页,把旧的页表项清除,并且冲刷页表缓存,建立新物理页到虚拟页的反向映射,把物理页添加到活动 LRU 链表或不可回收 LRU 链表中,调用函数set_pte_at_notify修改页表项,还要把旧的物理页释放。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
在上一篇文章中,我们介绍了什么是URL重写和为什么要使用动态页面静态化。本文将进一步探讨如何使用URL重写将动态页面转换为静态页面。 1. 确定需要静态化的页面 通常情况下,我们会选择那些频繁访问、数据不经常更新的页面进行静态化,这样可以大幅度减少服务器压力,提高网站响应速度。 2. 编写URL重写规则 接下来,我们需要编写URL重写规则,将动态页面的URL转换为静态页面的URL。这里我们以Apache服务器为例,使用mod_rewrite模块来实现URL重写。 例如,我们要将动态页面http://www.example.com/article.php?id=1 转换为静态页面http://www.example.com/article/1.html,则可以使用以下规则: ``` RewriteEngine On RewriteRule ^article/([^/]*)\.html$ /article.php?id=$1 [L] ``` 这个规则的意思是:将以“/article/”开头、以“.html”结尾的URL请求重写为“/article.php?id=”后面接文章ID的形式。 3. 编写静态页面生成程序 重写URL只是第一步,我们还需要编写程序将动态页面生成为静态页面。这个程序可以是一个独立的脚本,也可以是在页面加载时自动执行的程序。 例如,我们可以在article.php页面中加入以下代码: ``` if(!file_exists("article/".$_GET['id'].".html")){ ob_start(); // 页面内容 $content = ob_get_contents(); ob_end_clean(); file_put_contents("article/".$_GET['id'].".html", $content); } ``` 这个程序的作用是:当访问article.php页面时,如果“/article/”后面的ID对应的静态文件不存在,则将页面内容缓存起来,并保存为“/article/”后面的ID对应的静态文件。 这样,当下一次访问同一页面时,服务器会直接返回静态页面,而不用再去执行动态页面生成的过程,从而提高网站响应速度。 4. 静态页面更新 由于静态页面不像动态页面那样能够自动更新,因此我们需要编写相应的程序来实现静态页面的更新。 例如,我们可以在article.php页面中添加以下代码: ``` if(file_exists("article/".$_GET['id'].".html")){ unlink("article/".$_GET['id'].".html"); } ``` 这个程序的作用是:当文章内容发生变化时,删除对应的静态文件,下一次访问该页面时会重新生成静态文件。 总结: 通过URL重写和动态页面静态化,可以大幅度提高网站的响应速度,减少服务器压力。但是需要注意的是,静态页面不适合频繁更新的内容,否则可能导致用户看到的内容与实际情况不符。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小坚学Linux

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值