USERFAULTFD(2) Руководство программиста Linux USERFAULTFD(2)

ИМЯ

userfaultfd - создаёт файловый дескриптор для обработки страничных ошибок в пользовательском пространстве

ОБЗОР

#include <sys/types.h>
#include <linux/userfaultfd.h>
int userfaultfd(int flags);

Замечание: В glibc нет обёрточной функции для данного системного вызова; смотрите ЗАМЕЧАНИЯ.

ОПИСАНИЕ

Вызов userfaultfd() создаёт новый объект userfaultfd, который можно использовать для передачи обработки страничных ошибок приложению пользовательского пространства, и возвращает файловый дескриптор, ссылающийся на новый объект. Новый объект userfaultfd настраивается с помощью ioctl(2).

После настройки объекта userfaultfd приложение может использовать вызов read(2) для получения уведомлений userfaultfd. Чтение из userfaultfd может быть блокирующим и не блокирующим, в зависимости от использованного при создании userfaultfd значения flags или последующих вызовов fcntl(2).

Для изменения поведения userfaultfd() можно использовать следующие значения flags (через OR):

Включить флаг close-on-exec для нового открытого файлового дескриптора userfaultfd. Смотрите описание флага O_CLOEXEC в open(2).
Включить не блокирующую работу с объектом userfaultfd. Смотрите описание флага O_NONBLOCK в open(2).

Когда закрывается последний ссылающийся на объект userfaultfd файловый дескриптор, для всех диапазонов памяти, зарегистрированных в этом объекте, снимается регистрация, а все непрочитанные события очищаются.

Использование

Механизм userfaultfd разработан для того, чтобы позволить нити в многонитевой программе выполнять деление на страницы пользовательского пространства других нитей процесса. При возникновении страничной ошибки в одной из зарегистрированных в объекте userfaultfd областей нить с ошибкой засыпает и генерируется событие, которое можно прочитать через файловый дескриптор userfaultfd. Нить обработки страничных ошибок читает сообщения из этого файлового дескриптора и обслуживает из с помощью операций, описанных в ioctl_userfaultfd(2). В время этого нить обработки страничных ошибок может привести в действие механизм пробуждения спящей нити.

Нити с ошибкой и нити обработки страничных ошибок могут быть запущены в контексте различных процессов. В этом случае данные нити могут принадлежать разным программам, а программам, выполняющим нити с ошибкой, не обязательно взаимодействовать с программой, обрабатывающей страничные ошибки. В таком разобщённом режиме процессу, следящему за userfaultfd и обрабатывающему страничные ошибки, необходимо знать об изменениях в раскладке виртуальной памяти процесса с ошибкой, чтобы не допустить повреждение памяти.

Начиная с Linux 4.11, userfaultfd также уведомляет нити обработки страничных ошибок об изменениях в раскладке виртуальной памяти процесса с ошибкой. Также, если процесс с ошибкой вызывает fork(2), то для объекта userfaultfd, связанного с родителем, в дочернем процессе может быть создать дубликат, и отслеживающий userfaultfd также будет уведомлён (смотрите описание UFFD_EVENT_FORK ниже) о файловом дескрипторе, связанном с объектом userfault, который был создан для дочернего процесса, что позволяет отслеживающему userfaultfd выполнять деление на страницы пользовательское пространство дочернего процесса. В отличие от страничных ошибок, которые происходят синхронно и требуют явного или неявного пробуждения, все остальные события доставляются асинхронно и не взаимодействующий процесс возобновляет выполнение сразу же после того как отслеживающий userfaultfd выполняет read(2). Отслеживающий userfaultfd должен корректно синхронизировать вызовы UFFDIO_COPY при обработке событий.

Имеющаяся асинхронная модель доставки событий оптимальна для реализации однонитевой не взаимодействующей отслеживающей userfaultfd программы.

Работа с userfaultfd

После создания объекта userfaultfd с помощью userfaultfd() приложение должно включить его с помощью операции UFFDIO_API вызова ioctl(2). Данная операция позволяет согласовать между ядром и пользовательским пространством версию программного интерфейса поддерживаемых свойств. Эта операция должна быть выполнено самой первой среди других операций ioctl(2), описываемых ниже (в противном случае эти операции завершаются ошибкой EINVAL).

После успешного выполнения UFFDIO_API приложение должно зарегистрировать диапазоны адресов памяти с помощью операции UFFDIO_REGISTER вызова ioctl(2). После успешного выполнения UFFDIO_REGISTER страничная ошибка, возникающая в запрошенном диапазоне и удовлетворяющая режиму, определённому в момент регистрации, будет переслана ядром приложению в пользовательском пространстве. Для решения страничной ошибки приложение может использовать операцию UFFDIO_COPY или UFFDIO_ZEROPAGE вызова ioctl(2).

