近年,一些本为内核处理的任务,分别出现用户态的实现,有的是为了提升开发灵活性(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,例如:
// userfaultfd系统调用创建并返回一个uffd,类似一个文件的fd
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
然后,所有的注册内存区间、配置和最终的缺页处理等就都需要用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对应的注册操作如下:
// 注册时要用一个struct uffdio_register结构传递注册信息:
// struct uffdio_range {
// __u64 start; /* Start of range */
// __u64 len; /* Length of range (bytes) */
// };
//
// struct uffdio_register {
// struct uffdio_range range;
// __u64 mode; /* Desired mode of operation (input) */
// __u64 ioctls; /* Available ioctl() operations (output) */
// };
addr = mmap(NULL, page_size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0)
// addr 和 len 分别是我匿名映射返回的地址和长度,赋值到uffdio_register
uffdio_register.range.start = (unsigned long) addr;
uffdio_register.range.len = len;
// mode 只支持 UFFDIO_REGISTER_MODE_MISSING
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
// 用ioctl的UFFDIO_REGISTER注册
ioctl(uffd, UFFDIO_REGISTER, &uffdio_register);
STEP 3. 创建一个处理专用的线程轮询和处理”user-fault”事件
要使用userfaultfd,需要创建一个处理专用的线程轮询和处理”user-fault”事件。主进程中就要调用pthread_create
创建这个自定义的handler线程:
// 主进程中调用pthread_create创建一个fault handler线程
pthread_create(&thr, NULL, fault_handler_thread, (void *) uffd);
一个自定义的线程函数举例如下,这里处理的是一个普通的匿名页用户态缺页,我们要做的是把我们一个已有的一个page大小的buffer内容拷贝到缺页的内存地址处。用到了poll
函数轮询uffd
,并对轮询到的UFFD_EVENT_PAGEFAULT
事件(event)用拷贝(ioctl的UFFDIO_COPY
选项)进行处理。
static void * fault_handler_thread(void *arg)
{
// 轮询uffd读到的信息需要存在一个struct uffd_msg对象中
static struct uffd_msg msg;
// ioctl的UFFDIO_COPY选项需要我们构造一个struct uffdio_copy对象
struct uffdio_copy uffdio_copy;
uffd = (long) arg;
......
for (;;) { // 此线程不断进行polling,所以是死循环
// poll需要我们构造一个struct pollfd对象
struct pollfd pollfd;
pollfd.fd = uffd;
pollfd.events = POLLIN;
poll(&pollfd, 1, -1);
// 读出user-fault相关信息
read(uffd, &msg, sizeof(msg));
// 对于我们所注册的一般user-fault功能,都应是UFFD_EVENT_PAGEFAULT这个事件
assert(msg.event == UFFD_EVENT_PAGEFAULT);
// 构造uffdio_copy进而调用ioctl-UFFDIO_COPY处理这个user-fault
uffdio_copy.src = (unsigned long) page;
uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address & ~(page_size - 1);
uffdio_copy.len = page_size;
uffdio_copy.mode = 0;
uffdio_copy.copy = 0;
// page(我们已有的一个页大小的数据)中page_size大小的内容将被拷贝到新分配的msg.arg.pagefault.address内存页中
ioctl(uffd, UFFDIO_COPY, &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 个用于注册、注销的ioctl选项:
UFFDIO_REGISTER 注册将触发user-fault的内存地址
UFFDIO_UNREGISTER 注销将触发user-fault的内存地址
# 3 个用于处理user-fault事件的ioctl选项:
UFFDIO_COPY 用已知数据填充user-fault页
UFFDIO_ZEROPAGE 将user-fault页填零
UFFDIO_WAKE 用于配合上面两项中 UFFDIO_COPY_MODE_DONTWAKE 和
UFFDIO_ZEROPAGE_MODE_DONTWAKE模式实现批量填充
# 1 个用于配置uffd特殊用途的ioctl选项:
UFFDIO_API 它又包括如下feature可以配置:
UFFD_FEATURE_EVENT_FORK (since Linux 4.11)
UFFD_FEATURE_EVENT_REMAP (since Linux 4.11)
UFFD_FEATURE_EVENT_REMOVE (since Linux 4.11)
UFFD_FEATURE_EVENT_UNMAP (since Linux 4.11)
UFFD_FEATURE_MISSING_HUGETLBFS (since Linux 4.11)
UFFD_FEATURE_MISSING_SHMEM (since Linux 4.11)
UFFD_FEATURE_SIGBUS (since Linux 4.14)
(详见 ioctl_userfaultfd man page [2])
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