Look Into Userfaultfd

1)    前言

userfaultfd是Linux Kernel在v4.3中加入的新功能。本文的分析将包含它的背景,使用场景以及内部实现。

在分析userfaultfd之前,需要来了解一下什么叫page fault。Page fault也被称为缺页异常。如果CPU不能进行正常的内存访问,如地址没有映射再如访问的权限不够,就会触发这个异常。该异常在触发后,CPU会跳转到内核自定义的入口,然后由内核来修正这个异常,在修正完page fault以后,CPU会重新执行引发该异常的指令。一个经典的应用场景就是memory swap,它把内存的内容交换到存储设备然后该内存可以变为空闲来满足其它的请求,在以后需要用到这部份数据的时候就会触发page fault,内核会分配内存然后从存储设备上把内容读回来。

userfaultfd简单地来说就是通过文件描述符(fd)的机制来将page fault的信息传递给userspace,然后由userspace来决定要往这个地址写的内容。更简单一点就是,传统的page fault由内核独自完成,现在改为由内核和userspace一起控制。

2)    Userfaultfd的背景

Userfaultfd是为了解决QEMU/KVM live migration的问题而出现的,所谓Live migration就是将guest从一端迁移到另一端,而在迁移的过程中guest能够继续提供服务。当前QEMU/KVM Live migration采用的方案是先将guest的内存迁移到对端然后再来检查在迁移的过程中是否有页面发生更改(也称为脏页),如果有,再把脏页传到对端,一直重复这个过程直到没有脏页或者是脏页的数目足够少,脏页全部迁移过去之后就可以把源端的guest关闭掉,然后启动目的端的guest。这种实现有个术语叫precopy,即在目地端的guest运行前,所有的东西都已经copy过去了。

另一种与之相对的方案就是postcopy,即先把目地端的guest跑起来,当guest运行过程中需要访问尚末迁移的内存的时候才把内存从源端读过来。

precopy和postcopy各有自己的优缺点,如precopy较postcopy有较高的吐吞,而postcopy可以在guest workload较高的情况下能够较快地完全迁移的工作。

Userfaultfd就是为了postcopy而准备的,当guest在目地端运行的时候,目的端的kernel不可能知道要往页面里面填充的内容,它需要用户空间的程序去把内容从远端读过来,然后再把这些内容放到guest的内存中。

3)    Userfaultfd API

幸运的是,我们在使用userfaultfd的时候不需要了解诸如precopy/postcopy这样复杂的场景,内核提供了简单的API来供用户空间使用。

3.1) userfaultfd的流程

在分析API之前,我们先来看一下userfaultfd的使用流程,本文末尾也附带了简单的代码来演示userfaultfd,分了方便分析,暂且将userfaultfd的处理分为这几个阶段。

3.1.1) 准备阶段

userfaultfd

如上图所示,该阶段主要是做一些初始化的工作。所有userfaultfd的操作都是基于userfaultfd系统调用所创建的fd,一些配置的操作采用ioctl的方式来完成。UFFDIO_API用来做API的检测,它为了不同版本间的用户空间application和内核的兼容而存在。最终通过UFFDIO_REGISTER注册了一段region,以用来告诉内核该region中所发生的page fault需要userspace的协助。

3.1.2) region访问阶段

userfaultfd2

在这个阶段,程序访问了注册的region,产生page fault进而被kernel所捕获到,在内核的page fault处理函数中,根据fault的地址以及访问的类型生成一条message放入message pool中,然后把自己睡眠以等待用户空间的处理。这个阶段并未涉及到API的调用。

3.1.3) fault的处理阶段

userfaultfd3

这个阶段是由userspace来处理page fault的过程。在userfaultfd system call返回的fd上进行poll()可用来测检message pool里是否有待处理的message。Read()可以将该message读出来,用户空间可以解析读到的message得到fault的信息,比如fault地址,fault类型等等。根据这些信息,用户可空可以调用UFFDIO_COPY或者UFFDIO_ZEROPAGE来往fault address的区域进行填充,该ioctl带有一个参数可用来控制填充完后是否立即唤醒handler。如果是使用batch wakeup的方式,可在batch copy之后单独调用一次UFFDIO_WAKE。