Начиная с Linux 4.14, если приложение устанавливает бит свойства UFFD_FEATURE_SIGBUS с помощью UFFDIO_API ioctl(2), то уведомления о страничных ошибках не пересылаются в пользовательское пространство. Вместо этого в ошибшийся процесс посылается сигнал SIGBUS. С данным свойством userfaultfd можно использовать в целях надёжности, просто ловя все попытки доступа к областях внутри зарегистрированного адресного диапазона, в котором нет выделенных страниц, не слушая при этом события userfaultfd. При таком доступе к памяти не потребуется процесс слежения за userfaultfd. Например, данное свойство может оказаться полезным приложениям, которые хотят не давать ядру выполнять автоматическое выделение страниц и заполнение дыр в разреженных файлах при обращении к дыре через отображение в памяти.

Свойство UFFD_FEATURE_SIGBUS неявно наследуется при fork(2), если используется вместе с UFFD_FEATURE_FORK.

Подробности о различных операциях ioctl(2) можно найти в ioctl_userfaultfd(2).

Начиная с Linux 4.11 при операции UFFDIO_API можно включить не только события страничной ошибки.

До Linux 4.11 объект userfaultfd мог быть использован только с анонимными частными отображениями памяти. Начиная с Linux 4.11 объект userfaultfd может также использоваться с отображениями общей памяти и hugetlbfs.

Чтение из структуры userfaultfd

Каждый вызов read(2) из файлового дескриптора userfaultfd возвращает одну или более структур uffd_msg, каждая из которых описывает событие страничной ошибки или событие, требуемое для использования userfaultfd в разобщённом режиме:

struct uffd_msg {

    __u8  event;            /* тип события */

    ...

    union {

        struct {

            __u64 flags;    /* флаги, описывающие ошибку */

            __u64 address;  /* ошибочный адрес */

        } pagefault;

        struct {            /* начиная с Linux 4.11 */

            __u32 ufd;      /* файловый дескриптор userfault

                               дочернего процесса */

        } fork;

        struct {            /* начиная с Linux 4.11 */

            __u64 from;     /* старый адрес переотображаемой области */

            __u64 to;       /* новый адрес переотображаемой области */

            __u64 len;      /* начальный размер отображения */

        } remap;

        struct {            /* начиная с Linux 4.11 */

            __u64 start;    /* начальный адрес удаляемой области */

            __u64 end;      /* конечный адрес удаляемой области */

        } remove;

        ...

    } arg;

    /* поля-заполнители не показаны */
} __packed;

Если доступно несколько событий и переданный буфер достаточного размера, то read(2) возвращает столько событий сколько влезает в буфер. Если буфер, указанный read(2), меньше размера структуры uffd_msg, то read(2) завершается ошибкой EINVAL.

Поля структуры uffd_msg:

Тип события. Тип события влияет на заполняемые поля объединения arg, представляющего детали, требуемые для обработки события. События, не относящиеся к страничным ошибкам, генерируются только когда включено соответствующее свойство при согласовании программного интерфейса с помощью операции UFFDIO_API вызова ioctl(2).
В поле event могут появляться следующие значения:
Событие страничной ошибки. Детали ошибки доступны в поле pagefault.
Генерируется, когда процесс с ошибкой вызывает fork(2) (или clone(2) без флага CLONE_VM). Детали ошибки доступны в поле fork.
Генерируется, когда процесс с ошибкой вызывает mremap(2). Детали ошибки доступны в поле remap.
Генерируется, когда процесс с ошибкой вызывает madvise(2) с советом MADV_DONTNEED или MADV_REMOVE. Детали ошибки доступны в поле remove.
Генерируется, когда процесс с ошибкой отменяет проецирование диапазона памяти явным образом с помощью munmap(2) или неявно при вызове mmap(2) или mremap(2). Детали ошибки доступны в поле remove.
Адрес, из-за которого возникла страничная ошибка.
Битовая маска флагов, описывающих событие. Для UFFD_EVENT_PAGEFAULT может появляться следующий флаг:
Если адрес в диапазоне, который был зарегистрирован с флагом UFFDIO_REGISTER_MODE_MISSING (смотрите ioctl_userfaultfd(2)) и этот флаг установлен, то это ошибка записи; в противном случае это ошибка чтения.
С помощью fork(2) был создан потомок, для которого был создан файловый дескриптор, связанный с объектом userfault.
Первоначальный адрес диапазона памяти, который был переотображён с помощью mremap(2).
Новый адрес диапазона памяти, который был переотображён с помощью mremap(2).
Первоначальный размер диапазона памяти, который был переотображён с помощью mremap(2).
Начальный адрес диапазона памяти, который был освобождён с помощью madvise(2) или было отменено проецирование.
Конечный адрес диапазона памяти, который был освобождён с помощью madvise(2) или было отменено проецирование.

