了解 64 位 Linux 上的 kmap

ils*_*tam 2 memory-management linux-kernel virtual-address-space 32bit-64bit arm64

首先我要承认,即使在阅读了一些相关资源之后,我对 Linux 上的高内存和低内存的概念仍然没有完全清楚。然而,根据我对 64 位 Linux 的了解,无论如何都没有高内存(如果我错了,请纠正我)。

我试图了解 kmap 和地址空间如何在defconfig为arm64配置的Linux内核版本5.8.1上工作。

我添加了以下系统调用:

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;
}
Run Code Online (Sandbox Code Playgroud)

我从用户空间分配整个内存页面(在页面边界上),并将其作为该系统调用的参数传递给内核。通过从内核内部取消引用任一指针来修改该内存效果非常好。但是,这两个指针具有不同的值:

[    4.493480] kernel: first: 0x4ff3000
[    4.493888] kernel: second: 0xffff000007ce9000
Run Code Online (Sandbox Code Playgroud)

据我了解,get_user_pages返回与该用户地址相对应的物理页(在当前的地址空间中)。然后,由于没有高端内存,我希望kmap从地址空间的用户部分返回完全相同的地址。

根据arm64的虚拟内存布局,返回的地址kmap位于“内核逻辑内存映射”描述的范围内。这是刚刚创建的新映射kmap还是同一页面的另一个先前存在的映射?

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

Ian*_*ott 5

一旦实际被 固定到物理内存中,由user_addr(或p1) 和 byp2引用的内存将是相同的物理内存页面get_user_pages()。(在get_user_pages()调用之前,这些页面可能尚未位于物理内存中。)但是,user_addr( 和p1) 是页面的用户空间地址,并且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);
Run Code Online (Sandbox Code Playgroud)

put_user()成功或-EFAULT失败时将返回 0。

get_user_pages()将返回固定到内存中的页面数,或者如果没有任何请求的页面可以被固定,则返回负 errno 值。(仅当请求的页面数为 0 时才会返回。0)实际固定的页面数可能小于请求的数量,但由于您的代码仅请求单个页面,因此在这种情况下的返回值将是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;
    }
Run Code Online (Sandbox Code Playgroud)

注意:您可以使用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;
    }
Run Code Online (Sandbox Code Playgroud)

kmap()将临时将页面映射到内核地址空间。kunmap()它应该与释放临时映射的调用配对:

    p2 = kmap(pages[0]);
    /* do something with p2 here ... */
    kunmap(p2);
Run Code Online (Sandbox Code Playgroud)

固定的页面需要在完成后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]);
Run Code Online (Sandbox Code Playgroud)

上面的代码并不完全健壮。指针p2可能因取消引用而未对齐*p2,或者*p2可能跨越页面边界。健壮的代码需要处理这种情况。

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