最近工作中经常遇见get_user_pages和get_user_pages_fast,虽然知道他们都是用来pin住一个页的,但是依然没搞明白后者是如何实现fast的,两者的区别具体在哪。刚好利用周末时间研究一下。本文的分析基于linux 4.19.195.
先看看get_user_pages_fast函数的定义

/**
 * get_user_pages_fast() - pin user pages in memory
 * @start:	starting user address
 * @nr_pages:	number of pages from start to pin
 * @write:	whether pages will be written to
 * @pages:	array that receives pointers to the pages pinned.
 *		Should be at least nr_pages long.
 *
 * Attempt to pin user pages in memory without taking mm->mmap_sem.   尝试不获取锁就pin memory
 * If not successful, it will fall back to taking the lock and
 * calling get_user_pages().
 *
 * Returns number of pages pinned. This may be fewer than the number
 * requested. If nr_pages is 0 or negative, returns 0. If no pages
 * were pinned, returns -errno.
 */
int get_user_pages_fast(unsigned long start, int nr_pages, int write,
			struct page **pages)

get_user_pages的定义

/*
 * This is the same as get_user_pages_remote(), just with a
 * less-flexible calling convention where we assume that the task
 * and mm being operated on are the current task's and don't allow
 * passing of a locked parameter.  We also obviously don't pass
 * FOLL_REMOTE in here.
 */
long get_user_pages(unsigned long start, unsigned long nr_pages,
		unsigned int gup_flags, struct page **pages,
		struct vm_area_struct **vmas)
{
	return __get_user_pages_locked(current, current->mm, start, nr_pages,
				       pages, vmas, NULL,
				       gup_flags | FOLL_TOUCH);
}
EXPORT_SYMBOL(get_user_pages);

static __always_inline long __get_user_pages_locked(struct task_struct *tsk,
						struct mm_struct *mm,
						unsigned long start,
						unsigned long nr_pages,
						struct page **pages,
						struct vm_area_struct **vmas,
						int *locked,
						unsigned int flags)

可以看到,get_user_pages_fast函数的传参更少,get_user_pages函数只是__get_user_pages_locked函数的一个封装,而且参数也多了一些。先分析get_user_pages_fast函数。

int get_user_pages_fast(unsigned long start, int nr_pages, int write,
			struct page **pages)
{
	***
	/*
	 * The FAST_GUP case requires FOLL_WRITE even for pure reads,
	 * because get_user_pages() may need to cause an early COW in
	 * order to avoid confusing the normal COW routines. So only
	 * targets that are already writable are safe to do by just
	 * looking at the page tables.
	 */
	if (gup_fast_permitted(start, nr_pages, write)) {
		local_irq_disable();
		gup_pgd_range(addr, end, 1, pages, &nr);
		local_irq_enable();
		ret = nr;
	}

	if (nr < nr_pages) { //如果gup_pgd_rang没有获取到足够多的page数量,重来;原因可关注gup_pte_range函数,里面有对pte_value的并发判断
		/* Try to get the remaining pages with get_user_pages */
		start += nr << PAGE_SHIFT;
		pages += nr;

		ret = get_user_pages_unlocked(start, nr_pages - nr, pages,
				write ? FOLL_WRITE : 0); //最终调用__get_user_pages_locked完成

		/* Have to be a bit careful with return values */
		if (nr > 0) {
			if (ret < 0)
				ret = nr;
			else
				ret += nr;
		}
	}

	return ret;
}

可以看到,get_user_pages_fast函数先调用了gup_pgd_range,这个函数就是gup的快速路径,如果这个函数失败了,会走慢速路径,也就是get_user_pages_unlocked,并最终调用到了__get_user_pages_locked。那么,快速路径是在什么情况下能够快的呢?
走进gup_pgd_range函数的实现里,我们能够看到,gup_pgd_range就是在关中断的情况下(这样在多核的情况下无法做到并发安全),遍历一下页表,这个函数展开太长了就不贴了。拿简单的pin一个4k页举例,如果每一级页表都做了映射,且相关页都被分配了的话,最终会走入到函数gup_pte_range中,增加了相关的page引用计数后,便将页记录在pages数组里,计数加一,便可退出了。
可见,fast路径是简历在页表以及实际物理页都被分配了的基础上,方可成功执行。
但凡其中一个页的页表或者物理页没有分配好,就会走到慢速路径。
我们可以看到,慢速路径就和get_user_pages基本一样了(几个传参的区别请忽略,因为并不影响函数实现的本质),除了get_user_pages需要调用者自己在函数外拿一下mm->mmap_sem的读锁。
我们看看__get_user_pages_locked的原理,一个无限循环调用__get_user_pages函数,直到所有要求的页都被pin住或者错误发生。那__get_user_pages函数和上述快速路径有什么区别呢?
本质上,两者都是通过查询页表找到的page结构体,但是若是页表或者物理页尚未创建成功,那么,__get_user_pages会通过faultin_page模拟一个缺页中断,完成相关物理页及页表建立,这个流程是快速路径没有做的。
可能还有些细节上的差异,因为没有实际问题的触发,就没有深入研究下去了。
总结一下,大体上,get_user_pages和get_user_pages_fast,都是通过查询页表实现pin page的效果;两者的区别是,get_user_pages_fast只能pin住页表和物理页都建立好的page,而get_user_pages在相关页表或物理页尚未建立的情况下,能够主动的去创建,然后再执行pin的动作。
指的一提的是,get_user_pages_fast的快速路径是通过try_get_compound_head函数增加相关page结构体的引用计数的,而get_user_pages是通过FOLL_TOUCH这个标志位来给相关page结构体做引用计数的加一操作。也就是说,通过适当的传参,gup函数并不一定能pin住页,pin住页只是他的其中一项功能罢了。