Handler wakeup之后,会继续修正page fault,然后返回到用户空间,用户空间的内存访问会继续下去。

在这里需要注意的是,第三个阶段不一定要在第二个阶段发生后再进行,先将数据copy再来访问也是可以的,或者可以一边访问一边copy(例如postcopy live migration, guest running 和memory migrate同时进行)。

3.2) API介绍

接下来详细介绍userfaultfd的API。

3.2.1) userfaultfd system call

它是我们面临的第一个API,所有后续的操作都是基于这个API,它的原型如下:

SYSCALL_DEFINE1(userfaultfd,int,flags)

它返回一个文件描述符(fd),可以将它视为将文件open后得到的fd,该系统调用只有一个参数,flags,当前它支持两个flag: O_CLOEXEC和O_NONBLOCK,这两个标志我们在Linux里经常经看到,它也是int open(const char *pathname,int flags)的flags参数中的标志。

O_CLOEXEC:表示close-on-exec,即在exec()系统调用将一个新的执行文件替换当前的环境时,有该标志的fd都会主动被关闭掉。

O_NONBLOCK:表示该fd上对应的操作为非阻塞操作。

调用这个系统调用后,我们完成了第一步,接着要在这个fd上做一些ioctl的配置操作才能enable userfaultfd。

3.2.2) UFFDIO_API Ioctl

UFFDIO_API是一个起始操作,所有其它操作必须要在它之后才能进行,它用来验证用户空间程序的API version是否被kernel所支持。

该API所带的参数如下示:

struct uffdio_api {

/* userland asks for an API number and the features to enable */

__u64 api;

/*

* Kernel answers below with the all available features for

* the API,this notifies userland of which events and/or

* which flags for each event are enabled in the current

* kernel,

*

* Note: UFFD_EVENT_PAGEFAULT and UFFD_PAGEFAULT_FLAG_WRITE

* are to be considered implicitly always enabled in all kernels as

* long as the uffdio_api,api requested matches UFFD_API,

*/

#if 0 /* not available yet */

#define UFFD_FEATURE_PAGEFAULT_FLAG_WP           (1<<0)

#define UFFD_FEATURE_EVENT_FORK                       (1<<1)

#endif

__u64 features;

 

__u64 ioctls;

};

api由用户空间写入,表示其支持的API number,当前所唯一支持的API number是UFFD_API,其值为0xaa。

features: 由内核更新,表示内核支持的feature,当前内核总是将它设为0,从注释中可以看到,有两个feature是默认被支持的,它们是UFFD_EVENT_PAGEFAULT和UFFD_PAGEFAULT_FLAG_WRITE,分别表示用来捕获page fault和该page fault是否是对内存的写操作所导致的。

ioctls: 由内核更新,表示内核支持的ioctl,当前内核将它设置为UFFD_API_IOCTLS,定义如下:

#define UFFD_API_IOCTLS                                     \

((__u64)1 << _UFFDIO_REGISTER |                    \

(__u64)1 << _UFFDIO_UNREGISTER |   \

(__u64)1 << _UFFDIO_API)

表示该fd上所支持的ioctl的操作,_UFFDIO_API就是我们在这节分析的UFFDIO_API Ioctl,其它两个我们接下来马上就可以看到。

3.2.3) UFFDIO_REGISTER Ioctl

在和内核协商好了API以及所支持的feature和ioctl后,就需要告诉内核哪一段内存的page fault是需要userspace来控制,这就是UFFDIO_REGISTER要做的事情。

这个ioctl所带的参数为:

struct uffdio_range {

__u64 start;

__u64 len;

};

 

struct uffdio_register {

struct uffdio_range range;

#define UFFDIO_REGISTER_MODE_MISSING         ((__u64)1<<0)

#define UFFDIO_REGISTER_MODE_WP                    ((__u64)1<<1)

__u64 mode;

 

/*

* kernel answers which ioctl commands are available for the

* range,keep at the end as the last 8 bytes aren’t read,

*/

__u64 ioctls;

};

range为用户空间填写,表示要user page fault区域的地址和大小。

mode为用户空间填写,表示要捕捉的模式,有两种模式被定义:

  •  UFFDIO_REGISTER_MODE_MISSING,要捕捉的page fault是因为页表没有映射导致的。
  •  UFFDIO_REGISTER_MODE_WP,要捕捉的page fault是因为在只读的内存上进行写操作所导致的。

