缺页中断处理函数中,为了实现高效的并发操作,做了不少优化工作,使得锁的粒度非常小,并且很好的解决了多个线程对同一地址空间的并发处理问题。本文简单讲解一下我对缺页中断并发处理的几个小技巧的理解。本文继续linux内核4.19.195.
首先,缺页中断能并发吗?缺页中断处理函数handle_mm_fault的注释中不是写着已经拿到了mm semaphore锁吗?怎么还会并发呢?
/* * By the time we get here, we already hold the mm semaphore * * The mmap_sem may have been released depending on flags and our * return value. See filemap_fault() and __lock_page_or_retry(). */
其实,从arm的do_page_fault函数可以知道,缺页中断确实持有了mm semaphore锁,但是使用的持锁方式是down_read(&mm->mmap_sem);这样一来,多个缺页中断确实是可以并发执行的。 从下面开始的分析,基于内核支持thp这个假设前提 我们知道,缺页中断会走到函数__handle_mm_fault中处理。
static vm_fault_t __handle_mm_fault(struct vm_area_struct *vma,
unsigned long address, unsigned int flags)
{
***
vmf.pmd = pmd_alloc(mm, vmf.pud, address);
if (!vmf.pmd)
return VM_FAULT_OOM;
if (pmd_none(*vmf.pmd) && __transparent_hugepage_enabled(vma)) {
ret = create_huge_pmd(&vmf);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
pmd_t orig_pmd = *vmf.pmd;
barrier();
if (unlikely(is_swap_pmd(orig_pmd))) {
VM_BUG_ON(thp_migration_supported() &&
!is_pmd_migration_entry(orig_pmd));
if (is_pmd_migration_entry(orig_pmd))
pmd_migration_entry_wait(mm, vmf.pmd);
return 0;
}
if (pmd_trans_huge(orig_pmd) || pmd_devmap(orig_pmd)) {
if (pmd_protnone(orig_pmd) && vma_is_accessible(vma))
return do_huge_pmd_numa_page(&vmf, orig_pmd);
if (dirty && !pmd_write(orig_pmd)) {
ret = wp_huge_pmd(&vmf, orig_pmd);
if (!(ret & VM_FAULT_FALLBACK))
return ret;
} else {
huge_pmd_set_accessed(&vmf, orig_pmd);
return 0;
}
}
}
return handle_pte_fault(&vmf);
}
我们先看pmd_alloc这一行,如果同一进程下的两个线程同时执行到这里并且都申请pmd成功了,那么接下来会发生什么情况呢? 要么同时走进if分支,一块执行create_huge_pmd,要么一个走if,一个走else。 先看后者这种情况,为简单起见,假设系统中未配置swap分区,那么如果一个走if,一个走else,那肯定是因为走if的流程,完成了create_huge_pmd的处理,做好了映射,而另一个流程,判断pmd_none(*vmf.pmd)才会失败,从而走的else。 我们看一下else里面做了什么。else里,首先判断pmd_trans_huge(orig_pmd),此时这个条件必然成立,从而进入29行的if流程。省略30-31行的numa balance判断,直接看33行的if判断。如果此时两个缺页中断都是读或者都是写操作,这显然33行的条件不会成立,代码会在39行完成返回。38行的代码主要是调用pmd_mkyoung。否则,如果读的缺页中断完成了pmd页表的建设,此时写的缺页中断就会进入33行的if判断,完成huge_pmd的cow动作,进而结束缺页中断处理流程。 再看前者的情况,也就是两个缺页中断都走进if,也就是create_huge_pmd函数里。
static inline vm_fault_t create_huge_pmd(struct vm_fault *vmf)
{
if (vma_is_anonymous(vmf->vma))
return do_huge_pmd_anonymous_page(vmf);
if (vmf->vma->vm_ops->huge_fault)
return vmf->vma->vm_ops->huge_fault(vmf, PE_SIZE_PMD);
return VM_FAULT_FALLBACK;
}
可见把情况分成了匿名页映射以及文件页映射,这里拿匿名页映射举例。
vm_fault_t do_huge_pmd_anonymous_page(struct vm_fault *vmf)
{
***
zero_page = mm_get_huge_zero_page(vma->vm_mm);
if (unlikely(!zero_page)) {
pte_free(vma->vm_mm, pgtable);
count_vm_event(THP_FAULT_FALLBACK);
return VM_FAULT_FALLBACK;
}
vmf->ptl = pmd_lock(vma->vm_mm, vmf->pmd);
ret = 0;
if (pmd_none(*vmf->pmd)) {
ret = check_stable_address_space(vma->vm_mm);
if (ret) {
spin_unlock(vmf->ptl);
pte_free(vma->vm_mm, pgtable);
} else if (userfaultfd_missing(vma)) {
spin_unlock(vmf->ptl);
pte_free(vma->vm_mm, pgtable);
ret = handle_userfault(vmf, VM_UFFD_MISSING);
VM_BUG_ON(ret & VM_FAULT_FALLBACK);
} else {
set_huge_zero_page(pgtable, vma->vm_mm, vma,
haddr, vmf->pmd, zero_page);
spin_unlock(vmf->ptl);
}
} else {
spin_unlock(vmf->ptl);
pte_free(vma->vm_mm, pgtable);
}
return ret;
}
gfp = alloc_hugepage_direct_gfpmask(vma);
page = alloc_hugepage_vma(gfp, vma, haddr, HPAGE_PMD_ORDER);
if (unlikely(!page)) {
count_vm_event(THP_FAULT_FALLBACK);
return VM_FAULT_FALLBACK;
}
prep_transhuge_page(page);
return __do_huge_pmd_anonymous_page(vmf, page, gfp);
}
函数很长,我们拿重点代码分析。这里分析两个读中断的情况。 在做好一切的准备工作,包括get全局零页,申请pgtable,然后才开始持锁pmd_lock(vma->vm_mm, vmf->pmd);可以看到,持锁后,再次在12行判断了相关pmd表项是否被映射到。这是因为,如果两个缺页中断并发执行,因为锁保护的区域具有原子性,那么只有一个流程能够进到临界区完,此时第一个进入临界区的流程会完成页表的映射操作;而第二个进入临界区的流程进来再次在12行判断时会发现页表已经被映射好了,那么只需要愉快的执行28行和29行代码,完成锁的释放以及pgtable的free操作即可。内核这么做,目的是为了缩短临界区并且处理并发问题。这种处理并发问题的方法,在缺页中断中经常使用,包括pmd_alloc函数、insert_pfn_pmd等,感觉已经成为了一种范式了。 另外,在do_anonymous_page中也是使用这种范式完成pte页表的映射处理,这样做能够极大的缩短临界区。
|