Вызов read(2) с файловым дескриптором userfaultfd может завершиться следующими ошибками:

Объект userfaultfd не был включён с помощью операции UFFDIO_API вызова ioctl(2).

Если в связанном открытом файловом описании указан флаг O_NONBLOCK, то файловый дескриптор userfaultfd можно отслеживать с помощью poll(2), select(2) и epoll(7). При возникновении событий, файловый дескриптор помечается как доступный на чтение. Если флаг O_NONBLOCK не задан, то poll(2) (всегда) показывает, что файл находится в состоянии POLLERR, а select(2) показывает, что файловый дескриптор доступен на чтение и запись.

ВОЗВРАЩАЕМОЕ ЗНАЧЕНИЕ

При успешном выполнении userfaultfd() возвращает новый файловый дескриптор, который ссылается на объект userfaultfd. При ошибке возвращается -1, и errno изменяется соответствующим образом.

ОШИБКИ

В flags указано неподдерживаемое значение.
Было достигнуто ограничение по количеству открытых файловых дескрипторов на процесс.
Достигнуто максимальное количество открытых файлов в системе.
Недостаточное количество памяти ядра.

ВЕРСИИ

Системный вызов userfaultfd() впервые появился в Linux 4.3.

Поддержка hugetlbfs и общих областей памяти, а также событий, не относящихся к страничным ошибкам, была добавлена в Linux 4.11.

СООТВЕТСТВИЕ СТАНДАРТАМ

Вызов userfaultfd() есть только в Linux и поэтому не должен использоваться в программах, предназначенных для переноса на другие платформы.

ЗАМЕЧАНИЯ

В glibc нет обёртки для данного системного вызова; запускайте его с помощью syscall(2).

Механизм userfaultfd может быть использован как альтернатива обычному страничному делению пользовательского пространства на основе использования сигнала SIGSEGV и mmap(2). Также он может быть использован для реализации отложенного (lazy) восстановления checkpoint/restore mechanisms, as well as post-copy migration to allow (почти) не прерываемого выполнения при переносе виртуальных машин и контейнеров Linux с одного узла на другой.

ДЕФЕКТЫ

Если указано UFFD_FEATURE_EVENT_FORK и системный вызов из семейства fork(2) прерывается по сигналу или завершается ошибкой, то может быть создан повисший дескриптор userfaultfd. В этом случае программе слежения за userfaultfd может быть доставлен ложный UFFD_EVENT_FORK.

ПРИМЕР

Программа, представленная далее, показывает использование механизма userfaultfd. Она создаёт две нити, одна служит обработчиком страничных ошибок процесса для страниц в режиме выделения при необходимости, созданных mmap(2).

Программа имеет один параметр командной строки, определяющий количество страниц, которые будут созданы в отображении, чьи страничные ошибки будут обработаны userfaultfd. После создания объекта userfaultfd программа создаёт анонимное частное отображение указанного размера и регистрирует адресный диапазон отображения с помощью операции UFFDIO_REGISTER вызовом ioctl(2). После этого программа создаёт вторую нить, которая будет выполнять задачу по обработке страничных ошибок.

После этого главная нить обходит страницы отображения запрашивая байты следующей страницы. Так как к страницам ещё не обращались, первый доступ к байту в каждой странице будет вызывать событие страничной ошибки в файловом дескрипторе userfaultfd.

Каждое событие страничной ошибки обрабатывается второй нитью, которая выполняет цикл обработки ввода из файлового дескриптора userfaultfd. При каждом проходе цикла вторая нить сначала вызывает poll(2) для проверки состояния файлового дескриптора, затем читает событие из файлового дескриптора. Все события должны быть UFFD_EVENT_PAGEFAULT, для их обработки нить копирует страницу данных в ошибочную область с помощью операции UFFDIO_COPY вызова ioctl(2).

Результат работы программы:

$ ./userfaultfd_demo 3
Адрес, возвращённый mmap() = 0x7fd30106c000
fault_handler_thread():

    poll() вернул: nready = 1; POLLIN = 1; POLLERR = 0

    событие UFFD_EVENT_PAGEFAULT: флаги = 0; адрес = 7fd30106c00f

        (uffdio_copy.copy равно 4096)
