解决完上篇博客的问题后,随之而来的,又是一堆因好奇心引发的问题,其中,最让我好奇的是这个问题:
内核的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);
********
}
主要分为以下三步完成页面的迁移:
- 解除现有pte映射,由函数try_to_unmap完成
- 将数据迁移到新的页面,由函数move_to_new_page完成
- 将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又可以愉快的读写相关内存,就像什么事情都没发生过一样。