USERFAULTFD - Linux手册页

Linux程序员手册 第2部分
更新日期: 2020-06-09

BUGS

如果启用了UFFD_FEATURE_EVENT_FORK,并且fork(2)系列的系统调用被信号中断或失败,则可能会创建过时的userfaultfd描述符。在这种情况下,伪造的UFFD_EVENT_FORK将被传递到userfaultfd监视器。

示例

下面的程序演示了userfaultfd机制的用法。该程序为使用mmap(2)创建的需求页面零区域中的页面创建两个线程,其中一个充当该进程的页面错误处理程序。

该程序采用一个命令行参数,该参数是将在映射中创建的页面数,该页面的页面错误将通过userfaultfd处理。创建userfaultfd对象之后,程序将创建指定大小的匿名私有映射,并使用UFFDIO_REGISTER ioctl(2)操作注册该映射的地址范围。然后,程序将创建第二个线程,该线程将执行处理页面错误的任务。

然后,主线程遍历映射的页面,从连续的页面中获取字节。由于尚未访问页面,因此每个页面中字节的首次访问将触发userfaultfd文件描述符上的页面错误事件。

每个页面错误事件都由第二个线程处理,第二个线程位于循环处理来自userfaultfd文件描述符的输入。在每个循环迭代中,第二个线程首先调用poll(2)来检查文件描述符的状态,然后从文件描述符中读取一个事件。所有此类事件均应为UFFD_EVENT_PAGEFAULT事件,该线程通过使用UFFDIO_COPY ioctl(2)操作将数据页复制到故障区域中来进行处理。

以下是我们在运行程序时看到的示例:

$ ./userfaultfd_demo 3
Address returned by mmap() = 0x7fd30106c000

fault_handler_thread():
    poll() returns: nready = 1; POLLIN = 1; POLLERR = 0
    UFFD_EVENT_PAGEFAULT event: flags = 0; address = 7fd30106c00f
        (uffdio_copy.copy returned 4096)
Read address 0x7fd30106c00f in main(): A
Read address 0x7fd30106c40f in main(): A
Read address 0x7fd30106c80f in main(): A
Read address 0x7fd30106cc0f in main(): A

fault_handler_thread():
    poll() returns: nready = 1; POLLIN = 1; POLLERR = 0
    UFFD_EVENT_PAGEFAULT event: flags = 0; address = 7fd30106d00f
        (uffdio_copy.copy returned 4096)
Read address 0x7fd30106d00f in main(): B
Read address 0x7fd30106d40f in main(): B
Read address 0x7fd30106d80f in main(): B
Read address 0x7fd30106dc0f in main(): B

fault_handler_thread():
    poll() returns: nready = 1; POLLIN = 1; POLLERR = 0
    UFFD_EVENT_PAGEFAULT event: flags = 0; address = 7fd30106e00f
        (uffdio_copy.copy returned 4096)
Read address 0x7fd30106e00f in main(): C
Read address 0x7fd30106e40f in main(): C
Read address 0x7fd30106e80f in main(): C
Read address 0x7fd30106ec0f in main(): C

Program source

/* userfaultfd_demo.c

   Licensed under the GNU General Public License version 2 or later.
*/
#define _GNU_SOURCE
#include <sys/types.h>
#include <stdio.h>
#include <linux/userfaultfd.h>
#include <pthread.h>
#include <errno.h>
#include <unistd.h>
#include <stdlib.h>
#include <fcntl.h>
#include <signal.h>
#include <poll.h>
#include <string.h>
#include <sys/mman.h>
#include <sys/syscall.h>
#include <sys/ioctl.h>
#include <poll.h>

#define errExit(msg)    do { perror(msg); exit(EXIT_FAILURE); \
                        } while (0)

static int page_size;

