这段时间在看Fuchsia的代码,发现有很多去自己去做虚拟地址与物理地址映射的代码,一直觉得很奇怪,之前一直觉得MMU做虚拟地址与物理地址的映射,只是构建完成相关的映射表格,在x86上把这个表格指给cr3寄存器,后面开启虚拟地址后,程序访问的都是虚拟地址空间,MMU把虚拟地址转成物理地址,然后进行相关的寻址操作就欧克,至于这个映射表格的建立,我一直以为是一次性的,或者产生缺页的时候由MMU去维护更新就好了,刚好趁着这个机会,边读fuchsia的代码边理解下怎么去维护MMU的虚拟地址与物理地址的映射关系
什么是MMU
为了理解什么是MMU,可能先得捋清楚虚拟内存与物理内存的区别。
物理内存
这个很好理解,比如你买了台电脑,里面装了2跟8G的内存条,那么这个物理内存的大小就是16G,而物理地址空间,则是用来描述物理地址的范围的,这个跟芯片平台有比较大的关系,如果目前在intel X86的芯片上,对物理内存大小的支持分别如下:
- 分段方式:支持的 CPU: 8086 以上
地址长度: 20
寻址能力: 1M - 分页方式:支持的 CPU: 80386 以上, PSE 需要 Pentium 以上
地址长度: 32
寻址能力: 4GB - PAE: Physical Address Extension 物理地址扩展模式:支持的 CPU: Pentium Pro 以上
地址长度: 36
寻址能力: 64GB - 长模式 (long-mode, IA-32e 模式):支持的CPU: x86-64 的 CPU
地址长度: 48
寻址能力: 256 TB
目前fuchsia只支持intel 64位的CPU,所以不用关注前面三种的物理地址类型,只需要关注x86-64下的长模式就行,在这种模式下采用的是64位物理地址,但是只要低48位是有效地址,高16位是相关的标志位。至于为啥是48位有效地址,这个咱们就不关注了,反正是intel就是这么设计的。
虚拟内存
虚拟内存这个也好理解,意思就是这个内存所指向的数据不是真实可用的嘛,这个地址是需要一系列的转换才能转换成真实可用的数据(物理内存上的数据),这一系列的转换包括逻辑地址经过通过分段机制转成线性地址,线性地址再经过分页机制再转成了物理地址,而这个分页机制则是在我们这篇文章里需要重点探讨的。这里的逻辑地址与线性地址都可以认为是虚拟地址。
虚拟内存的存在的由来是,主要要两个原因:
- 因为硬件内存的多样性,比如一台电脑有2G的物理内存也有4G的物理内存还可能有16G的物理内存,不管是操作系统还是应用程序都不希望自己需要根据真实物理内存的大小去做相关的调整,这样估计得烦死,这就是为啥引入虚拟内存,目的就是为了屏蔽掉机器上物理内存的差异,让操作系统或者应用程序都只采用虚拟地址。
- 物理内存是需要钱的,你买个8G内存跟16G内存价格是有差异的,但是虚拟内存是不要钱的。又不要钱,又能让操作系统跟应用的开发者不那么头疼,这样的事大家都愿意接受哈。
还有一个关注点就是,比如x86 64位是机器上虚拟地址空间往往也是48位的有效地址,那么可以寻址的空间大小就是256TB,物理地址也是采用48位的有效地址,物理地址的寻址空间大小也是256TB,但是有个问题:谁的机器上会有256TB的内存?目前主流的机器基本上是16G内存。怎么把256TB的虚拟地址映射到16G的真实物理地址上?这也是MMU需要去做的。
MMU
正因为上面所讲的目前计算机体系中有物理内存与虚拟内存的存在,同是这就就引入了一个虚拟地址与物理地址的转换问题。如前面讲到的,经过分段机制之后的线性地址才会经过分页机制去转成最终的物理地址,这就是MMU所干的活:
虚拟地址------》物理地址
分段机制,就不在这里赘述了,重点讲讲分页机制。
简单用文字描述下:
- X86上有一系列的页表来完成分页机制。
- X86平台上有一个CR3的控制寄存器,这个寄存器里存放的是一个顶级页表的物理地址,这个页表叫做PLM4
- 在X86 64位平台上采用了四级页表的方式,分别是PML4表,PDP表(Page directory point),PD表(Page directory),PT表(Page table)
- 上面的各个表是一个级联的指向关系:PML4指向PDP,PDP指向PD,PD指向PT,PT指向具体的物理页面。
- 48位的虚拟地址是这样去分的,最高的9位标记在PML4表指向哪个PDP,后面9位标记PDP表中指向哪个PD,再后面9位是标记PD表中指向哪个PT,再跟着的9位是标记PT表中指向的是哪个物理页地址,最后12位是物理页内的偏移,注意这里的物理页大小采用的是4K模式。
用一张Intel官方的图来概括以上内容:
Fuchsia上X86平台的MMU操作
在上面,大致介绍了下虚拟内存与物理内存,已经虚拟内存与物理内存映射的基本概念,接下来我们看看在X86平台上大致是怎样的一种操作方式。
映射表的创建
如上面提到的在X86平台上这个映射表可以直接叫PML4表,另外如我们在上一遍文章Fuchsia X86 kernel启动代码分析中提到的,在Start.S中我们可以看到这样的代码:
/*
* Set PGE to enable global kernel pages
*/
mov %cr4, %rax
or $(X86_CR4_PGE), %rax
mov %rax, %cr4
//将pml4放进cr3中,物理内存与虚拟内存映射生效
/* load the physical pointer to the top level page table */
mov $PHYS(pml4), %rax
mov %rax, %cr3
————————————————
版权声明:本文为CSDN博主「影子LEON」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/ljp1205/article/details/106231332
至于整个PML4表的创建过程,依然可以参考Fuchsia X86 kernel启动代码分析,在这个里面重点介绍了下,在这个阶段重点做了fuchsia kernel的地址空间的虚拟地址与物理地址映射。
今天我们这里重点介绍下后续这个PML4表格的更新,建立真实可用的映射关系。
映射表的维护
在进入介绍怎么维护更新映射表之前,可能需要先大致捋一下在Fuchsia当中关于内存的一些简单概念:
- VmAspace:这个是一整个范围的虚拟地址空间,这个虚拟地址空间有起始地址与范围长度,这个概念是整个虚拟地址的一个子集,比如整个虚拟地址空间是用48位来描述,相当与整个虚拟地址空间是0到256TB的范围,而一个VmAspace则是0到256TB这个范围中的一段。Fuchsia在启动过程中会为Kernel建立一个Kernel 的VmAspace:
void VmAspace::KernelAspaceInitPreHeap() TA_NO_THREAD_SAFETY_ANALYSIS {
// the singleton kernel address space
static VmAspace _kernel_aspace(KERNEL_ASPACE_BASE, KERNEL_ASPACE_SIZE, VmAspace::TYPE_KERNEL,
"kernel");
// the singleton dummy root vmar (used to break a reference cycle in
// Destroy())
static VmAddressRegionDummy dummy_vmar;
#if LK_DEBUGLEVEL > 1
_kernel_aspace.Adopt();
dummy_vmar.Adopt();
#endif
dummy_root_vmar = &dummy_vmar;
static VmAddressRegion _kernel_root_vmar(_kernel_aspace);
_kernel_aspace.root_vmar_ = fbl::AdoptRef(&_kernel_root_vmar);
zx_status_t status = _kernel_aspace.Init();
ASSERT(status == ZX_OK);
// save a pointer to the singleton kernel address space
VmAspace::kernel_aspace_ = &_kernel_aspace;
aspaces.push_front(kernel_aspace_);
}
这个VmAspace的起始地址与大小分别是:
#define KERNEL_ASPACE_BASE 0xffffff8000000000UL // -512GB
#define KERNEL_ASPACE_SIZE 0x0000008000000000UL
也就是说起点是从512GB的位置开始,大小是64GB
- VmAddressRegion :这个是指一段连续的虚拟内存区域,一个VmAspace会有个一个root 的VmAddressRegion,root VmAddressRegion的起始地址与大小与VmAspace的起始地址跟大小一致。然后还会有很多个子的 VmAddressRegion。Kernel VmAspace 中目前有预留一个Kernel VmAddressRegion :
zx_status_t status = aspace->RootVmar()->CreateSubVmar(
kernel_regions[0].base - aspace->RootVmar()->base(), kernel_region_size, 0,
VMAR_FLAG_CAN_MAP_SPECIFIC | VMAR_FLAG_SPECIFIC | VMAR_CAN_RWX_FLAGS, "kernel region vmar",
&kernel_region);
ASSERT(status == ZX_OK);
for (const auto& region : kernel_regions) {
ASSERT(IS_PAGE_ALIGNED(region.base));
dprintf(INFO,
"VM: reserving kernel region [%#" PRIxPTR ", %#" PRIxPTR ") flags %#x name '%s'\n",
region.base, region.base + region.size, region.arch_mmu_flags, region.name);
status =
kernel_region->ReserveSpace(region.name, region.base, region.size, region.arch_mmu_flags);
ASSERT(status == ZX_OK);
}
在Kernel VmAddressRegion预留了这么些部分的空间地址长度:
const ktl::array _kernel_regions = {
kernel_region{
.name = "kernel_code",
.base = (vaddr_t)__code_start,
.size = ROUNDUP((uintptr_t)__code_end - (uintptr_t)__code_start, PAGE_SIZE),
.arch_mmu_flags = ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_EXECUTE,
},
kernel_region{
.name = "kernel_rodata",
.base = (vaddr_t)__rodata_start,
.size = ROUNDUP((uintptr_t)__rodata_end - (uintptr_t)__rodata_start, PAGE_SIZE),
.arch_mmu_flags = ARCH_MMU_FLAG_PERM_READ,
},
kernel_region{
.name = "kernel_data",
.base = (vaddr_t)__data_start,
.size = ROUNDUP((uintptr_t)__data_end - (uintptr_t)__data_start, PAGE_SIZE),
.arch_mmu_flags = ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE,
},
kernel_region{
.name = "kernel_bss",
.base = (vaddr_t)__bss_start,
.size = ROUNDUP((uintptr_t)_end - (uintptr_t)__bss_start, PAGE_SIZE),
.arch_mmu_flags = ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE,
},
};
Kernel VmAspace 与Kernel VmAddressRegion的区别是,Kernel VmAspace描述的是整个kernel内存空间,而Kernel VmAddressRegion描述的是kernel image被装载进内存的空间与大小。
. = KERNEL_BASE;
PROVIDE_HIDDEN(__code_start = .);
/kernel/BUILD.gn:95: "KERNEL_BASE=$kernel_base"
if (current_cpu == "arm64") {
kernel_base = "0xffffffff00000000"
} else if (current_cpu == "x64") {
kernel_base = "0xffffffff80100000" # Has KERNEL_LOAD_OFFSET baked into it.
}
可以看到Kernel VmAddressRegion的base地址是0xffffffff80100000,而Kernel VmAspace的base地址是0xffffffff80000000
- VmMapping:前面的VmAspace与VmAddressRegion都是一段段的虚拟内存,还没有跟物理内存挂上钩,而VmMapping则是需要真实去分配物理内存的,也就是说VmMapping是需要分配物理的内存的比较小的虚拟内存颗粒度,当某个VmAddressRegion需要映射到具体的物理内存上的时候,VmAddressRegion会通过构建VmMapping的方式去映射。VmAspace太大了,像Kernel VmAspace就是64G的长度,不可能都去映射到物理内存,只有小点的VmAddressRegion才会去映射
zx_status_t VmAddressRegion::ReserveSpace(const char* name, vaddr_t base, size_t size,
uint arch_mmu_flags) {
canary_.Assert();
if (!is_in_range(base, size)) {
return ZX_ERR_INVALID_ARGS;
}
size_t offset = base - base_;
// We need a zero-length VMO to pass into CreateVmMapping so that a VmMapping would be created.
// The VmMapping is already mapped to physical pages in start.S.
// We would never call MapRange on the VmMapping, thus the VMO would never actually allocate any
// physical pages and we would never modify the PTE except for the permission change bellow
// caused by Protect.
fbl::RefPtr<VmObject> vmo;
zx_status_t status = VmObjectPaged::Create(PMM_ALLOC_FLAG_ANY, 0u, 0, &vmo);
if (status != ZX_OK) {
return status;
}
vmo->set_name(name, strlen(name));
// allocate a region and put it in the aspace list
fbl::RefPtr<VmMapping> r(nullptr);
// Here we use permissive arch_mmu_flags so that the following Protect call would actually
// call arch_aspace().Protect to change the mmu_flags in PTE.
status = CreateVmMapping(
offset, size, 0, VMAR_FLAG_SPECIFIC, vmo, 0,
ARCH_MMU_FLAG_PERM_READ | ARCH_MMU_FLAG_PERM_WRITE | ARCH_MMU_FLAG_PERM_EXECUTE, name, &r);
if (status != ZX_OK) {
return status;
}
return r->Protect(base, size, arch_mmu_flags);
zx_status_t VmAddressRegion::CreateVmMapping(size_t mapping_offset, size_t size, uint8_t align_pow2,