内核虚拟地址到底是如何转换为物理RAM的?

kai*_*wan 17 linux memory-management linux-kernel

从表面上看,这似乎是一个愚蠢的问题.请耐心等.. :-)我将这个qs分为两部分:

第1部分: 我完全理解平台RAM映射到内核段; 特别是在64位系统上,这将运作良好.因此每个内核虚拟地址实际上只是物理内存(DRAM)的偏移量.

另外,我的理解是,由于Linux是一个现代虚拟内存操作系统,(几乎所有)所有地址都被视为虚拟地址,必须在运行时通过硬件"TLB/MMU",然后通过TLB/MMU进行转换通过内核分页表.同样,易于理解用户模式进程.

但是,内核虚拟地址怎么样?为了提高效率,直接映射这些并不简单(并且身份映射确实从PAGE_OFFSET开始设置).但是,在运行时,内核虚拟地址必须通过TLB/MMU并正确转换?实际情况如此吗?或者内核虚拟地址转换只是一个偏移计算?(但那怎么可能,因为我们必须通过硬件TLB/MMU?).举个简单的例子,我们考虑一下:

char *kptr = kmalloc(1024, GFP_KERNEL);
Run Code Online (Sandbox Code Playgroud)

现在kptr是一个内核虚拟地址.我知道virt_to_phys()可以执行偏移计算并返回物理DRAM地址.但是,这是实际问题:它不能通过软件以这种方式完成 - 这可能会很慢!所以,回到我之前的观点:它必须通过硬件(TLB/MMU)进行翻译. 这是真的吗?

第2部分: 好的,让我们说是这种情况,我们确实在内核中使用分页来做到这一点,我们当然必须设置内核分页表; 据我所知,它的根源在于swapper_pg_dir.

(我也理解vmalloc()与kmalloc()不同是一种特殊情况 - 它是一个纯虚拟区域,仅在页面错误时才被物理帧支持).

如果(在第1部分中)我们得出结论内核虚拟地址转换是通过内核分页表完成的,那么内核分页表(swapper_pg_dir)到底是如何"附加"或"映射"到用户模式进程?这应该发生在上下文切换代码中?怎么样?哪里?

例如.在x86_64上,2个进程A和B是活动的,1个cpu.A正在运行,因此它是更高规范的addr 0xFFFF8000 00000000 through 0xFFFFFFFF FFFFFFFF "映射"到内核段,它是低级规范的addr 0x0 through 0x00007FFF FFFFFFFF 映射到它的私有用户空间.

现在,如果我们上下文切换A-> B,进程B的低规范区域是唯一的但是它必须"映射"到同一个内核当然!这究竟是怎么发生的?在内核模式下,我们如何"自动"引用内核分页表?或者这是一个错误的陈述?

感谢您的耐心,非常感谢您深思熟虑的答案!

Stu*_*efy 15

先来一点背景.

这是一个架构之间存在很多潜在差异的领域,但原始海报表明他主要对x86和ARM感兴趣,它们具有以下几个特征:

  • 没有硬件段或虚拟地址空间的类似分区(当由Linux使用时)
  • 硬件页面表走
  • 多页面大小
  • 物理标记的缓存(至少在现代ARM上)

因此,如果我们将自己限制在那些系统中,它会让事情更简单.

启用MMU后,它通常永远不会关闭.因此,所有CPU地址都是虚拟的,并将使用MMU转换为物理地址.MMU将首先在TLB中查找虚拟地址,并且只有在TLB中找不到它才会引用页面表 - TLB是页表的缓存 - 因此我们可以忽略TLB对于这个讨论.

页表描述了整个虚拟32位或64位地址空间,并包含以下信息:

  • 虚拟地址是否有效
  • 处理器必须处于哪种模式才能使其有效
  • 内存映射硬件寄存器等特殊属性
  • 和要使用的物理地址

Linux的划分虚拟地址空间分成两个:下部被用于用户进程,并有一个不同的虚拟为每个过程的物理映射.上部用于内核,即使在不同的用户进程之间切换,映射也是相同的.这使事情变得简单,因为地址在用户或内核空间中是明确的,在进入或离开内核时不需要更改页表,并且内核可以简单地将指针解引用到当前用户进程的用户空间.通常在32位处理器上,拆分是3G用户/ 1G内核,尽管这可能会有所不同.仅当处理器处于内核模式时,地址空间的内核部分的页面才会被标记为可访问,以防止用户进程可以访问它们.内核地址空间中标识映射到RAM(内核逻辑地址)的部分将尽可能使用大页面进行映射,这可能允许页表更小但更重要的是减少TLB未命中的数量.

当内核启动时,它会为self(swapper_pg_dir)创建一个页面表,它只描述虚拟地址空间的内核部分,并且没有地址空间的用户部分的映射.然后,每次创建用户进程时,将为该进程生成新的页表,描述内核内存的部分在每个页表中都是相同的.这可以通过复制所有相关部分来完成swapper_pg_dir,但由于页表通常是树结构,内核经常能够将描述内核地址空间的树部分移植swapper_pg_dir到每个用户进程的页表中只需复制页表结构上层的几个条目.除了在内存(以及可能的缓存)使用方面更高效之外,还可以更容易地保持映射的一致性.这是内核和用户虚拟地址空间之间的分割只能在某些地址发生的原因之一.

要了解如何针对特定体系结构执行此操作,请查看实现pgd_alloc().例如ARM(arch/arm/mm/pgd.c)使用:

pgd_t *pgd_alloc(struct mm_struct *mm)
{
    ...
    init_pgd = pgd_offset_k(0);
    memcpy(new_pgd + USER_PTRS_PER_PGD, init_pgd + USER_PTRS_PER_PGD,
               (PTRS_PER_PGD - USER_PTRS_PER_PGD) * sizeof(pgd_t));
    ...
}
Run Code Online (Sandbox Code Playgroud)

或者x86(arch/x86/mm/pgtable.c)pgd_alloc()调用pgd_ctor():

static void pgd_ctor(struct mm_struct *mm, pgd_t *pgd)
{
    /* If the pgd points to a shared pagetable level (either the
       ptes in non-PAE, or shared PMD in PAE), then just copy the
       references from swapper_pg_dir. */
        ...
        clone_pgd_range(pgd + KERNEL_PGD_BOUNDARY,
                swapper_pg_dir + KERNEL_PGD_BOUNDARY,
                KERNEL_PGD_PTRS);
    ...
}
Run Code Online (Sandbox Code Playgroud)

那么,回到原来的问题:

第1部分:内核虚拟地址是否真的由TLB/MMU翻译?

是.

第2部分:如何swapper_pg_dir"附加"到用户模式进程.

所有页表(无论是否swapper_pg_dir为用户进程)都具有与用于内核虚拟地址的部分相同的映射.因此,作为内核上下文用户进程之间切换,改变当前页表中,地址空间的内核部分的映射保持不变.