当前内核只支持第一种,即UFFDIO_REGISTER_MODE_MISSING。

ioctl由内核来填写,表示内核在该range上所支持的ioctl操作,当前内核支持的操作为:

#define UFFD_API_RANGE_IOCTLS                              \

((__u64)1 << _UFFDIO_WAKE |                 \

(__u64)1 << _UFFDIO_COPY |                   \

(__u64)1 << _UFFDIO_ZEROPAGE)

我们在接下来的分析里会看到这三个ioctl所对应的含义。

3.2.4) UFFDIO_UNREGISTER ioctl

它是UFFDIO_REGISTER的反操作,用来撤消对某段range的捕捉,它所带的参数很简单,就是我们在上面看到的struct uffdio_range,只需要指定该段的起始地址和大小就可以了。

好了,现在到了来看range上的Ioctl操作的时候了,从上面的分析中可以看到,它支持三种类型的操作,我们依次来看他们的含义。

3.2.5) UFFDIO_WAKE ioctl

如果一个page fault需要由userspace来处理,就会把当前进程加入一个等待队列来等待userspace的操作,当userspace操作完了之后就会调用这个ioctl来将其唤配,它所带的参数结构为struct uffdio_range,即将在该range等待的page fault操作唤醒。

3.2.6) UFFDIO_COPY ioctl

这个ioctl用来告诉内核要往指定的range里要填充的内容,它的参数如下:

struct uffdio_copy {

__u64 dst;

__u64 src;

__u64 len;

/*

* There will be a wrprotection flag later that allows to map

* pages wrprotected on the fly,And such a flag will be

* available if the wrprotection ioctl are implemented for the

* range according to the uffdio_register,ioctls,

*/

#define UFFDIO_COPY_MODE_DONTWAKE                     ((__u64)1<<0)

__u64 mode;

 

/*

* “copy” is written by the ioctl and must be at the end: the

* copy_from_user will not read the last 8 bytes,

*/

__s64 copy;

};

dst: 要copy的目的地址。

src: 要copy的源地址。

len: 要copy的数据总大小。

mode: 当前只支持一个flag,即UFFDIO_COPY_MODE_DONTWAKE,表示在copy数据后不需要去唤醒page fault handler, 在需要batch copy的时候,可以设置此标志,等所有数据copy完成之后,再调用一次UFFDIO_WAKE ioctl即可。

copy: 由内核填写,表示已经copy的数据量,当内核一次不能完全所有的copy的时候,它将已经完成的量写入copy中,并且返回-EAGAIN通知userspace将剩余的数据写进去。

3.2.7) UFFDIO_ZEROPAGE ioctl

该ioctl与UFFDIO_COPY ioctl类似,只不过要copy的数据全是0,它的参数结构如下:

struct uffdio_zeropage {

struct uffdio_range range;

#define UFFDIO_ZEROPAGE_MODE_DONTWAKE                    ((__u64)1<<0)

__u64 mode;

 

/*

* “zeropage” is written by the ioctl and must be at the end:

* the copy_from_user will not read the last 8 bytes,

*/

__s64 zeropage;

};

因为要copy的数据全是0,在这里没有必要再从source address中去找数据了,只需要range表示它所操作的目的地址即可,其余的成员和UFFDIO_COPY ioctl类似。

4)    Userfaultfd的实现

在前面的分析中,看到了Userfaultfd的使用流程,接下来我们从代码的层面来分析几个核心的事件和几个关键的API。

4.1) userfaultfd system call

该system call跟文件open的流程很相似,无非是分配fd,然后生成file,再将file和fd关联起来,在这里值的注意的代码在userfaultfd_file_create()函数中:

1279

1280         atomic_set(&ctx->refcount,1);

1281         ctx->flags = flags;

1282         ctx->state = UFFD_STATE_WAIT_API;

1283         ctx->released = false;

1284         ctx->mm = current->mm;

1285         /* prevent the mm struct to be freed */

1286         atomic_inc(&ctx->mm->mm_users);

1287

1288         file = anon_inode_getfile(“[userfaultfd]”,&userfaultfd_fops,ctx,

1289                                   O_RDWR | (flags & UFFD_SHARED_FCNTL_FLAGS));

