从表面上看,这似乎是一个愚蠢的问题。有些耐心请.. :-) 我将这个qs分为两部分:
第1部分: 我完全理解平台RAM映射到内核段;特别是在64位系统上,这将运作良好。因此,每个内核虚拟地址实际上只是物理内存(DRAM)的偏移量。
另外,我的理解是,由于Linux是一个现代虚拟内存操作系统,(几乎所有)所有地址都被视为虚拟地址,必须通过硬件“转发” - TLB / MMU - at运行时,然后由TLB / MMU通过内核分页表进行转换。同样,易于理解用户模式流程。
但是,内核虚拟地址怎么样?为了提高效率,直接映射这些并不简单(并且身份映射确实是从PAGE_OFFSET开始设置的)。但是,在运行时,内核虚拟地址 必须 通过TLB / MMU进行转换并正确转换???实际情况如此吗?或者内核虚拟地址转换只是一个偏移计算? (但是怎么可能,因为我们必须通过硬件TLB / MMU?)。举个简单的例子,我们考虑一下:
char *kptr = kmalloc(1024, GFP_KERNEL);
现在kptr是一个内核虚拟地址。 我知道virt_to_phys()可以执行偏移计算并返回物理DRAM地址。 但是,这里是实际问题:它不能通过软件以这种方式完成 - 这可能会很慢!所以,回到我之前的观点:它必须通过硬件(TLB / MMU)进行翻译。 实际上是这样的吗?
第2部分: 好吧,让我们说是这种情况,我们确实在内核中使用分页来做到这一点,我们当然必须设置内核分页表;据我所知,它的根源是swapper_pg_dir。
(我也明白,与kmalloc()不同的是,vmalloc()是一种特殊情况 - 它是一个纯虚拟区域,仅在页面错误时才被物理帧支持。)
如果(在第1部分中)我们得出结论内核虚拟地址转换是通过内核分页表完成的,那么内核分页表(swapper_pg_dir)如何“附加”或“映射”到用户模式进程? ?这应该发生在上下文切换代码中?怎么样?在哪里?
EG。
在x86_64上,2个进程A和B是活动的,1个cpu。
A正在运行,所以它是更高规范的地址
0xFFFF8000 00000000 through 0xFFFFFFFF FFFFFFFF
“映射”到内核段,它是较低规范的地址
0x0 through 0x00007FFF FFFFFFFF
映射到它的私有用户空间。
现在,如果我们上下文切换A-> B,则进程B的低规范区域是唯一的但是 它当然必须“映射”到同一个内核! 这究竟是怎么发生的?我们如何“自动”引用内核分页表 在内核模式?或者这是一个错误的陈述?
感谢您的耐心,非常感谢您深思熟虑的答案!
答案 0 :(得分:14)
首先是一些背景知识。
这个区域之间存在很多潜在的差异 建筑,但原始海报表明他主要是 对x86和ARM感兴趣,它们具有以下几个特征:
因此,如果我们将自己限制在那些系统中,它会让事情变得更简单。
启用MMU后,它永远不会被关闭。所以所有的CPU 地址是虚拟的,并将被转换为物理地址 使用MMU。 MMU将首先在中查找虚拟地址 TLB,只有它没有在TLB中找到它才会引用它 页表 - TLB是页表的缓存 - 所以我们可以 忽略TLB进行讨论。
页面表 描述整个虚拟32或64位地址空间,并包括 信息如:
Linux将虚拟地址空间分为两部分:下部是 用于用户进程,并且存在不同的虚拟到物理 映射每个进程。上部用于内核, 即使在不同用户之间切换,映射也是一样的 流程。这样可以保持简单,因为地址是明确的 用户或内核空间,页面表不需要更改时 进入或离开内核,内核可以简单地取消引用 指向用户空间的指针 当前用户进程。通常在32位处理器上,拆分为3G user / 1G内核,虽然这可能会有所不同。内核部分的页面 仅当处理器时,地址空间的标记才会被标记为可访问 处于内核模式以防止用户进程可以访问它们。 内核地址空间的一部分,它被标识映射到RAM (核心逻辑地址)将尽可能使用大页面进行映射, 这可能允许页表更小但更重要的是 减少TLB未命中数。
当内核启动时,它会为自己创建一个页面表
(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));
...
}
或
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);
...
}
所以,回到最初的问题:
第1部分:内核虚拟地址是否真的由TLB / MMU翻译?
是
第2部分:swapper_pg_dir
"如何附加"到用户模式过程。
所有页面表(无论是swapper_pg_dir
还是用户进程的表)
对于用于内核虚拟的部分具有相同的映射
地址。因此,当内核上下文在用户进程之间切换时,
更改当前页表,内核部分的映射
地址空间保持不变。
答案 1 :(得分:2)
内核地址空间映射到每个进程的一部分,例如在地址0xC0000000之后的3:1映射上。如果用户代码尝试访问此地址空间,则会生成页面错误,并且内核会对其进行保护。 内核地址空间分为两部分,即逻辑地址空间和虚拟地址空间。它由常量VMALLOC_START定义。 CPU一直在用户空间和内核空间中使用MMU(无法打开/关闭)。 内核虚拟地址空间的映射方式与用户空间映射相同。逻辑地址空间是连续的,将其转换为物理地址很简单,因此可以使用MMU故障异常按需完成。那就是内核正在尝试访问一个地址,MMU生成错误,故障处理程序使用宏__pa,__ go映射页面并在发生故障之前将CPU pc寄存器更改回上一条指令,现在一切正常。这个过程实际上是依赖于平台的,在某些硬件架构中,它的映射方式与用户相同(因为内核不会占用大量内存)。