[翻译] Page faults in user space: MADV_USERFAULT, remap_anon_range(), and userfaultfd()

Page faults in user space: MADV_USERFAULT, remap_anon_range(), and userfaultfd()

译文作者:zhangzl2013
译文链接:http://www.cnblogs.com/zhangzl2013/p/page_faults_in_user_space.html
原文作者:Jonathan Corbet
原文链接:Page faults in user space: MADV_USERFAULT, remap_anon_range(), and userfaultfd()
本文有可能会被转载,从而导致评论留言的碎片化。想参与评论和探讨的同学,请找到原文或译文的原始地址,与原文或译文作者互动讨论。

内核开发者们经常想把内核中的功能移到用户空间来实现,从而得到更好的性能。网络方面的一些功能就是这样的。要把内存管理的一些功能移到用户空间的想法可不太常见,但是并非没有,比如Andrea Arcangeli的user-space page fault handing补丁。

页面错误的处理一般需要从二级存储中获得数据,并将它放到出错进程的地址空间中的某个地方。为啥想在用户空间做这事儿呢?一个主要的应用场景是KVM虚拟机的动态迁移。迁移需要移动虚拟机的内存,这需要花费很长时间,而虚拟机的用户又希望在迁移时越快越好。最好是根本就意识不到虚拟机正在迁移就好了。为了达到这个目标,就要只移动虚拟机能在新宿主机上运行所需要的最少的内存。一旦虚拟机开始在新宿主机上运行了,它必然会访问一些还没有移动过来的内存。如果(用户空间的)虚拟机管理器能获取到页面错误,那它就可以对页面的需求程度进行排序。也就是在最小延迟的基础上完成页面的跨主机调度。

此外还有其他的应用场景——比如跨网络的分布式共享内存。

这个补丁集添加了两个get_user_pages()的变体,用于使用户空间获取内核页面:

long get_user_pages_locked(struct task_struct *tsk, struct mm_struct *mm,
                       unsigned long start, unsigned long nr_pages,
                       int write, int force, struct page **pages,
                   int *locked);
    long get_user_pages_unlocked(struct task_struct *tsk, struct mm_struct *mm,
                 unsigned long start, unsigned long nr_pages,
                 int write, int force, struct page **pages);

前一个函数在调用时会需要获取mmap_sem信号量。当*locked参数为零时,也可以释放信号量。第二个函数不需要获取mmap_sem信号量。在内核中使用这些函数可以在处理页面错误时释放mmap_sem信号量从而提升性能。这在当前的内核中也很有用,而要把页面错误处理交给用户空间的话,它就是必须的了。在用户空间占有mmap_sem信号量可不是啥好事儿。

然后给mdavise()系统调用MADV_USERFAULT标识。如果在某一块内存上有这个标识,那内核就不会对这段内存进行错误处理。在没有其他错误处理时,出错的进程会收到SIGBUS信号。从而把错误处理的任务交给了出错的进程本身。一个新的系统调用在错误处理时用得到:

int remap_anon_pages(void *dest, void *src, unsigned long len,
                 unsigned long flags);

它把src处起始的len字节大小的页面移动到dest处。做这个操作之前有一些约束,首先dest处的内存必须为映射过——remap_anon_pages()函数不会覆盖已经映射的区域的。src处的内存必须存在并且已经映射,而且页面不能被其他进程共享。这些限制简化了实现,但也增加用户空间错误处理的静态条件。

如果src是一个大页面,并且len是2MB的整数倍,那整个页面都会移动到dest。

有了这个机制,应用程序的SIGBUG信号处理函数会响应内存分配错误,它会给内存中填入适当的内容,并调用ramap_anon_pages()函数进行内存映射。信号处理函数返回时,页面错误会继续检查,此时由于内存已经分配好,所以程序可以继续正常运行。

用过类Unix系统信号处理函数的人会认为这些工作不应该由这些函数来做。信号处理函数不应该用于处理页面错误。为此,Andrea添加了一个系统调用:

int userfaultfd(int flags);

这个函数返回一个文件描述符,用于跟内核通信,处理页面错误。flags参数置为O_NONBLOCK时表示启用非阻塞行为,但一般都不用。

获取文件描述符之后,应用程序写入一个64位整数,用以表明协议版本。如果内核支持的话,也返回同样的整数,如果不支持则返回-1。然后在有页面错误产生时,应用程序可以读取一个64位地址。应用程序解决了页面错误之后,再将表示内存范围的两个指针写回内核。

这里进程需要一个专门用于页面错误处理的线程。一旦有页面错误产生,原来的工作线程暂停,错误处理线程开始工作。如果使用了userfaultfd()系统调用,则不会产生SIGBUS信号。对于出错的工作线程来说,一切跟原来都是一样的,只不过速度会慢一点儿。

前文说过,用户空间页面错误处理有很多应用场景。如果一个应用程序要同时应付多个场景怎么办呢?如果这样的话,应用程序可以用userfaultfd()函数打开多个文件描述符,并把每个文件描述符限定在一定的内存范围之内。可以通过写入两个指针来制定内存范围;最低有效位由起始指针设置。这样的话,只有在某内存范围之内的页面错误才会对应相应的文件描述符。应用程序还必须设置MADV_USERFAULT标识。可以在同一个文件描述符上设置多个范围,但是同一范围的内存只能由一个文件描述符来处理。

关于remap_anon_pages()系统调用有很多讨论。起初Linus怀疑remap_anon_pages()有没有比remap_file_pages()更好,他觉得remap_file_pages()不好,不久就会被清除。然后他又觉得应该提供一个简单的接口,错误处理程序只要调用write()进行处理就行。Andrea回复说这样的接口也能实现;错误处理函数只要将数据写入文件描述符,由内核处理剩下的事情。但他担心这样会失去零拷贝的功能。Linus回复说他不关心零拷贝功能,他觉得不值得去实现它。

我们可以预见对get_user_pages()的这个优化很快就要进入内核了,尽管Linus对它还不完全满意。余下还有很多工作要做,可能要很长时间,而且最终可能也没有remap_anon_pages()。但因为因为应用场景需要,对动态迁移的改进将会长期进行下去。

-- 结束 --

posted @ 2014-10-23 07:24 zhangzl2013 阅读(...) 评论(...) 编辑 收藏