1290         if (IS_ERR(file)) {

1291                 mmput(ctx->mm);

1292                 kmem_cache_free(userfaultfd_ctx_cachep,ctx);

1293         }

1294 out:

1295         return file;

 

在1288行中,定义了该fd上的所有操作,它们存放在userfaultfd_fops中,而且在这里可以看到,创建的file对应的private field 为ctx,它用来表示userfaultfd的上下文。

4.2) UFFDIO_REGISTER

从userfaultfd_fops顺藤摸瓜可以找到ioctl对应的函数,很容易可以找到该API是在userfaultfd_register()中实现的,该函数的前面一段是用来进行validation的check,有几个细节需要注意一下:

730         ret = -EINVAL;

731         if (!uffdio_register,mode)

732                 goto out;

733         if (uffdio_register,mode & ~(UFFDIO_REGISTER_MODE_MISSING|

734                                      UFFDIO_REGISTER_MODE_WP))

735                 goto out;

736         vm_flags = 0;

737         if (uffdio_register,mode & UFFDIO_REGISTER_MODE_MISSING)

738                 vm_flags |= VM_UFFD_MISSING;

739         if (uffdio_register,mode & UFFDIO_REGISTER_MODE_WP) {

740                 vm_flags |= VM_UFFD_WP;

741                 /*

742                  * FIXME: remove the below error constraint by

743                  * implementing the wprotect tracking mode,

744                  */

745                 ret = -EINVAL;

746                 goto out;

747         }

748

从731-733行可以看到,mode只支持UFFDIO_REGISTER_MODE_MISSING和UFFDIO_REGISTER_MODE_WP且至少要指定一个。

730-747可以看到,UFFDIO_REGISTER_MODE_WP暂时还没有enable (有部份代码的实现但并不完整)。

接下来validate_range()会对range的合法性进行检查,它的代码比较简单,跟踪代码进去可以看到,它要求给定的region是需要页面大小对齐的,这里因为page fault的最小粒度就是PAGE_SIZE。

758         vma = find_vma_prev(mm,start,&prev);

759

760         ret = -ENOMEM;

761         if (!vma)

762                 goto out_unlock;

763

764         /* check that there’s at least one vma in the range */

765         ret = -EINVAL;

766         if (vma->vm_start >= end)

767                 goto out_unlock;

 

这一段的逻辑很简单,就是查看api所指的range是否包含在VMA映射中,如果没有找到VMA则认为是非法。

从这里可以看到,userfaultfd的region 必须要至少包含一个合法的地址区间,所谓合法,就是这个region是需要事先malloc好的或者是在程序加载的时候就已经创建好的,我们并不能期望userfaultfd会为我们分配好内存供我们使用,这也就是示例代码中需要调用posix_memalign()来分配一段内存的原因。

776         found = false;

777         for (cur = vma; cur && cur->vm_start < end; cur = cur->vm_next) {

778                 cond_resched();

779

780                 BUG_ON(!!cur->vm_userfaultfd_ctx,ctx ^

781                        !!(cur->vm_flags & (VM_UFFD_MISSING | VM_UFFD_WP)));

782

783                 /* check not compatible vmas */

784                 ret = -EINVAL;

785                 if (cur->vm_ops)

786                         goto out_unlock;

787

788                 /*

789                  * Check that this vma isn’t already owned by a

790                  * different userfaultfd,We can’t allow more than one

791                  * userfaultfd to own a single vma simultaneously or we

792                  * wouldn’t know which one to deliver the userfaults to,

793                  */

794                 ret = -EBUSY;

795                 if (cur->vm_userfaultfd_ctx,ctx &&

796                     cur->vm_userfaultfd_ctx,ctx != ctx)

797                         goto out_unlock;

798

799                 found = true;

800         }

801         BUG_ON(!found);

这一段代码用来判断这一段range能否允许被userfault,它有两个限制:

  • 785行可以看到,如果有cur->vm_ops操作,则不被允许,直观一点说,就是只有anonymous的memory才被允许,所以hugetlbfs是不支持userfaultfd的。感兴趣的话,可以把示例程序中的’dst’改为一个全局的静态数组,看看会发生什么情况。
  • 795行可以看到,一段region只能被一个userfaultfd注册,那就是说,不允许一个region在多个userfaultfd system call的fd上注册。

