Как я могу предварительно установить ошибки и заблокировать страницы памяти, которые отображены с помощью MAP_PRIVATE?

Вопрос или проблема

Я пишу приложение на реальном времени Linux, где мне нужно предотвратить любые страничные ошибки после первоначального запуска моего приложения.

Моей первоначальной мыслью было просто вызвать mlockall(MCL_CURRENT | MCL_FUTURE);. Вызов этой функции не возвращает код ошибки, но если я исследую страницы моего процесса, кажется, что все еще есть много страниц, у которых размер Locked: равен 0 (что, как я предполагаю, означает, что эти страницы все еще могут вызывать страничные ошибки).

$ cat /proc/<pid>/smaps | grep -B 21 -A 2 "Locked:                0 kB" | grep -B 1 "^Size:" | grep -v "Size" | grep -v "^\-\-"
7effd0021000-7effd4000000 ---p 00000000 00:00 0 
7effd4021000-7effd8000000 ---p 00000000 00:00 0 
7effd80c6000-7effdc000000 ---p 00000000 00:00 0 
7effddf02000-7effddfa0000 rw-s 00000000 00:05 368 /dev/some_char_device
7effddfa0000-7effde1a0000 rw-s f0000000 00:05 368 /dev/some_char_device
7effde1c1000-7effde1c2000 ---p 00000000 00:00 0 
7effde1c6000-7effde1ca000 rw-s f7c00000 00:05 368 /dev/some_char_device
7effde1ca000-7effde1cb000 ---p 00000000 00:00 0 
7effe221b000-7effe221c000 ---p 00000000 00:00 0 
7effe2220000-7effe2223000 rw-s 00000000 00:05 90 /dev/another_char_device
7effe22df000-7effe22e0000 ---p 00013000 08:02 2234654 /<path_to>/shared_library1.so
7effe22fd000-7effe22fe000 ---p 0000c000 08:02 2231701 /<path_to>/shared_library2.so
7effe23fc000-7effe23fd000 ---p 0001c000 08:02 2234652 /<path_to>/shared_library3.so
7effe2e15000-7effe2e16000 ---p 00215000 08:02 1957 /usr/lib/x86_64-linux-gnu/libc.so.6
7effe2e40000-7effe2e41000 ---p 00011000 08:02 2234649 /<path_to>/shared_library4.so
7effe2f14000-7effe2f15000 ---p 00046000 08:02 2232115 /<path_to>/shared_library5.so
7effe321a000-7effe321b000 ---p 0021a000 08:02 855 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
7effe3258000-7effe3259000 ---p 0001f000 08:02 2234643 /<path_to>/shared_library6.so
7effe327d000-7effe327e000 ---p 00021000 08:02 2234641 /<path_to>/shared_library7.so
7effe328a000-7effe328b000 ---p 00009000 08:02 2232116 /<path_to>/shared_library8.so
7effe348e000-7effe348f000 ---p 00102000 08:02 91759 /<path_to>/shared_library9.so
7effe34c6000-7effe34c8000 r--p 00000000 08:02 175 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7effe34f2000-7effe34fd000 r--p 0002c000 08:02 175 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffc1d1b0000-7ffc1d1b4000 r--p 00000000 00:00 0 [vvar]
7ffc1d1b4000-7ffc1d1b6000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]

Некоторые попытки и некоторые вопросы…

Попытка: Переместить местоположение mlockall в функцию инициализации

Моя основная функция содержит несколько вызовов dlopen. Ранее вызов mlockall был после вызовов dlopen. Перемещение вызова mlockall перед вызовами dlopen похоже, фиксирует в памяти те библиотеки, которые загружаются после него. Однако он не фиксирует память для библиотек, загруженных до вызова mlockall (эти библиотеки связываются во время компиляции и указываются в исполняемом файле).

Почему MCL_CURRENT не фиксирует уже загруженные библиотеки?

$ cat /proc/<mypid>/smaps | grep -B 21 -A 2 "Locked:                0 kB" | grep -B 1 "^Size:" | grep -v "Size" | grep -v "^\-\-"
7fef0c021000-7fef10000000 ---p 00000000 00:00 0 
7fef10021000-7fef14000000 ---p 00000000 00:00 0 
7fef140c6000-7fef18000000 ---p 00000000 00:00 0 
7fef1875d000-7fef187fb000 rw-s 00000000 00:05 368 /dev/some_char_device
7fef187fb000-7fef189fb000 rw-s f0000000 00:05 368 /dev/some_char_device
7fef18a0a000-7fef18a0b000 ---p 00000000 00:00 0 
7fef1ca2e000-7fef1ca2f000 ---p 00000000 00:00 0 
7fef1ca33000-7fef1ca37000 rw-s f7c00000 00:05 368 /dev/some_char_device
7fef1ca37000-7fef1ca38000 ---p 00000000 00:00 0 
7fef1ca3c000-7fef1ca3f000 rw-s 00000000 00:05 90 /dev/another_char_device
7fef1d615000-7fef1d616000 ---p 00215000 08:02 1957 /usr/lib/x86_64-linux-gnu/libc.so.6
7fef1da1a000-7fef1da1b000 ---p 0021a000 08:02 855 /usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.30
7fef1dcea000-7fef1dceb000 ---p 00102000 08:02 91760 /<path_to>/shared_library9.so
7fef1dd22000-7fef1dd24000 r--p 00000000 08:02 175 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7fef1dd4e000-7fef1dd59000 r--p 0002c000 08:02 175 /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
7ffece1c4000-7ffece1c8000 r--p 00000000 00:00 0 [vvar]
7ffece1c8000-7ffece1ca000 r-xp 00000000 00:00 0 [vdso]
ffffffffff600000-ffffffffff601000 --xp 00000000 00:00 0 [vsyscall]

