linux缺页中断的并发设计

缺页中断处理函数中,为了实现高效的并发操作,做了不少优化工作,使得锁的粒度非常小,并且很好的解决了多个线程对同一地址空间的并发处理问题。本文简单讲解一下我对缺页中断并发处理的几个小技巧的理解。本文继续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中处理。

/*
 * 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().
 */
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); //获取pmd表项
	if (!vmf.pmd)
		return VM_FAULT_OOM;
	if (pmd_none(*vmf.pmd) && __transparent_hugepage_enabled(vma)) {
    
     //pmd为空且支持thp
		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); //COW
				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页表的映射处理,这样做能够极大的缩短临界区。

猜你喜欢

转载自blog.csdn.net/kaka__55/article/details/122372147