linux页面迁移与用户态访问的并发处理

解决完上篇博客的问题后,随之而来的,又是一堆因好奇心引发的问题,其中,最让我好奇的是这个问题:
内核的kcompactd线程在做页面迁移,刚好用户态的进程也在对该页面进行读写操作,这两个步骤是并行的,那么,内核是如何做好并发保护工作的?具体一点的说,就是内核如何保证,迁移前,用户态对该内存的读写是在迁移前的页,迁移后,用户态对该内存的读写是在迁移后的页,不会发生交叉的情况,也就是不会发生一部分写入的内容遗留在迁移前的页,而没有同步到迁移后的页。
此问题可以扩展为一般类问题:内核线程对用户task的内存进行迁移、swap out等等操作时,如何保证数据同步的?脏页的flush操作,如何与write系统调用互斥?等等等等

当然,这个一般类问题范围比较广,处理方法也不尽相同,本文focus在页面迁移流程上对这个问题的处理过程上。代码基于linux 4.19.195.
上篇博客中提到,页面迁移的主函数migrate_pages,会调用unmap_and_move完成页面的unmap和move的动作,而unmap_and_move函数的工作主要由__unmap_and_move完成。
__unmap_and_move函数很长,我们把最关键的代码提取出来。

static int __unmap_and_move(struct page *page, struct page *newpage, //page被迁移的页面,newpage迁移页面的目的地
		int force, enum migrate_mode mode) //force表示是否强制迁移,mode迁移模式
{
	**********
	if (!page->mapping) {
		VM_BUG_ON_PAGE(PageAnon(page), page);
		if (page_has_private(page)) {
			try_to_free_buffers(page);
			goto out_unlock_both;
		}
	} else if (page_mapped(page)) { // 有pte映射
		/* Establish migration ptes */
		VM_BUG_ON_PAGE(PageAnon(page) && !PageKsm(page) && !anon_vma,
				page);
		try_to_unmap(page, // 解除映射
			TTU_MIGRATION|TTU_IGNORE_MLOCK|TTU_IGNORE_ACCESS);
		page_was_mapped = 1;
	}

	if (!page_mapped(page))
		rc = move_to_new_page(newpage, page, mode); //迁移到新页面

	if (page_was_mapped)
		remove_migration_ptes(page, //删掉迁移的pte,并指向新的page
			rc == MIGRATEPAGE_SUCCESS ? newpage : page, false);
	out_unlock_both:
	unlock_page(newpage);
out_unlock:
	/* Drop an anon_vma reference if we took one */
	if (anon_vma)
		put_anon_vma(anon_vma);
	unlock_page(page);
		********
}

主要分为以下三步完成页面的迁移:

  1. 解除现有pte映射,由函数try_to_unmap完成
  2. 将数据迁移到新的页面,由函数move_to_new_page完成
  3. 将pte由旧的修改为新的,即从指向旧的page修改为指向新的page,由函数remove_migration_ptes完成

这么一看,如果在第一步完成后,用户态又对该位置的内存有读写操作,那岂不是会触发缺页中断,然后分配个新页亦或是怎么处理~~~ 当然不是这样,下面仔细分析代码。
先看try_to_unmap函数。

bool try_to_unmap(struct page *page, enum ttu_flags flags)
{
	struct rmap_walk_control rwc = {
		.rmap_one = try_to_unmap_one, //断开某个vma上映射的pte
		.arg = (void *)flags,
		.done = page_mapcount_is_zero, //表示判断一个页面是否成功断开
		.anon_lock = page_lock_anon_vma_read,
	};

	/*
	 * During exec, a temporary VMA is setup and later moved.
	 * The VMA is moved under the anon_vma lock but not the
	 * page tables leading to a race where migration cannot
	 * find the migration ptes. Rather than increasing the
	 * locking requirements of exec(), migration skips
	 * temporary VMAs until after exec() completes.
	 */
	if ((flags & (TTU_MIGRATION|TTU_SPLIT_FREEZE))
	    && !PageKsm(page) && PageAnon(page))
		rwc.invalid_vma = invalid_migration_vma;

	if (flags & TTU_RMAP_LOCKED)
		rmap_walk_locked(page, &rwc);
	else
		rmap_walk(page, &rwc);

	return !page_mapcount(page) ? true : false;
}

填充完rwc结构体,然后交由rmap_walk处理,最终会回调到try_to_unmap_one函数完成处理。
try_to_unmap_one函数在我看来真的又长又臭,这里只列出关键部分。

