从表面上看,这似乎是一个愚蠢的问题。请耐心点.. :-)
我将这个qs分为两部分:
第1部分:
我完全理解平台RAM已映射到内核段。尤其是在64位系统上,效果很好。因此,每个内核虚拟地址的确只是物理内存(DRAM)的偏移量。
另外,据我了解,由于Linux是现代的虚拟内存操作系统,(几乎)所有地址都被视为虚拟地址,必须在运行时通过硬件(TLB/MMU)“运行”,然后由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。
(我也知道vmalloc()与kmalloc()不同,这是特例-它是一个纯虚拟区域,仅在页面错误时才由物理帧支持)。
如果(在第1部分中)我们确实得出结论,内核虚拟地址转换是通过内核分页表完成的,那么内核分页表(swapper_pg_dir)究竟是如何“附加”或“映射”到用户模式进程的呢?这应该在上下文切换代码中发生吗?怎么样?哪里?
例如。
在x86_64上,两个进程A和B处于 Activity 状态,即1 cpu。
A正在运行,因此它是规范较高的地址
0xFFFF8000 00000000 through 0xFFFFFFFF FFFFFFFF
“映射”到内核段,它是标准的下位地址0x0 through 0x00007FFF FFFFFFFF
映射到它的私有(private)用户空间。现在,如果我们上下文切换A-> B,则进程B的下规范区域是唯一的,但是
它必须“映射”到相同的内核!
这到底是怎么发生的?我们如何“自动”引用内核分页表时
在内核模式下?还是那是错误的说法?
多谢您的耐心配合,我们将不胜感激!
最佳答案
首先介绍一下背景。
这个区域之间存在很大的潜在差异
建筑,但是原始海报表明他主要是
对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
到每个页表的内核地址空间用户流程,只需复制一些内容即可
页表结构。以及更有效的存储(可能
缓存)的使用情况,可以更轻松地保持映射的一致性。这个
是内核和用户虚拟机之间 split 的原因之一
地址空间只能出现在某些地址。
要了解如何针对特定架构完成此操作,请参阅
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
还是用于用户进程的页表)对于用于内核虚拟的部分具有相同的映射
地址。因此,随着内核上下文在用户进程之间切换,
更改当前页表,内核部分的映射
的地址空间保持不变。
关于linux - 内核虚拟地址如何准确地转换为物理RAM?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/36639607/