接下面的来的代码就是vma是否可以合并,如果region是span一个vma的,就需要把它split。

然后把userfaultfd的信息设置 到VMA上,如下示:

846                 /*

847                  * In the vma_merge() successful mprotect-like case 8:

848                  * the next vma was merged into the current one and

849                  * the current one has not been updated yet,

850                  */

851                 vma->vm_flags = new_flags;

852                 vma->vm_userfaultfd_ctx,ctx = ctx;

从vm_flags中可以找到被捕获的信息, vma->vm_userfaultfd_ctx,ctx可以用来确定userfaultfd的属主。

4.2) page fault handler

这是一个核心的操作,我们以处理小页面的page fault为例来看看它的处理,在mm/memory.c中:

2718         if (!pte_none(*page_table))

2719                 goto release;

2720

2721         /* Deliver the page fault to userland,check inside PT lock */

2722         if (userfaultfd_missing(vma)) {

2723                 pte_unmap_unlock(page_table,ptl);

2724                 mem_cgroup_cancel_charge(page,memcg);

2725                 page_cache_release(page);

2726                 return handle_userfault(vma,address,flags,

2727                                         VM_UFFD_MISSING);

2728         }

从2718行可以看到,pte (PTE是x86的页表映射中的最后一层映射) 没有映射的话才会继续往下走,这符合UFFDIO_REGISTER_MODE_MISSING的语义。

2722行到2726行,如果该vma需要捕足UFFDIO_REGISTER_MODE_MISSING,就会调用handle_userfault()进行处理。

在THP (transparent huge page )中也有类似的处理,感兴趣的可以在其它找到handle_userfault的入口点。

来看看handle_userfault()做了些什么,

316         /*

317          * Handle nowait,not much to do other than tell it to retry

318          * and wait,

319          */

320         ret = VM_FAULT_RETRY;

321         if (flags & FAULT_FLAG_RETRY_NOWAIT)

322                 goto out;

这段代码只是遵守了FAULT_FLAG_RETRY_NOWAIT的语义,之所义在这里单独拿出来分析是因为有必要介绍一下它出现的背景。

FAULT_FLAG_RETRY_NOWAIT的语义是,如果修正这次page fault需要睡眠,比如需要swap page in,那就不需要等待,直接返回即可,可能有人就会有疑问,直接返回的话,page fault没有被fix,这个时候会导致引起fault的指令一直被重试,进而导致CPU一直在做无用的事情。嗯,确实会这样,于是这个标志只用在”伪造”的page fault中。所谓伪造,是指这个page fault并不是由内存访问直接引起的,而是由GUP (get_user_pages())的操作引起的,该操作会遍历进程的页表,如果页表映射有异常,就尝试对它进行修正。

KVM使用这个flag实现了async page fault的功能,它的原理很简单,就是vCPU在访问内存时,如果该内存在host上没有映射且host需要较长的时间才能将内存准备好的时候,直接返回,再把这个等待的操作交给后台线程去处理。它的好处表现在,此时vCPU还是可以响应IO events的。设想一下,如果期间有一个guest 的tick clock触发,这个vCPU响应,然后guest 的scheduler调度一个新的进程来运行,那么之前引起page fault的指令就不会在短时间内再次被触发,于是提高了guest的吞吐。

327         init_waitqueue_func_entry(&uwq,wq,userfaultfd_wake_function);

328         uwq,wq,private = current;

329         uwq,msg = userfault_msg(address,flags,reason);

330         uwq,ctx = ctx;

331

332         return_to_userland = (flags & (FAULT_FLAG_USER|FAULT_FLAG_KILLABLE)) ==

333                 (FAULT_FLAG_USER|FAULT_FLAG_KILLABLE);

334

335         spin_lock(&ctx->fault_pending_wqh,lock);

336         /*

337          * After the __add_wait_queue the uwq is visible to userland

338          * through poll/read(),

339          */

340         __add_wait_queue(&ctx->fault_pending_wqh,&uwq,wq);

341         /*

342          * The smp_mb() after __set_current_state prevents the reads

343          * following the spin_unlock to happen before the list_add in

344          * __add_wait_queue,

345          */

