从内核到用户空间(1) — 用户态缺页处理机制 userfaultfd 的使用

近年,一些本为内核处理的任务,分别出现用户态的实现,有的是为了提升开发灵活性(FUSE、userfaultfd),有的则是为了提高与外设通信的性能(SPDK、DPDK)。本系列文章对我所了解到的用户空间实现的内核机制进行使用介绍或原理分析。第一篇文章介绍用户态的缺页处理 — userfaultfd机制,以后还可能根据我的学习进度介绍userfaultfd的内核实现原理、FUSE的使用和原理、SPDK等内容。文章若有错误,恳请指正。

userfaultfd 机制让在用户控制缺页处理提供可能,进程可以在用户空间为自己的程序定义page fault handler,增加了灵活性,但也可能由于类似FUSE之于内核FS的问题(调用层次加深)而影响性能。

1. 基本使用步骤

以最基本的用户空间进行匿名页缺页处理为例,(例子代码基本来自userfaultfd的man page[1],)步骤大致如下:

STEP 1. 创建一个描述符uffd

要使用此功能,首先应该用userfaultfd调用[1]来创建一个fd,例如:

然后,所有的注册内存区间、配置和最终的缺页处理等就都需要用ioctl来对这个uffd操作。ioctl-userfaultfd[2]支持UFFDIO_API、UFFDIO_REGISTER、UFFDIO_UNREGISTER、UFFDIO_COPY、UFFDIO_ZEROPAGE、UFFDIO_WAKE等选项。比如UFFDIO_REGISTER用来向userfaultfd机制注册一个监视区域,这个区域发生缺页时,需要用UFFDIO_COPY来向缺页的地址拷贝自定义数据。

STEP 2. 用ioctl的UFFDIO_REGISTER选项注册监视区域

比如,UFFDIO_REGISTER对应的注册操作如下:

STEP 3. 创建一个处理专用的线程轮询和处理”user-fault”事件

要使用userfaultfd,需要创建一个处理专用的线程轮询和处理”user-fault”事件。主进程中就要调用pthread_create创建这个自定义的handler线程:

一个自定义的线程函数举例如下,这里处理的是一个普通的匿名页用户态缺页,我们要做的是把我们一个已有的一个page大小的buffer内容拷贝到缺页的内存地址处。用到了poll函数轮询uffd,并对轮询到的UFFD_EVENT_PAGEFAULT事件(event)用拷贝(ioctl的UFFDIO_COPY选项)进行处理。

在userfaultfd man page[1]及内核源码中的测试文件KERNEL_SRC/linux-4.18.8/tools/testing/selftests/vm/userfaultfd.c中,分别关于userfaultfd系统调用有两个例程。上述最一般的“缺页-用户态拷贝数据”例子源自前者中的例程;后者中的例程则涵盖了目前内核中user-fault机制的所有选项和功能。

2. 其他的ioctl选项

目前为止,user-fault机制支持UFFDIO_REGISTER、UFFDIO_UNREGISTER、UFFDIO_COPY、UFFDIO_ZEROPAGE、UFFDIO_WAKE、UFFDIO_API等五种选项,分别用于注册、配置或处理用户态缺页功能如下。

2.1. ioctl-UFFDIO_API选项的最新特性

值得注意的是,4.11后的UFFDIO_API选项。UFFDIO_API提供的features,让匿名(anonymous)页之外的hugetlbfs、shared-memory(shmem)页也得到了支持;也提供了对”non-cooperative events”的支持,包括mapping、unmapping、fork()、remove等操作[3]。我理解,这里的non-cooperative events指user-fault handler(处理程序)对产生fork/madvise/mremap等事件的进程是透明的,user-fault handler读取到这些事件后,产生事件的进程就会继续进行[4],而不会被阻塞。

UFFD_FEATURE_SIGBUS是最新被加入的。加入它最初的目的是,很多数据库系统采用hugetlbfs中的大页文件,并且这些文件时(带洞的)稀疏文件,当数据库程序有bug时,可能错误地将洞进行mmap,这会导致内核尝试自动地填洞最终导致文件非预期地扩大,为了让bug“误触”到文件洞时直接返回SIGBUS信号,UFFD_FEATURE_SIGBUS被提出[5]。这个feature不需要对应的user-fault handler处理线程。

总之,要正确理解这些较新的features,还是推荐看一下KERNEL_SRC/tools/testing/selftests/vm/userfaultfd.c代码及最新的用户文档。

此特性目前只对匿名页、shmem以及hugetlb等页支持,内核中对应的handle_userfault函数可能被这几部分的page fault handler所调用,普通文件映射的mmap暂时不支持userfault。

3. 应用

userfaultfd 最广泛的应用时虚拟机或者进程的迁移。

3.1. 虚拟机动态迁移中的post-copy模式

虚拟机的动态迁移(live migration)即不停机的中包括两类,一类称为pre-copy,另一类称为post-copy。 其中post-copy就可以用userfaultfd进行实现。

pre-copy更为人熟知,源到目的端拷贝是多轮迭代进行的,第一轮拷贝所有的数据到目的端,第二轮拷贝第一轮拷贝过程中发生更改的数据,第三轮拷贝第二轮拷贝过程中发生更改的数据…… 直到某一轮需要拷贝的数据量小于一定的阈值,暂停一下源端虚拟机将最后需要传输的这些少量数据传到目的端,最后启动目的端虚拟机。

而post-copy则不同,暂停在发生在最开始,因为只需要传输和VM运行状态相关的很少数据,如CPU状态、寄存器、无页表内存等 [6],之后就运行目的端VM,其他的内存页则是按需进行传输的。这时,userfaultfd机制就起到了作用,它让远程传输可以参与到缺页异常处理流程中。下图[7]展示了基于userfaultfd的post-copy流程:

3.2. CRIU的lazy restore和lazy migration特性

CRIU用于进程状态的保存、迁移和恢复。其lazy restore和lazy migration功能与虚拟机动态迁移的post-copy道理很类似,下图为其流程图,详见其文档[8]。


[1] USERFAULTFD(2), http://man7.org/linux/man-pages/man2/userfaultfd.2.html

[2] IOCTL_USERFAULTFD(2), http://man7.org/linux/man-pages/man2/ioctl_userfaultfd.2.html

[3] The next steps for userfaultfd(), https://lwn.net/Articles/718198/

[4] [PATCH 0/3] userfaultfd: non-cooperative: syncronous events, https://lkml.org/lkml/2018/2/27/78

[5] [RFC PATCH v2] userfaultfd: Add feature to request for a signal delivery, https://marc.info/?l=linux-mm&m=149857975906880&w=2

[6] Live migration – Wikipedia, https://en.wikipedia.org/wiki/Live_migration

[7] Userfaultfd: Post-copy VM migration and beyond, https://blog.linuxplumbersconf.org/2017/ocw//system/presentations/4699/original/userfaultfd_%20post-copy%20VM%20migration%20and%20beyond.pdf

[8] Userfaultfd – CRIU Documents, https://criu.org/Userfaultfd

发表评论

电子邮件地址不会被公开。 必填项已用*标注