Попытка: Использовать madvise для предварительного запроса страниц

Я попробовал вызвать эту функцию prefault (вдохновлен здесь), но madvise, похоже, возвращает -1 или код EPERM для страниц с правами ---p. Если я правильно понимаю, эти страницы сопоставлены с MAP_PRIVATE и должны выделяться в физической памяти только при записи в них (в соответствии с шаблоном Copy-On-Write). Однако madvise, похоже, не вызывает выделение памяти и просто возвращает ошибку.

Как я могу предварительно запросить страницы, сопоставленные с MAP_PRIVATE?


void prefault()
{
    const pid_t pid = getpid();

    FILE *fp;
    char path[PATH_MAX];
    char buf[4096];

    (void)snprintf(path, sizeof(path), "/proc/%" PRIdMAX "/maps", (intmax_t)pid);

    fp = fopen(path, "r");

    volatile uint8_t val;

    while (fgets(buf, sizeof(buf), fp)) {
        void *start, *end, *offset;
        int major, minor, n, ret;
        uint64_t inode;
        char prot[5];

        n = sscanf(buf, "%p-%p %4s %p %x:%x %" PRIu64 " %s\n",
            &start, &end, prot, &offset, &major, &minor,
            &inode, path);

        if (n < 7) { continue; /* плохие данные sscanf */ }
        if (start >= end) { continue; /* недопустимый диапазон адресов */ }

        ret = madvise(start, (size_t)((uint8_t *)end - (uint8_t *)start), MADV_POPULATE_WRITE);
    }

    (void)fclose(fp);
}

Ответ или решение

К сожалению, техника работы с страницами памяти в Linux, особенно в контексте memory mapping с флагом MAP_PRIVATE, может быть несколько сложной, особенно когда речь идет о предохранении от "пробоя" страниц (page faults) в реальном времени. Давайте разберем ваши попытки и предложим целый ряд решений, учитывая специфику MAP_PRIVATE и другие аспекты.

1. mlockall и загруженные библиотеки

Команда mlockall(MCL_CURRENT | MCL_FUTURE) блокирует текущие страницы и будущие страницы памяти вашего процесса, но она не блокирует страницы, которые уже загружены в память до вызова этой функции, если страницы не были активно использованы. Это объясняет, почему вы наблюдаете, что загруженные библиотеки (например, динамические библиотеки) не заблокированы.

Чтобы заблокировать страницы, которые были загружены до вызова mlockall, вам нужно сначала подтверждать использование этих страниц. Это можно сделать с помощью простой операции чтения из этих библиотек или вызова функции madvise для их предварительной настройки, как вы и пытались сделать.

2. Принудительная установка страниц (Prefaulting)

Использование функции madvise с параметром MADV_POPULATE_WRITE действительно кажется подходящим методом, однако, как вы заметили, это вызывает ошибку при попытке работы с страницами, помеченными как ---p, поскольку они присутствуют из-за механизма Copy-On-Write (COW).

Попробуйте изменить ваш метод написания функций prefault. Вместо MADV_POPULATE_WRITE, используйте mprotect, чтобы изменить права доступа к страницам, что вызовет их предварительное выделение:

void prefault(void *addr, size_t length) {
    // Меняем права доступа, например на READ_WRITE
    if (mprotect(addr, length, PROT_READ | PROT_WRITE) == -1) {
        perror("mprotect");
        return;
    }

    // Вызов *volatile* для фактического доступа к страницам
    volatile char *p = addr;
    for (size_t i = 0; i < length; i += getpagesize()) {
        p[i] = 0;  // Принудительное использование страницы
    }
}

3. Полное решение

Вот интеграция этих идей. Вы можете использовать комбинацию mlockall и mprotect для обеспечения того, чтобы ваши страницы были физически заперты в памяти:

#include <sys/mman.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

void lock_memory() {
    // Блокируем текущее и будущие страницы
    if (mlockall(MCL_CURRENT | MCL_FUTURE) != 0) {
        perror("mlockall");
        exit(EXIT_FAILURE);
    }
}

void prefault_and_lock() {
    // Индикатор, указывающий на инициализацию
    // Можно использовать для выполнения функций перед блокировкой
    lock_memory();

    // Здесь ранее загруженные библиотеки
    // Должны перебрать их адреса и выделить страницы
    // Для примера ставим фиксированные адреса - изменитеตาม своим нуждам
    void *start_address = /* ваш начальный адрес */;
    size_t length = /* длина, которую хотите охватить */;

    // Предварительное выделение страниц памяти
    prefault(start_address, length);
}

int main() {
    // Ваша логика приложения
    prefault_and_lock();

    // ... ваша основная программа

    return 0;
}

Заключение

Это минимальное работоспособное решение, учитывающее динамическое выделение памяти и блокировку страниц. Не забудьте корректно оставлять ваши страницы заблокированными до завершения работы приложения, используя munlockall. Следует внимательно следить за тем, чтобы ваше приложение не вызывало чрезмерное использование оперативной памяти, так как использование mlockall и других функций может привести к его истощению, крайне важном для реальных приложений.

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

Оцените материал
Добавить комментарий

Капча загружается...