작업 중.

NAME

userfaultfd - 사용자 공간에서 페이지 폴트를 처리하기 위한 파일 디스크립터 만들기

SYNOPSIS

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

int userfaultfd(int flags);

주의: 이 시스템 호출들에 대한 glibc 래퍼가 없다. NOTES 참고.

DESCRIPTION

userfaultfd()는 페이지 폴트 처리를 사용자 공간 응용에게 위임하는 데 쓸 수 있는 userfaultfd 객체를 새로 생성하고 그 새 객체를 가리키는 파일 디스크립터를 반환한다. ioctl(2)을 이용해 새 userfaultfd 객체를 설정한다.

userfaultfd를 설정하고 나면 응용에서 read(2)를 사용해 userfaultfd 알림을 수신할 수 있다. userfaultfd 읽기는 userfaultfd 생성이나 이어진 fcntl(2) 호출에 사용한 flags 값에 따라 블로킹일 수도 있고 논블로킹일 수도 있다.

flags에 다음 값들을 비트 OR 해서 userfaultfd()의 동작 방식을 바꿀 수 있다.

O_CLOEXEC
새 userfaultfd 파일 디스크립터에 exec에서 닫기 플래그를 켠다. open(2)O_CLOEXEC 플래그 설명을 보라.
O_NONBLOCK
userfaultfd 객체에 논블로킹 동작을 켠다. open(2)O_NONBLOCK 플래그 설명을 보라.

userfaultfd 객체를 가리키는 마지막 파일 디스크립터가 닫힐 때 그 객체에 등록되어 있던 모든 메모리 범위들을 해제하고 읽지 않은 이벤트들을 버린다.

사용법

userfaultfd 메커니즘은 다중 스레드 프로그램에서 한 스레드가 프로세스 내 다른 스레드들을 위해 사용자 공간 페이징을 수행할 수 있도록 설계되었다. userfaultfd 객체에 등록된 영역들 중 하나에 페이지 폴트가 일어나면 폴트를 일으킨 스레드가 잠재워지고 userfaultfd 파일 디스크립터를 통해 읽을 수 있는 이벤트가 생성된다. 폴트 처리 스레드가 이 파일 디스크립터에서 이벤트를 읽어들여서 ioctl_userfaultfd(2)에서 기술하는 작업들을 이용해 처리한다. 페이지 폴트 이벤트를 처리할 때 폴트 처리 스레드가 잠든 스레드의 기상을 촉발시킬 수 있다.

...

...

...

userfaultfd 작업

...

...

...

...

...

...

userfaultfd 구조체 읽어들이기

...

struct uffd_msg {
    __u8  event;            /* 이벤트 종류 */
    ...
    union {
        struct {
            __u64 flags;    /* 폴트를 설명하는 플래그들 */
            __u64 address;  /* 플트를 일으킨 주소 */
        } pagefault;

        struct {            /* 리눅스 4.11부터 */
            __u32 ufd;      /* 자식 프로세스의 userfault
                               파일 디스크립터 */
        } fork;

        struct {            /* 리눅스 4.11부터 */
            __u64 from;     /* 리매핑 영역의 이전 주소 */
            __u64 to;       /* 리매핑 영역의 새 주소 */
            __u64 len;      /* 원래 매핑 길이 */
        } remap;

        struct {            /* 리눅스 4.11부터 */
            __u64 start;    /* 제거된 영역의 시작 주소 */
            __u64 end;      /* 제거된 영역의 끝 주소 */
        } remove;
        ...
    } arg;

    /* 패딩 필드 생략 */
} __packed;

...

...

event

...

...

UFFD_EVENT_PAGEFAULT (리눅스 4.3부터)
...
UFFD_EVENT_FORK (리눅스 4.11부터)
...
UFFD_EVENT_REMAP (리눅스 4.11부터)
...
UFFD_EVENT_REMOVE (리눅스 4.11부터)
...
UFFD_EVENT_UNMAP (리눅스 4.11부터)
...
pagefault.address
...
pagefault.flags

...

UFFD_PAGEFAULT_FLAG_WRITE
...
fork.ufd
...
remap.from
...
remap.to
...
remap.len
...
remove.start
...
remove.end
...

...

EINVAL
...

...

RETURN VALUE

성공 시 userfaultfd()는 userfaultfd 객체를 가리키는 새 파일 디스크립터를 반환한다. 오류 시 -1을 반환하며 오류를 나타내도록 errno를 설정한다.

ERRORS

EINVAL
...
EMFILE
...
ENFILE
...
ENOMEM
...
EPERM (리눅스 5.2부터)
...

VERSIONS

리눅스 4.3에서 userfaultfd() 시스템 호출이 처음 등장했다.

...

CONFORMING TO

userfaultfd()는 리눅스 전용이므로 이식성이 있어야 하는 프로그램에서는 사용하지 말아야 한다.

NOTES

...

...

BUGS

...

EXAMPLES

...

...

...

...

...

$ ./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

프로그램 소스

/* userfaultfd_demo.c

   Licensed under the GNU General Public License version 2 or later.
*/
#define _GNU_SOURCE
#include <inttypes.h>
#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;   /* userfaultfd에서 읽는 데이터 */
    static int fault_cnt = 0;     /* 지금까지 처리한 폴트 수 */
    long uffd;                    /* userfaultfd 파일 디스크립터 */
    static char *page = NULL;
    struct uffdio_copy uffdio_copy;
    ssize_t nread;

    uffd = (long) arg;

    /* 폴트 발생 영역으로 복사할 페이지 만들기. */

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

    /* 루프, userfaultfd 파일 디스크립터로 들어온 이벤트
       처리하기. */

    for (;;) {

        /* userfaultfd에 대해 poll()이 뭐라는지 보자. */

        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);

        /* userfaultfd에서 이벤트 읽기. */

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

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

        /* 한 가지 이벤트만 있을 것이다. 가정이 맞는지 확인해 보자. */

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

        /* 페이지 폴트 이벤트에 대한 정보 표시하기. */

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

        /* 'page'가 가리키는 페이지를 폴트 발생 영역으로 복사.
           복사해 넣는 내용물을 다르게 해서 각 폴트를 따로 처리하는
           걸 더 분명하게 함. */

        memset(page, 'A' + fault_cnt % 20, page_size);
        fault_cnt++;

        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;
        if (ioctl(uffd, UFFDIO_COPY, &uffdio_copy) == -1)
            errExit("ioctl-UFFDIO_COPY");

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

int
main(int argc, char *argv[])
{
    long uffd;          /* userfaultfd 파일 디스크립터 */
    char *addr;         /* userfaultfd로 처리하는 영역의 시작 */
    uint64_t len;       /* userfaultfd로 처리하는 영역의 길이 */
    pthread_t thr;      /* 페이지 폴트를 처리하는 스레드의 ID */
    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 = strtoull(argv[1], NULL, 0) * page_size;

    /* userfaultfd 객체를 만들고 활성화하기. */

    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");

    /* userfaultfd 이벤트를 처리할 스레드 만들기. */

    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);
}

SEE ALSO

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

리눅스 커널 소스 트리의 Documentation/admin-guide/mm/userfaultfd.rst


2021-03-22