Чтение по адресу 0x7fd30106c00f в main(): A
Чтение по адресу 0x7fd30106c40f в main(): A
Чтение по адресу 0x7fd30106c80f в main(): A
Чтение по адресу 0x7fd30106cc0f в main(): A
fault_handler_thread():

    poll() вернул: nready = 1; POLLIN = 1; POLLERR = 0

    событие UFFD_EVENT_PAGEFAULT: флаги = 0; адрес = 7fd30106d00f

        (uffdio_copy.copy равно 4096)
Чтение по адресу 0x7fd30106d00f в main(): B
Чтение по адресу 0x7fd30106d40f в main(): B
Чтение по адресу 0x7fd30106d80f в main(): B
Чтение по адресу 0x7fd30106dc0f в main(): B
fault_handler_thread():

    poll() вернул: nready = 1; POLLIN = 1; POLLERR = 0

    событие UFFD_EVENT_PAGEFAULT: флаги = 0; адрес = 7fd30106e00f

        (uffdio_copy.copy равно 4096)
Чтение по адресу 0x7fd30106e00f в main(): C
Чтение по адресу 0x7fd30106e40f в main(): C
Чтение по адресу 0x7fd30106e80f в main(): C
Чтение по адресу 0x7fd30106ec0f в main(): C

Исходный код программы

/* userfaultfd_demo.c

   распространяется по лицензии GNU General Public License version 2 и новее.
*/
#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;   /* данные, прочитанные из 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 (;;) {

        /* С помощью poll() проверяем 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() вернул: 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 в userfaultfd!\n");

            exit(EXIT_FAILURE);

        }

        if (nread == -1)

            errExit("read");

        /* ожидаем только один тип событий; проверяем, что это так */

        if (msg.event != UFFD_EVENT_PAGEFAULT) {

            fprintf(stderr, "Неожидаемый тип события в userfaultfd\n");

            exit(EXIT_FAILURE);

        }

        /* показываем информацию о событии страничной ошибки */

        printf("    событие UFFD_EVENT_PAGEFAULT: ");

        printf("флаги = %llx; ", msg.arg.pagefault.flags);

        printf("адрес = %llx\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 равно %lld)\n",

                uffdio_copy.copy);

    }
}
int
main(int argc, char *argv[])
{

    long uffd;          /* файловый дескриптор userfaultfd */

    char *addr;         /* Начало области, обрабатываемое userfaultfd */

    unsigned long len;  /* Размер области, обрабатываемой userfaultfd */

    pthread_t thr;      /* ID нити, обрабатывающей страничные ошибки */

    struct uffdio_api uffdio_api;

    struct uffdio_register uffdio_register;

    int s;

    if (argc != 2) {

        fprintf(stderr, "Использование: %s количество-страниц\n", argv[0]);

        exit(EXIT_FAILURE);

    }

    page_size = sysconf(_SC_PAGE_SIZE);

    len = strtoul(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");

    /* Копируем частное анонимное отображение. Память будет

       выделена по требованию, то есть реально не выделяется. Когда мы

       обратимся к памяти, она будет выделена с помощью

       userfaultfd. */

    addr = mmap(NULL, len, PROT_READ | PROT_WRITE,

                MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);

    if (addr == MAP_FAILED)

        errExit("mmap");

    printf("Адрес, возвращённый mmap() = %p\n", addr);

    /* Регистрируем в объекте userfaultfd область памяти отображения

       которое мы только что создали. Запрашиваем режим слежения

       за отсутствующими страницами (т. е., которые пока не

       были заполнены). */

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

    }

    /* Теперь главная нить обращается к памяти в отображении c

       интервалом в 1024 байта. Это создаст события в userfaultfd

       для всех страниц в области. */

    int l;

    l = 0xf;    /* Гарантируем, что ошибочный адрес не на границе

                   страницы, чтобы протестировать что, мы правильно

                   обрабатываем этот случай в fault_handling_thread() */

    while (l < len) {

        char c = addr[l];

        printf("Чтение по адресу %p в main(): ", addr + l);

        printf("%c\n", c);

        l += 1024;

        usleep(100000);         /* замедлим программу */

    }

    exit(EXIT_SUCCESS);
}

СМОТРИТЕ ТАКЖЕ

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

Файл Documentation/admin-guide/mm/userfaultfd.rst из дерева исходного кода ядра Linux

2019-03-06 Linux