文章目录
-
重要:本系列文章内容摘自
<Linux内核深度解析>
基于ARM64架构的Linux4.x内核一书,作者余华兵。系列文章主要用于记录Linux内核的大部分机制及参数的总结说明
1 用户页错误文件描述符
userfaultfd(用户页错误文件描述符)用来拦截和处理用户空间的页错误异常,内核通过文件描述符将页错误异常的信息传递给用户空间,然后由用户空间决定要往虚拟页写入的数据。传统的页错误异常由内核独自处理,现在改为由内核和用户空间一起控制。
userfaultfd是为了解决QEMU/KVM虚拟机动态迁移的问题而出现的。所谓动态迁移,就是将虚拟机从一端迁移到另一端,而在迁移的过程中虚拟机能够继续提供服务,有两种实现方案:
(1)前复制(precopy)方案:这种方案在目地端的虚拟机运行前把所有的数据复制过去。先将虚拟机的内存迁移到对端,再检查在迁移的过程中是否有页面发生更改(即脏页)。如果有,就把脏页传到对端,一直重复这个过程,直到没有脏页或者脏页的数目足够少。脏页全部迁移过去之后,就可以把源端的虚拟机关闭掉,然后启动目的端的虚拟机。
(2)后复制(postcopy)方案:先让目地端的虚拟机运行起来,当虚拟机在运行过程中需要访问尚未迁移的内存时才把内存从源端读过来。
后复制方案和前复制方案各有自己的优缺点,如前复制方案有较高的吞吐率,而后复制方案可以在虚拟机工作负载较高的情况下能够较快地完成迁移工作。
userfaultfd是为后复制方案准备的,当虚拟机在目地端运行的时候,目的端的内核不可能知道要往页里面填充的内容,需要借助用户空间的程序来把内容从远端读过来,然后把这些内容放到虚拟机的内存中。
1.1 使用方法
应用程序使用userfaultfd跟踪处理页错误异常的方法如下:
(1)使用系统调用userfaultfd创建一个文件描述符。
int userfaultfd(int flags);
参数flags可以是0或以下标志的组合。
1)O_CLOEXEC:使用execve装载新程序时关闭文件描述符。
2)O_NONBLOCK:非阻塞模式。
其代码如下:
int uffd;
uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
(2)使用控制命令UFFDIO_API请求验证版本号和启用某些特性,如果内核userfaultfd的版本号是请求的版本号,并且支持请求启用的所有特性,那么成功启用userfaultfd,返回内核支持的所有特性和控制命令。
参数的数据类型如下:
struct uffdio_api {
__u64 api;
__u64 features;
__u64 ioctls;
};
调用者使用成员api指定版本号,使用成员features指定请求启用的特性,成员features为0表示启用默认特性。如果成功启用userfaultfd,那么成员features返回内核支持的所有特性,成员ioctls返回内核支持的所有控制命令。
默认启用的特性是跟踪普通页的页错误异常,userfaultfd还支持以下特性:
1)UFFD_FEATURE_PAGEFAULT_FLAG_WP表示跟踪类型为“写只读页”的页错误异常。
2)UFFD_FEATURE_EVENT_FORK表示启用父进程跟踪子进程的页错误异常:父进程调用fork创建子进程,把userfaultfd跟踪的虚拟内存区域复制给子进程的时候,为子进程的相同虚拟内存区域创建新的userfaultfd上下文,父进程收到事件UFFD_EVENT_FORK和新的userfaultfd上下文的文件描述符,使用文件描述符跟踪处理子进程的页错误异常。
3)UFFD_FEATURE_EVENT_REMAP表示启用mremap调用的通知。当进程使用mremap把一个虚拟内存区域移到不同位置的时候,userfaultfd收到事件UFFD_EVENT_REMAP,uffd_msg.remap包含虚拟内存区域的旧地址、新地址和旧长度。
4)UFFD_FEATURE_EVENT_REMOVE表示启用madvise(MADV_REMOVE)和madvise (MADV_DONTNEED)调用的通知。调用madvise的时候,userfaultfd收到事件UFFD_EVENT_REMOVE,uffd_msg.remove包含区域的起始地址和结束地址。
5)UFFD_FEATURE_MISSING_HUGETLBFS表示跟踪标准巨型页的页错误异常。
6)UFFD_FEATURE_MISSING_SHMEM表示跟踪tmpfs文件页和共享内存的页错误异常。
7)UFFD_FEATURE_EVENT_UNMAP表示启用munmap调用的通知。调用munmap的时候,userfaultfd收到事件UFFD_EVENT_UNMAP,uffd_msg.remove包含被删除的虚拟内存区域的起始地址和结束地址。
例如启用默认特性:
struct uffdio_api uffdio_api;
uffdio_api.api = UFFD_API;
uffdio_api.features = 0;
ioctl(uffd, UFFDIO_API, &uffdio_api);
(3)创建内存映射,从用户虚拟地址空间分配一个虚拟地址范围。
例如创建一个私有的匿名映射,其代码如下:
char *addr;
addr = mmap(NULL, len, PROT_READ | PROT_WRITE,
MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
(4)使用控制命令UFFDIO_REGISTER注册虚拟地址范围,跟踪指定虚拟地址范围的页错误异常。
参数的数据类型如下:
struct uffdio_register {
struct uffdio_range range;
__u64 mode;
__u64 ioctls;
};
成员range指定起始地址和长度。
成员mode指定跟踪哪些类型的页错误异常,可以是这些标志的组合:UFFDIO_REGISTER_MODE_MISSING表示跟踪缺页,UFFDIO_REGISTER_MODE_WP表示跟踪写只读页,当前只支持UFFDIO_REGISTER_MODE_MISSING。
成员ioctls返回在指定虚拟地址范围内可以使用哪些控制命令。
例如跟踪某个虚拟地址范围的缺页异常:
struct uffdio_register uffdio_register;
uffdio_register.range.start = (unsigned long) addr;
uffdio_register.range.len = len;
uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
ioctl(uffd, UFFDIO_REGISTER, &uffdio_register);
(5)使用select或poll监听文件可读。进程访问注册的虚拟地址范围,如果生成页错误异常,内核将会把页错误异常传递给进程,userfaultfd文件变成可读。
需要使用两个线程:一个线程访问虚拟地址以触发页错误异常,另一个线程调用poll以监听文件可读。
其代码如下:
struct pollfd pollfd;
int nready;
pollfd.fd = uffd;
pollfd.events = POLLIN;
nready = poll(&pollfd, 1, -1);
(6)使用read读取事件。
事件信息的数据类型是结构体uffd_msg,事件的类型如下:
1)UFFD_EVENT_PAGEFAULT:页错误异常。
2)UFFD_EVENT_FORK:fork调用。
3)UFFD_EVENT_REMAP:mremap调用。
4)UFFD_EVENT_REMOVE:madvise(MADV_REMOVE)和madvise(MADV_DONTNEED)调用。
5)UFFD_EVENT_UNMAP:munmap调用。
其代码如下:
ssize_t nread;
struct uffd_msg msg;
nread = read(uffd, &msg, sizeof(msg));
if (msg.event != UFFD_EVENT_PAGEFAULT) {
fprintf(stderr, "Unexpected event on userfaultfd
");
exit(EXIT_FAILURE);
}
/* 显示页错误事件的信息 */
printf(" UFFD_EVENT_PAGEFAULT event: ");
printf("flags = %llx; ", msg.arg.pagefault.flags);
printf("address = %llx
", msg.arg.pagefault.address);
(7)使用控制命令UFFDIO_COPY把数据复制到触发页错误异常的虚拟页,或者使用控制命令UFFDIO_ZERO把虚拟页映射到零页。
控制命令UFFDIO_COPY的参数的数据类型如下:
struct uffdio_copy {
__u64 dst;
__u64 src;
__u64 len;
#define UFFDIO_COPY_MODE_DONTWAKE ((__u64)1<<0)
__u64 mode;
__s64 copy;
};
成员dst是目的地址,成员src是源地址,成员len是长度。
成员mode是模式,UFFDIO_COPY_MODE_DONTWAKE表示不要唤醒等待事件被读取的线程,稍后进程使用命令UFFDIO_WAKE唤醒等待的线程。
成员copy返回复制的字节数。
其代码如下:
int page_size;
struct uffdio_copy uffdio_copy;
page_size = sysconf(_SC_PAGE_SIZE); /* 获取页长度 */
uffdio_copy.src = (unsigned long) src;/* 复制的源地址 */
/* 复制的目的地址是触发页错误异常的虚拟地址所属的虚拟页的起始地址 */
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;
ioctl(uffd, UFFDIO_COPY, &uffdio_copy);
父进程跟踪子进程的页错误异常的方法如下:
(1)使用控制命令UFFDIO_API请求启用特性UFFD_FEATURE_EVENT_FORK。
(2)使用控制命令UFFDIO_REGISTER注册虚拟地址范围。
(3)使用fork创建子进程。
(4)使用read读取事件UFFD_EVENT_FORK,uffd_msg.fork.ufd是userfaultfd文件描述符,用来跟踪子进程的虚拟内存区域的页错误异常。
1.2 技术原理
应用程序使用userfaultfd跟踪处理页错误异常的主要步骤如下:
(1)使用系统调用userfaultfd创建文件描述符。
(2)使用控制命令UFFDIO_API请求验证版本号和启用某些特性。
(3)从用户虚拟地址空间分配一个虚拟地址范围。
(4)使用控制命令UFFDIO_REGISTER注册虚拟地址范围。
(5)使用select或poll监听文件可读。
(6)访问注册的虚拟地址范围,生成页错误异常,页错误异常处理程序唤醒使用select或poll监听的进程。
(7)使用read读取事件。
(8)使用控制命令UFFDIO_COPY把数据复制到触发页错误异常的虚拟页。
1.数据结构
userfaultfd的主要数据结构是userfaultfd上下文,每次调用系统调用userfaultfd就会创建一个userfaultfd上下文,其数据类型如下:
fs/userfaultfd.c
struct userfaultfd_ctx {
wait_queue_head_t fault_pending_wqh;
wait_queue_head_t fault_wqh;
wait_queue_head_t fd_wqh;
wait_queue_head_t event_wqh;
struct seqcount refile_seq;
atomic_t refcount;
unsigned int flags;
unsigned int features;
enum userfaultfd_state state;
bool released;
struct mm_struct *mm;
};
(1)成员fault_pending_wqh是未读取页错误等待队列,线程触发页错误异常以后,等待userfaultfd读取页错误事件。
(2)成员fault_wqh是已读取页错误等待队列,userfaultfd已读取页错误事件,还没有唤醒触发页错误异常的线程。
(3)成员fd_wqh是文件描述符等待队列,userfaultfd等待事件发生。
(4)成员event_wqh是事件等待队列,等待userfaultfd读取事件。
(5)成员refile_seq是顺序锁,用来保护未读取页错误等待队列和已读取页错误等待队列。
(6)成员refcount是引用计数。
(7)成员flags保存进程调用系统调用userfaultfd指定的标志。
(8)成员features保存进程请求启用的特性。
(9)成员state是状态,UFFD_STATE_WAIT_API表示等待进程请求验证版本号和启用某些特性,UFFD_STATE_RUNNING表示运行状态。
(10)成员released表示userfaultfd文件描述符是否被关闭。
(11)成员mm指向进程的内存描述符。
2.创建文件描述符
系统调用userfaultfd负责创建文件描述符,执行流程如下:
(1)分配文件描述符。
(2)创建一个userfaultfd上下文:成员flags保存调用者传入的标志;成员state是状态,初始值是UFFD_STATE_WAIT_API;成员mm指向调用进程的内存描述符。
(3)创建内部文件的一个打开实例file。
成员f_inode指向内部文件的索引节点(全局变量anon_inode_inode指向内部文件的索引节点)。
成员f_op指向userfaultfd文件操作集合,进程使用poll查询状态时调用其中的poll方法,使用read读文件时调用其中的read方法,使用ioctl执行命令时调用其中的unlocked_ioctl方法。
成员private_data指向userfaultfd上下文。
(4)把文件描述符和file实例的映射添加到进程的打开文件表中。
userfaultfd数据结构的关系如下所示:
3.注册虚拟地址范围
进程执行控制命令UFFDIO_REGISTER的时候,ioctl将会调用userfaultfd文件操作集合的unlocked_ioctl方法,即函数userfaultfd_ioctl,函数userfaultfd_ioctl调用命令UFFDIO_REGISTER的处理函数userfaultfd_register。
函数userfaultfd_register针对注册的虚拟地址范围包含的每个虚拟内存区域vma,处理如下:
(1)如果跟踪缺页,vma->vm_flags设置标志位VM_UFFD_MISSING。
(2)把虚拟内存区域关联到userfaultfd上下文,即vma->vm_userfaultfd_ctx.ctx指向userfaultfd_ctx实例。
(3)对于第一个和最后一个虚拟内存区域,如果只跟踪其中的一部分,那么把虚拟内存区域分裂成两个。
4.监听事件
进程调用poll监听事件的时候,poll将会调用userfaultfd文件操作集合的poll方法,即函数userfaultfd_poll,执行过程如下:
(1)如果userfaultfd上下文的未读取页错误等待队列或事件等待队列不是空的,那么返回POLLIN,表示文件可读。
(2)否则,把进程挂在userfaultfd上下文的文件描述符等待队列上,睡眠等待。
5.页错误异常处理程序
如下所示,在页错误异常处理程序中,如果匿名映射的虚拟页没有映射到物理页,并且虚拟内存区域设置了标志VM_UFFD_MISSING,那么调用函数handle_userfault,把页错误信息传递给userfaultfd:
6.读取事件
页错误异常处理程序把等待事件发生的线程唤醒以后,线程调用read读取事件,read调用userfaultfd文件操作集合的read方法,即函数userfaultfd_read。
函数userfaultfd_read反复调用函数userfaultfd_ctx_read读取消息,直到把进程提供的缓冲区填满或者读完所有消息为止。
函数userfaultfd_ctx_read的执行流程如下:
(1)先查看未读取页错误等待队列,如果未读取页错误等待队列不是空的,那么从尾部取一个节点,从节点读取消息,把节点移到已读取页错误等待队列,避免下一次从未读取页错误等待队列取同一个节点。
(2)如果未读取页错误等待队列是空的,就查看事件等待队列。如果事件等待队列不是空的,那么从尾部取一个节点,读取消息,并且唤醒等待事件被读取的线程。
7.复制数据
进程读取事件以后,执行控制命令UFFDIO_COPY,把数据复制到触发页错误异常的虚拟页。ioctl将会调用函数userfaultfd_ioctl,然后函数userfaultfd_ioctl调用命令UFFDIO_COPY的处理函数userfaultfd_copy。
函数userfaultfd_copy的执行流程如下:
(1)调用函数mcopy_atomic,分配物理页,把数据复制到物理页,在进程的页表中把虚拟页映射到物理页。
(2)如果调用者允许唤醒,那么调用wake_userfault,唤醒userfaultfd上下文的未读取页错误等待队列和已读取页错误等待队列中访问复制的目的虚拟页时触发异常的所有线程;否则,进程使用命令UFFDIO_WAKE唤醒等待的线程。
8.父进程跟踪子进程的页错误异常
如下所示,调用fork创建子进程的时候,函数dup_mmap针对userfaultfd的处理如下:
(1)遍历当前进程的虚拟内存区域,调用函数dup_userfaultfd:如果虚拟内存区域被userfaultfd跟踪,并且userfaultfd启用特性UFFD_FEATURE_EVENT_FORK,那么为子进程的虚拟内存区域复制userfaultfd上下文。
(2)调用函数dup_userfaultfd_complete:针对当前进程的每个userfaultfd上下文,挂在事件等待队列上等待userfaultfd读取事件UFFD_EVENT_FORK。
如下所示,进程调用read以读取事件,函数userfaultfd_ctx_read从userfaultfd上下文的事件等待队列的尾部取一个事件,如果事件是UFFD_EVENT_FORK,处理如下:
(1)调用函数resolve_userfault_fork:分配文件描述符,关联到跟踪子进程的页错误异常的userfaultfd上下文,把文件描述符填写到消息uffd_msg的成员arg.fork.ufd中。
(2)调用函数userfaultfd_event_complete,唤醒等待事件被读取的线程。
进程读取消息以后,从消息的成员arg.fork.ufd得到文件描述符,使用文件描述符跟踪处理子进程的页错误异常。