static void *
fault_handler_thread(void *arg)
{
    static struct uffd_msg msg;   /* Data read from userfaultfd */
    static int fault_cnt = 0;     /* Number of faults so far handled */
    long uffd;                    /* userfaultfd file descriptor */
    static char *page = NULL;
    struct uffdio_copy uffdio_copy;
    ssize_t nread;

    uffd = (long) arg;

    /* Create a page that will be copied into the faulting region */

    if (page == NULL) {
        page = mmap(NULL, page_size, PROT_READ | PROT_WRITE,
                    MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
        if (page == MAP_FAILED)
            errExit("mmap");
    }

    /* Loop, handling incoming events on the userfaultfd
       file descriptor */

    for (;;) {

        /* See what poll() tells us about the userfaultfd */

        struct pollfd pollfd;
        int nready;
        pollfd.fd = uffd;
        pollfd.events = POLLIN;
        nready = poll(&pollfd, 1, -1);
        if (nready == -1)
            errExit("poll");

        printf("\nfault_handler_thread():\n");
        printf("    poll() returns: nready = %d; "
                "POLLIN = %d; POLLERR = %d\n", nready,
                (pollfd.revents & POLLIN) != 0,
                (pollfd.revents & POLLERR) != 0);

        /* Read an event from the userfaultfd */

        nread = read(uffd, &msg, sizeof(msg));
        if (nread == 0) {
            printf("EOF on userfaultfd!\n");
            exit(EXIT_FAILURE);
        }

        if (nread == -1)
            errExit("read");

        /* We expect only one kind of event; verify that assumption */

        if (msg.event != UFFD_EVENT_PAGEFAULT) {
            fprintf(stderr, "Unexpected event on userfaultfd\n");
            exit(EXIT_FAILURE);
        }

        /* Display info about the page-fault event */

        printf("    UFFD_EVENT_PAGEFAULT event: ");
        printf("flags = %llx; ", msg.arg.pagefault.flags);
        printf("address = %llx\n", msg.arg.pagefault.address);

        /* Copy the page pointed to by aqpageaq into the faulting
           region. Vary the contents that are copied in, so that it
           is more obvious that each fault is handled separately. */

        memset(page, aqAaq + fault_cnt % 20, page_size);
        fault_cnt++;

        uffdio_copy.src = (unsigned long) page;

        /* We need to handle page faults in units of pages(!).
           So, round faulting address down to page boundary */

        uffdio_copy.dst = (unsigned long) msg.arg.pagefault.address &
                                           ti(page_size - 1);
        uffdio_copy.len = page_size;
        uffdio_copy.mode = 0;
        uffdio_copy.copy = 0;
        if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
            errExit("ioctl-UFFDIO_COPY");

        printf("        (uffdio_copy.copy returned %lld)\n",
                uffdio_copy.copy);
    }
}

int
main(int argc, char *argv[])
{
    long uffd;          /* userfaultfd file descriptor */
    char *addr;         /* Start of region handled by userfaultfd */
    unsigned long len;  /* Length of region handled by userfaultfd */
    pthread_t thr;      /* ID of thread that handles page faults */
    struct uffdio_api uffdio_api;
    struct uffdio_register uffdio_register;
    int s;

    if (argc != 2) {
        fprintf(stderr, "Usage: %s num-pages\n", argv[0]);
        exit(EXIT_FAILURE);
    }

    page_size = sysconf(_SC_PAGE_SIZE);
    len = strtoul(argv[1], NULL, 0) * page_size;

    /* Create and enable userfaultfd object */

    uffd = syscall(__NR_userfaultfd, O_CLOEXEC | O_NONBLOCK);
    if (uffd == -1)
        errExit("userfaultfd");

    uffdio_api.api = UFFD_API;
    uffdio_api.features = 0;
    if (ioctl(uffd, UFFDIO_API, &uffdio_api) == -1)
        errExit("ioctl-UFFDIO_API");

    /* Create a private anonymous mapping. The memory will be
       demand-zero paged--that is, not yet allocated. When we
       actually touch the memory, it will be allocated via
       the userfaultfd. */

    addr = mmap(NULL, len, PROT_READ | PROT_WRITE,
                MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
    if (addr == MAP_FAILED)
        errExit("mmap");

    printf("Address returned by mmap() = %p\n", addr);

    /* Register the memory range of the mapping we just created for
       handling by the userfaultfd object. In mode, we request to track
       missing pages (i.e., pages that have not yet been faulted in). */

    uffdio_register.range.start = (unsigned long) addr;
    uffdio_register.range.len = len;
    uffdio_register.mode = UFFDIO_REGISTER_MODE_MISSING;
    if (ioctl(uffd, UFFDIO_REGISTER, &uffdio_register) == -1)
        errExit("ioctl-UFFDIO_REGISTER");

    /* Create a thread that will process the userfaultfd events */

    s = pthread_create(&thr, NULL, fault_handler_thread, (void *) uffd);
    if (s != 0) {
        errno = s;
        errExit("pthread_create");
    }

    /* Main thread now touches memory in the mapping, touching
       locations 1024 bytes apart. This will trigger userfaultfd
       events for all pages in the region. */

    int l;
    l = 0xf;    /* Ensure that faulting address is not on a page
                   boundary, in order to test that we correctly
                   handle that case in fault_handling_thread() */
    while (l < len) {
        char c = addr[l];
        printf("Read address %p in main(): ", addr + l);
        printf("%c\n", c);
        l += 1024;
        usleep(100000);         /* Slow things down a little */
    }

    exit(EXIT_SUCCESS);
}

名称

userfaultfd-创建文件描述符以处理用户空间中的页面错误

说明

userfaultfd()创建一个新的userfaultfd对象,该对象可用于将页面错误处理委派给用户空间应用程序,并返回引用该新对象的文件描述符。使用ioctl(2)配置新的userfaultfd对象。

一旦配置了userfaultfd对象,应用程序便可以使用read(2)来接收userfaultfd通知。从userfaultfd进行的读取可能是阻塞的或非阻塞的,具体取决于用于创建userfaultfd或后续调用fcntl(2)的标志的值。

可以对标志中的以下值进行按位或运算,以更改userfaultfd()的行为:

O_CLOEXEC
启用新的userfaultfd文件描述符的close-on-exec标志。请参见open(2)中O_CLOEXEC标志的描述。
O_NONBLOCK
为userfaultfd对象启用非阻塞操作。请参见open(2)中O_NONBLOCK标志的描述。

关闭引用userfaultfd对象的最后一个文件描述符时,将取消注册该对象注册的所有内存范围,并清除未读事件。

Usage

userfaultfd机制设计为允许多线程程序中的线程对进程中的其他线程执行用户空间分页。当针对userfaultfd对象注册的区域之一发生页面错误时,故障线程将进入睡眠状态,并生成可通过userfaultfd文件描述符读取的事件。故障处理线程从该文件描述符读取事件,并使用ioctl_userfaultfd(2)中描述的操作为事件提供服务。在处理页面错误事件时,错误处理线程可以触发睡眠线程的唤醒。

故障线程和故障处理线程可能在不同进程的上下文中运行。在这种情况下,这些线程可能属于不同的程序,并且执行故障线程的程序不一定会与处理页面错误的程序配合。在这种非合作模式下,监视userfaultfd并处理页面错误的过程需要知道该错误过程的虚拟内存布局中的变化,以避免内存损坏。

从Linux 4.11开始,userfaultfd还可以将故障过程的虚拟内存布局更改通知故障处理线程。另外,如果故障进程调用fork(2),则与父级关联的userfaultfd对象可能会复制到子进程中,并且将通过与userfault对象关联的文件描述符(通过下面描述的UFFD_EVENT_FORK)通知userfaultfd监视器。为子进程创建的,允许userfaultfd监视器为子进程执行用户空间分页。与必须同步并且需要显式或隐式唤醒的页面错误不同,所有其他事件都是异步传递的,并且一旦userfaultfd管理器执行read(2),则非合作过程将恢复执行。 userfaultfd管理器应谨慎地将对UFFDIO_COPY的调用与事件处理同步。

对于单线程非合作的userfaultfd管理器实现,事件传递的当前异步模型是最佳的。

Userfaultfd operation

使用userfaultfd()创建userfaultfd对象之后,应用程序必须使用UFFDIO_API ioctl(2)操作启用它。该操作允许内核和用户空间之间的握手来确定API版本和支持的功能。必须在以下所述的任何其他ioctl(2)操作之前执行此操作(否则,这些操作将因EINVAL错误而失败)。

成功执行UFFDIO_API操作之后,应用程序将使用UFFDIO_REGISTER ioctl(2)操作注册内存地址范围。成功完成UFFDIO_REGISTER操作后,内核将把在请求的内存范围内发生并满足注册时定义的模式的页面错误转发给用户空间应用程序。然后,应用程序可以使用UFFDIO_COPY或UFFDIO_ZEROPAGE ioctl(2)操作来解决页面错误。

从Linux 4.14开始,如果应用程序使用UFFDIO_API ioctl(2)设置UFFD_FEATURE_SIGBUS功能位,则不会将页面错误通知转发到用户空间。而是将SIGBUS信号传递到故障过程。利用此功能,userfaultfd可用于鲁棒性目的,以简单地捕获对​​已分配地址的未分配页面的区域的任何访问,而不必侦听userfaultfd事件。不需要userfaultfd监视器来处理此类内存访问。例如,此功能对于希望防止内核通过内存映射访问漏洞时自动阻止分配内存页并在稀疏文件中填充漏洞的应用程序很有用。

如果与UFFD_FEATURE_FORK结合使用,则UFFD_FEATURE_SIGBUS功能是通过fork(2)隐式继承的。

可以在ioctl_userfaultfd(2)中找到各种ioctl(2)操作的详细信息。

从Linux 4.11开始,在UFFDIO_API操作期间可能会启用除页面错误以外的事件。

在Linux 4.11之前,userfaultfd仅可与匿名私有内存映射一起使用。从Linux 4.11开始,userfaultfd也可以与hugetlbfs和共享内存映射一起使用。

Reading from the userfaultfd structure

从userfaultfd文件描述符进行的每个read(2)返回一个或多个uffd_msg结构,每个结构都描述一个页面错误事件或非合作使用userfaultfd所需的事件:

struct uffd_msg {
    __u8  event;            /* Type of event */
    ...
    union {
        struct {
            __u64 flags;    /* Flags describing fault */
            __u64 address;  /* Faulting address */
        } pagefault;

        struct {            /* Since Linux 4.11 */
            __u32 ufd;      /* Userfault file descriptor
                               of the child process */
        } fork;

        struct {            /* Since Linux 4.11 */
            __u64 from;     /* Old address of remapped area */
            __u64 to;       /* New address of remapped area */
            __u64 len;      /* Oroirnal mapping length */
        } remap;

        struct {            /* Since Linux 4.11 */
            __u64 start;    /* Start address of removed area */
            __u64 end;      /* End address of removed area */
        } remove;
        ...
    } arg;

    /* Padding fields omitted */
} __packed;

如果有多个事件可用,并且提供的缓冲区足够大,则read(2)返回的事件将与提供的缓冲区中容纳的事件数量一样多。如果提供给read(2)的缓冲区小于uffd_msg结构的大小,则read(2)失败,错误为EINVAL。

uffd_msg结构中设置的字段如下:

event
事件的类型。取决于事件类型,arg联合的不同字段表示事件处理所需的详细信息。仅当在使用UFFDIO_API ioctl(2)进行API握手时启用了适当的功能时,才会生成非页面错误事件。
The following values can appear in the event

field:

UFFD_EVENT_PAGEFAULT(since Linux 4.3)
页面错误事件。页面错误详细信息可在页面错误字段中找到。
UFFD_EVENT_FORK(since Linux 4.11)
当故障进程调用fork(2)(或没有CLONE_VM标志的clone(2))时生成。事件详细信息在fork字段中可用。
UFFD_EVENT_REMAP(since Linux 4.11)
当故障进程调用mremap(2)时生成。事件详细信息在重映射字段中可用。
UFFD_EVENT_REMOVE(since Linux 4.11)
当故障进程使用MADV_DONTNEED或MADV_REMOVE建议调用madvise(2)时生成。事件详细信息位于"删除"字段中。
UFFD_EVENT_UNMAP(since Linux 4.11)
当故障进程未使用munmap(2)显式映射内存范围或在mmap(2)或mremap(2)隐式映射内存范围时生成。事件详细信息位于"删除"字段中。
pagefault.address
触发页面错误的地址。
pagefault.flags
A bit mask of flags that describe the event. For UFFD_EVENT_PAGEFAULT,

the following flag may appear:

UFFD_PAGEFAULT_FLAG_WRITE
如果地址在用UFFDIO_REGISTER_MODE_MISSING标志注册的范围内(请参阅ioctl_userfaultfd(2))并且设置了此标志,则这是写错误;否则,这是读取错误。
fork.ufd
与为fork(2)创建的子级创建的userfault对象关联的文件描述符。
remap.from
使用mremap(2)重新映射的内存范围的原始地址。
remap.to
使用mremap(2)重新映射的内存范围的新地址。
remap.len
使用mremap(2)重新映射的内存范围的原始长度。
remove.start
使用madvise(2)释放或未映射的内存范围的起始地址
remove.end
使用madvise(2)释放或未映射的内存范围的结束地址

对userfaultfd文件描述符的read(2)可能由于以下错误而失败:

EINVAL
尚未使用UFFDIO_API ioctl(2)操作启用userfaultfd对象

如果在关联的打开文件描述中启用了O_NONBLOCK标志,则可以使用poll(2),select(2)和epoll(7)监视userfaultfd文件描述符。当事件可用时,文件描述符指示为可读。如果未启用O_NONBLOCK标志,则poll(2)(始终)指示文件具有POLLERR条件,而select(2)指示文件描述符为可读和可写。

版本

userfaultfd()系统调用首先出现在Linux 4.3中。

在Linux 4.11中添加了对hugetlbfs和共享内存区域以及非页面错误事件的支持。

语法

#include <sys/types.h>
#include <linux/userfaultfd.h>

int userfaultfd(int flags);

注意:此系统调用没有glibc包装器。请参阅注释。

另外参见

fcntl(2),ioctl(2),ioctl_userfaultfd(2),madvise(2),mmap(2)

Linux内核源代码树中的Documentation / admin-guide / mm / userfaultfd.rst

出版信息

这个页面是Linux手册页项目5.08版的一部分。有关项目的说明、有关报告错误的信息以及此页面的最新版本,请访问https://www.kernel.org/doc/man-pages/

错误说明

EINVAL
在标志中指定了不支持的值。
EMFILE
已达到打开文件描述符数量的每个进程限制
ENFILE
已达到系统范围内打开文件总数的限制。
ENOMEM
内核内存不足。
EPERM(since Linux 5.2)
调用方没有特权(初始用户名称空间中没有CAP_SYS_PTRACE功能),并且/ proc / sys / vm / unprivileged_userfaultfd的值为0。

返回值

成功后,userfaultfd()返回一个新文件描述符,该文件描述符引用了userfaultfd对象。如果出错,则返回-1,并正确设置errno。

备注

Glibc没有为该系统调用提供包装器;使用syscall(2)调用它。

基于使用SIGSEGV信号和mmap(2),可以将userfaultfd机制用作传统用户空间寻呼技术的替代方法。它还可以用于实现检查点/还原机制的延迟还原,以及复制后迁移,以在将虚拟机和Linux容器从一台主机转移到另一台主机时允许(几乎)不间断地执行。

遵循规范

userfaultfd()是特定于Linux的,不应在旨在可移植的程序中使用。

日期:2019-08-20 17:59:36 来源:oir作者:oir