Общие сведения о kmap в 64-разрядной версии Linux

Позвольте мне начать с признания того, что концепция высокой и низкой памяти в Linux все еще не совсем ясна для меня даже после прочтения нескольких соответствующих ресурсов. Однако, насколько я понимаю, в 64-битном Linux в любом случае нет верхней памяти (поправьте меня, если я ошибаюсь).

Я пытаюсь понять, как работают kmap и адресные пространства в ядре Linux версии 5.8.1, сконфигурированном с defconfig для arm64.

Я добавил следующий системный вызов:

SYSCALL_DEFINE1(mycall, unsigned long __user, user_addr)
{
    struct page *pages[1];
    int *p1, *p2;

    p1 = (int *) user_addr;
    *p1 = 1; /* this works */
    pr_info("kernel: first: 0x%lx", (long unsigned) p1);

    if (get_user_pages(user_addr, 1, FOLL_WRITE, pages, NULL) != 1)
        return -1;

    p2 = kmap(pages[0]);
    *p2 = 2; /* this also works */
    pr_info("kernel: second: 0x%lx", (long unsigned) p2);

    return 0;
}

Из пользовательского пространства я выделяю целую страницу памяти (на границе страницы), которую передаю ядру в качестве параметра этого системного вызова. Изменение этой памяти путем разыменования любого указателя из ядра работает отлично. Однако два указателя имеют разные значения:

[    4.493480] kernel: first: 0x4ff3000
[    4.493888] kernel: second: 0xffff000007ce9000

Насколько я понимаю, get_user_pages возвращает физическую страницу, соответствующую этому адресу пользователя (в текущем адресном пространстве). Затем, поскольку верхней памяти нет, я ожидал, что kmap вернет точно такой же адрес из пользовательской части адресного пространства.

Согласно разметке виртуальной памяти arm64, возвращаемый адрес by kmap лежит в диапазоне, описанном как карта логической памяти ядра. Это новое сопоставление, только что созданное kmap, или это другое ранее существовавшее сопоставление для той же страницы?

Может кто-нибудь объяснить, что именно здесь происходит?


person ilstam    schedule 16.08.2020    source источник


Ответы (1)


Память, на которую ссылаются user_addr (или p1) и p2, будет одними и теми же страницами физической памяти после того, как они будут фактически закреплены в физической памяти get_user_pages(). (До вызова get_user_pages() страницы могли еще не находиться в физической памяти.) Однако user_addrp1) — это адрес страницы в пространстве пользователя, а p2 — это адрес страницы в пространстве ядра. kmap() создаст временное сопоставление страницы физической памяти с пространством ядра.

В arm64 (а также в amd64), если бит 63 обрабатывается как бит знака, то адреса пространства пользователя неотрицательны, а адреса пространства ядра отрицательны. Таким образом, числовые значения адресов пространства пользователя и пространства ядра не могут быть равными.

Большая часть кода ядра не должна напрямую разыменовывать указатели пользовательского пространства. Следует использовать функции и макросы доступа к памяти пользовательского пространства и проверять их на наличие сбоев. Первая часть вашего примера должна быть примерно такой:

    int __user *p1 = (int __user *)user_addr;

    if (put_user(1, p1))
        return -EFAULT;
    pr_info("kernel: first: 0x%lx\n", (unsigned long)p1);

put_user() вернет 0 в случае успеха или -EFAULT в случае неудачи.

get_user_pages() вернет либо количество страниц, закрепленных в памяти, либо отрицательное значение ошибки, если ни одна из запрошенных страниц не может быть закреплена. (Он вернет 0 только в том случае, если количество запрошенных страниц равно 0.) Количество фактически закрепленных страниц может быть меньше запрошенного, но, поскольку ваш код запрашивает только одну страницу, возвращаемое значение в этом случае будет либо 1 или отрицательное значение ошибки. Вы можете использовать переменную для захвата номера ошибки. Обратите внимание, что он должен вызываться с заблокированным семафором mmap текущей задачи:

#define NR_REQ 1

    struct page *pages[NR_REQ];
    long nr_gup;

    mmap_read_lock(current->mm);
    nr_gup = get_user_pages(user_addr, NR_REQ, FOLL_WRITE, pages, NULL);
    mmap_read_unlock(current->mm);
    if (nr_gup < 0)
        return nr_gup;
    if (nr_gup < NR_REQ) {
        /* Some example code to deal with not all pages pinned - just 'put' them. */
        long i;

        for (i = 0; i < nr_gup; i++)
            put_page(pages[i]);
        return -ENOMEM;
    }

Примечание. Вместо get_user_pages() можно использовать get_user_pages_fast(). Если используется get_user_pages_fast(), вызовы mmap_read_lock() и mmap_read_unlock() выше должны быть удалены:

#define NR_REQ 1

    struct page *pages[NR_REQ];
    long nr_gup;

    nr_gup = get_user_pages_fast(user_addr, NR_REQ, FOLL_WRITE, pages);
    if (nr_gup < 0)
        return nr_gup;
    if (nr_gup < NR_REQ) {
        /* Some example code to deal with not all pages pinned - just 'put' them. */
        long i;

        for (i = 0; i < nr_gup; i++)
            put_page(pages[i]);
        return -ENOMEM;
    }

kmap() временно отобразит страницу в адресное пространство ядра. Он должен быть связан с вызовом kunmap() для освобождения временного сопоставления:

    p2 = kmap(pages[0]);
    /* do something with p2 here ... */
    kunmap(p2);

Страницы, закрепленные с помощью get_user_pages(), необходимо «поместить» с помощью put_page(), когда закончите. Если они были записаны, их сначала нужно пометить как «грязные» с помощью set_page_dirty_lock(). Последняя часть вашего примера должна выглядеть примерно так:

    p2 = kmap(pages[0]);
    *p2 = 2; /* this also works */
    pr_info("kernel: second: 0x%lx\n", (unsigned long)p2);
    kunmap(p2);
    set_page_dirty_lock(pages[0]);
    put_page(pages[0]);

Приведенный выше код не является полностью надежным. Указатель p2 может быть смещен для *p2 разыменования, или *p2 может выходить за границу страницы. Надежный код должен справляться с такими ситуациями.

Поскольку доступ к памяти через адреса пользовательского пространства необходимо выполнять с помощью специальных функций и макросов доступа к пользовательскому пространству, они могут засыпать из-за ошибок страниц (если только страницы не были заблокированы в физической памяти) и действительны только (если вообще) в рамках одного процесса блокировка области адресов пользовательского пространства в памяти с помощью get_user_pages() и сопоставление страниц с адресным пространством ядра (при необходимости) полезно в некоторых обстоятельствах. Это позволяет получить доступ к памяти из произвольного контекста ядра, такого как обработчик прерывания. Это позволяет выполнять массовое копирование в и из отображаемого в память ввода-вывода (memcpy_toio() или memcpy_fromio()). Операции прямого доступа к памяти могут выполняться с пользовательской памятью после того, как она будет заблокирована get_user_pages(). В этом случае страницы будут сопоставлены с адресами DMA API DMA.

person Ian Abbott    schedule 17.08.2020
comment
Спасибо за ваш ответ, это очень полезно. Однако, возможно, мой вопрос был неполным. Я забыл спросить: зачем ядру вообще создавать новое сопоставление для этой страницы? Почему бы просто не использовать адрес пользовательского пространства для ссылки на него (после предварительной проверки)? - person ilstam; 18.08.2020
comment
Но теперь, подумав еще немного. Это на тот случай, если ядру потребуется сослаться на эту страницу при выполнении в контексте другого пользовательского процесса (т. е. в другом адресном пространстве)? - person ilstam; 18.08.2020
comment
@ilstam Точно. Адрес пользовательского пространства действителен только в пределах определенного контекста процесса. Кроме того, макросам доступа к пользовательскому пространству может по-прежнему необходимо что-то делать с адресом, даже если страницы были заблокированы в памяти. - person Ian Abbott; 18.08.2020
comment
Хорошо, я вижу. Если хотите, добавьте к своему ответу это краткое объяснение того, почему ядру нужно создать новое сопоставление с помощью kmap, и я приму его. - person ilstam; 18.08.2020