346         set_current_state(return_to_userland ? TASK_INTERRUPTIBLE :

347                           TASK_KILLABLE);

348         spin_unlock(&ctx->fault_pending_wqh,lock);

这段代码初始化好了wait queue,准备把自己投入睡眠,return_to_userland 用来表示该page fault是否由user space触发,需要留心的是,page fault hander把自己加入了ctx->fault_pending_wqh 中。

userfault_msg()用来生成了一个message,这条message会把read()操作读取到,message的格式如下:

/* read() structure */

struct uffd_msg {

__u8          event;

 

__u8          reserved1;

__u16       reserved2;

__u32       reserved3;

 

union {

struct {

__u64       flags;

__u64       address;

} pagefault;

 

struct {

/* unused reserved fields */

__u64       reserved1;

__u64       reserved2;

__u64       reserved3;

} reserved;

} arg;

} __packed;

event表示对应的事情,当前只支持page fault,即UFFD_EVENT_PAGEFAULT。

address表示fault的地址

flags表示fault的原因,当前支持两种,a) UFFD_PAGEFAULT_FLAG_WRITE,该page fault是由于写操作造成的,b) UFFD_PAGEFAULT_FLAG_WP,表示region被写保护。

接着往下看:

350         must_wait = userfaultfd_must_wait(ctx,address,flags,reason);

351         up_read(&mm->mmap_sem);

352

353         if (likely(must_wait && !ACCESS_ONCE(ctx->released) &&

354                    (return_to_userland ? !signal_pending(current) :

355                     !fatal_signal_pending(current)))) {

356                 wake_up_poll(&ctx->fd_wqh,POLLIN);

357                 schedule();

358                 ret |= VM_FAULT_MAJOR;

359         }

userfaultfd_must_wait()用于在释放mmap_sem之前检查fault address对应的页表是否已经被fix好了,因为可能一个thread在访问region的memory,另一个线程正在做UFFDIO_COPY|ZEROPAGE。

如果对应的进程没有被pending的信号,那就将自己投入睡眠了,记得在睡眠之前对poll进行唤醒,因为已经有被pending的message了。

4.3) poll操作

其对应的代码如下:

506         switch (ctx->state) {

507         case UFFD_STATE_WAIT_API:

508                 return POLLERR;

509         case UFFD_STATE_RUNNING:

510                 /*

511                  * poll() never guarantees that read won’t block,

512                  * userfaults can be waken before they’re read(),

513                  */

514                 if (unlikely(!(file->f_flags & O_NONBLOCK)))

515                         return POLLERR;

516                 /*

517                  * lockless access to see if there are pending faults

518                  * __pollwait last action is the add_wait_queue but

519                  * the spin_unlock would allow the waitqueue_active to

520                  * pass above the actual list_add inside

521                  * add_wait_queue critical section,So use a full

522                  * memory barrier to serialize the list_add write of

523                  * add_wait_queue() with the waitqueue_active read

524                  * below,

525                  */

526                 ret = 0;

527                 smp_mb();

528                 if (waitqueue_active(&ctx->fault_pending_wqh))

529                         ret = POLLIN;

530                 return ret;

531         default:

532                 BUG();

533         }

507行用来check userfaultfd的状态机,只有在API ioctl之后才能进行其它操作。

从514行可以看到,仅仅O_NONBLOCK的fd才能被poll。

528行: 如果有page fault handler在等待,通知用户POLLIN,否则返回0继续等待。

4.4) read操作

542

543         /* always take the fd_wqh lock before the fault_pending_wqh lock */

544         spin_lock(&ctx->fd_wqh,lock);

545         __add_wait_queue(&ctx->fd_wqh,&wait);