static bool try_to_unmap_one(struct page *page, struct vm_area_struct *vma,
		     unsigned long address, void *arg)
{
	struct mm_struct *mm = vma->vm_mm;
	struct page_vma_mapped_walk pvmw = {
		.page = page,
		.vma = vma,
		.address = address,
	};
	pte_t pteval;
	struct page *subpage;
	bool ret = true;
	unsigned long start = address, end;
	enum ttu_flags flags = (enum ttu_flags)arg;
	**********
	} else if (IS_ENABLED(CONFIG_MIGRATION) &&
				(flags & (TTU_MIGRATION|TTU_SPLIT_FREEZE))) { //根据参数,migrate_pages函数做页面迁移的时候会走这个分支
			swp_entry_t entry;
			pte_t swp_pte;

			if (arch_unmap_one(mm, vma, address, pteval) < 0) {
				set_pte_at(mm, address, pvmw.pte, pteval);
				ret = false;
				page_vma_mapped_walk_done(&pvmw);
				break;
			}

			/*
			 * Store the pfn of the page in a special migration
			 * pte. do_swap_page() will wait until the migration
			 * pte is removed and then restart fault handling.
			 */
			entry = make_migration_entry(subpage,
					pte_write(pteval));
			swp_pte = swp_entry_to_pte(entry);
			if (pte_soft_dirty(pteval))
				swp_pte = pte_swp_mksoft_dirty(swp_pte);
			set_pte_at(mm, address, pvmw.pte, swp_pte);
			/*
			 * No need to invalidate here it will synchronize on
			 * against the special swap migration pte.
			 */
		} else if (PageAnon(page)) {
		***************
}

migrate_page函数会传入TTU_MIGRATION,从而走入上面代码的分支。奇怪的是,这里居然生成了一个swp_entry_t ??然后通过set_pte_at把这个写入页表项。
我们注意到这里的注释:

/*
* Store the pfn of the page in a special migration
* pte. do_swap_page() will wait until the migration
* pte is removed and then restart fault handling.
*/

原来,这里是利用了swap类型的pte,来标识这是一个“处于migration过程中的page”。当set_pte_at函数设置好页表项的那一刹那开始,用户态task访问相关内存时,就不能像之前那样顺利的访问了,会走到缺页中断函数里的do_swap_page流程。那么,这个流程里是怎么处理的呢?另外,上面注释中说的wait until the migration pte is removed 以及restart fault handling是怎么实现的呢?这一切都在do_swap_page函数的最开头。

vm_fault_t do_swap_page(struct vm_fault *vmf)
{
	struct vm_area_struct *vma = vmf->vma;
	struct page *page = NULL, *swapcache;
	struct mem_cgroup *memcg;
	swp_entry_t entry;
	pte_t pte;
	int locked;
	int exclusive = 0;
	vm_fault_t ret = 0;

	if (!pte_unmap_same(vma->vm_mm, vmf->pmd, vmf->pte, vmf->orig_pte))
		goto out;

	entry = pte_to_swp_entry(vmf->orig_pte); 
	if (unlikely(non_swap_entry(entry))) {
		if (is_migration_entry(entry)) {
			migration_entry_wait(vma->vm_mm, vmf->pmd,
					     vmf->address);
		} else if (is_device_private_entry(entry)) {
			/*
			 * For un-addressable device memory we call the pgmap
			 * fault handler callback. The callback must migrate
			 * the page back to some CPU accessible page.
			 */
			ret = device_private_entry_fault(vma, vmf->address, entry,
						 vmf->flags, vmf->pmd);
		} 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;
	}
	****************
}

不深究细节,直接说结果,此时用户读取相关区域内存触发缺页中断,会通过pte_to_swp_entry函数获取到try_to_unmap_one函数里设置好的用来标识这是一个“处于migration过程中的page”的pte,从而走入non_swap_entry分支,进而执行函数migration_entry_wait。
题外话,可以看到non_swap_entry函数里又有如此多的分支,可见内核使用swap pte做了多少坏事。
言归正传,migration_entry_wait函数的作用就是其字面意思,在等待migration的完成。本质是在等待旧page释放其page lock。代码非常简单,这里不再分析。
等待完成后,直接goto out了,简单梳理一下代码流程,可知这里返回0,咦,为什么呢?回想一下,这不就实现了上面注释中写的wait until the migration pte is removed 以及restart fault handling吗?
往上翻一下,回到__unmap_and_move函数,我们看看什么时候会把旧page的lock给释放。哦,原来是在remove_migration_ptes之后,函数remove_migration_pte里已经把页表项设置为指向新的page了,所以用户态task在缺页中断do_swap_page直接返回后,就能够愉快的继续读写页面了,除了中间可能的一点等待page lock的卡顿,其他时候无法感知到内核偷偷的在其背后做了migration的操作。
至此,分析完毕,总结一下本文最开头问题的答案。
内核巧妙的利用了swap pte,让用户态task进入缺页中断并在旧page的lock上等待,内核趁机完成旧页到新页的copy,页表项的设置操作,然后释放page lock从而通知用户态进程继续运行。实际上在缺页中断中除了等待,并没有做任何事情。退出缺页中断时,相关页表已经设置完成,接下来用户态task又可以愉快的读写相关内存,就像什么事情都没发生过一样。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值