了解 64 位上的 kmap Linux

Understanding kmap on 64-bit Linux

首先承认 Linux 上的高端内存和低端内存的概念,即使阅读了几个相关资源,我的脑海中仍然不是很清楚。但是,根据我对 64 位 Linux 的理解,无论如何都没有高内存(如果我错了请纠正我)。

我想了解 kmap 和地址 spaces 如何在 Linux 内核版本 5.8.1 上工作,为 arm64 配置 defconfig

我添加了以下系统调用:

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

从 user-space 我分配了整个内存页面(在页面边界上),我将其作为该系统调用的参数传递给内核。通过从内核中取消引用任一指针来修改该内存工作得很好。但是,这两个指针具有不同的值:

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

据我了解get_user_pages return是对应于该用户地址的物理页面(在当前地址space)。然后,由于没有高内存,我希望 kmap 到 return 与地址 space.

的用户部分完全相同的地址

根据 virtual memory layout of arm64,由 kmap 编辑的地址 return 位于描述为“内核逻辑内存映射”的范围内。这是 kmap 刚刚创建的新映射还是同一页面之前存在的另一个映射?

有人可以解释一下这里到底发生了什么吗?

user_addr(或p1)和p2引用的内存将是相同的物理内存页,一旦它们被[=18]实际固定到物理内存中=]. (在 get_user_pages() 调用之前,页面可能还不在物理内存中。)但是,user_addr(和 p1)是页面的 user-space 地址,并且 p2 是页面的 kernel-space 地址。 kmap() 将创建物理内存页到 kernel-space 的临时映射。

在 arm64(以及 amd64)上,如果第 63 位被视为符号位,则 user-space 地址为 non-negative,内核 space 地址为负。所以 user-space 和 kernel-space 地址的数值不可能相等。

大多数内核代码不应直接取消引用 user-space 指针。应使用 user-space 内存访问函数和宏,并应检查是否存在故障。你的例子的第一部分应该是这样的:

    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() 成功时 return 0 或失败时 -EFAULT

get_user_pages() 将 return 固定到内存中的页面数,或者如果可以固定所请求页面的 none 个负错误值。 (如果请求的页面数为 0,它只会 return 0。)实际固定的页面数可能少于请求的数量,但由于您的代码仅请求单个页面,因此 return 值在这种情况下要么是 1 要么是一个负的 errno 值。您可以使用变量来捕获错误号。请注意,必须在当前任务的 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_fast() 而不是 get_user_pages()。如果使用 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() 会临时映射一个页面到内核地址space。它应该与对 kunmap() 的调用配对以释放临时映射:

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

get_user_pages() 固定的页面在完成后需要使用 put_page() 'put'。如果它们已被写入,则首先需要使用 set_page_dirty_lock() 将它们标记为 'dirty'。你的例子的最后一部分应该是这样的:

    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 可能跨越页面边界。健壮的代码需要处理这种情况。

由于通过 user-space 地址访问内存需要通过特殊的 user-space 访问函数和宏来完成,可能会因页面错误而休眠(除非页面已被锁定到物理内存中),并且仅在单个进程中有效(如果有的话),使用 get_user_pages() 将 user-space 地址区域锁定到内存并将页面映射到内核地址 space (如果需要)在一些情况。它允许从任意内核上下文(例如中断处理程序)访问内存。它允许批量复制到和从内存映射 I/O(memcpy_toio()memcpy_fromio())。一旦被 get_user_pages() 锁定,就可以在 user-memory 上执行 DMA 操作。在那种情况下,页面将被 DMA API.

映射到“DMA 地址”