546         for (;;) {

547                 set_current_state(TASK_INTERRUPTIBLE);

548                 spin_lock(&ctx->fault_pending_wqh,lock);

549                 uwq = find_userfault(ctx);

 

545行准备好自己的wait queue,因为在读不到数据的时候可能需要将自己睡眠,

549行从page fault handler的等待队列中取出正在等待的handler。

 

550                 if (uwq) {

551                         /*

552                          * Use a seqcount to repeat the lockless check

553                          * in wake_userfault() to avoid missing

554                          * wakeups because during the refile both

555                          * waitqueue could become empty if this is the

556                          * only userfault,

557                          */

558                         write_seqcount_begin(&ctx->refile_seq);

559

560                         /*

561                          * The fault_pending_wqh,lock prevents the uwq

562                          * to disappear from under us,

563                          *

564                          * Refile this userfault from

565                          * fault_pending_wqh to fault_wqh,it’s not

566                          * pending anymore after we read it,

567                          *

568                          * Use list_del() by hand (as

569                          * userfaultfd_wake_function also uses

570                          * list_del_init() by hand) to be sure nobody

571                          * changes __remove_wait_queue() to use

572                          * list_del_init() in turn breaking the

573                          * !list_empty_careful() check in

574                          * handle_userfault(),The uwq->wq,task_list

575                          * must never be empty at any time during the

576                          * refile,or the waitqueue could disappear

577                          * from under us,The “wait_queue_head_t”

578                          * parameter of __remove_wait_queue() is unused

579                          * anyway,

580                          */

581                         list_del(&uwq->wq,task_list);

582                         __add_wait_queue(&ctx->fault_wqh,&uwq->wq);

583

584                         write_seqcount_end(&ctx->refile_seq);

585

586                         /* careful to always initialize msg if ret == 0 */

587                         *msg = uwq->msg;

588                         spin_unlock(&ctx->fault_pending_wqh,lock);

589                         ret = 0;

590                         break;

如果取到了数据,把他移动到另一个等待队列中,这样避免下一次read还会读到同一个数据,然后将message取出,返回给用户空间。

592                 spin_unlock(&ctx->fault_pending_wqh,lock);

593                 if (signal_pending(current)) {

594                         ret = -ERESTARTSYS;

595                         break;

596                 }

597                 if (no_wait) {

598                         ret = -EAGAIN;

599                         break;

600                 }

601                 spin_unlock(&ctx->fd_wqh,lock);

602                 schedule();

603                 spin_lock(&ctx->fd_wqh,lock);

 

593行:如果有信号被pending,返回-ERESTARTSYS让用户空间处理完信号后重试。

597行:如果是no blocking的fd,直接返回。

其余情况就将自己投入睡眠。

4.5) UFFDIO_WAKE

UFFDIO_WAKE用于唤醒正在等待的page fault handler,有两种情况:

  • 还没有被read的handler,此时它位于ctx->fault_pending_wqh中。
  • 有被read的,但是尚未唤醒的handler,read()将它移至了ctx->fault_wqh中。

知道了这一点,wake_userfault()的代码就很简单了:

675         /*

676          * Use waitqueue_active because it’s very frequent to

677          * change the address space atomically even if there are no

678          * userfaults yet,So we take the spinlock only when we’re

679          * sure we’ve userfaults to wake,

680          */

681         do {

682                 seq = read_seqcount_begin(&ctx->refile_seq);

683                 need_wakeup = waitqueue_active(&ctx->fault_pending_wqh) ||

684                         waitqueue_active(&ctx->fault_wqh);

685                 cond_resched();

686         } while (read_seqcount_retry(&ctx->refile_seq,seq));

687         if (need_wakeup)

688                 __wake_userfault(ctx,range);

 

它检查的就是这两个队列。

4.6) UFFDIO_COPY

同UFFDIO_ZEROPAGE一样,核心的操作是在mcopy_atomic()中完成的,该函数代码较长但是逻辑很简单,就是一层层遍历页表,如果页表异常则修正它,最后将数据写入到建好的指向的页面,对于ZEROPAGE来说,直接将它指向零页(zero page)。

5:小结

现在看起来userfaultfd的API比较简单,实现逻辑也很清晰。但它进入upstream kernel花了相当长的一段时间,或许讲讲它的开发过程中API的变化和实现方式的变化会更加有意思。把它做为下一个topic?或许吧,如果我还记得。:)

 

PS:

示例代码: userfaultfd.c

在编译之前需要在内核源代码下运行:

make headers_install INSTALL_HDR_PATH=/usr

来将新的内核头文件同步到系统头文件目录下。

1 thought on “Look Into Userfaultfd

发表评论

电子邮件地址不会被公开。

You may use these HTML tags and attributes:

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> 

*