Linux内存管理:虚拟地址空间(AArch64)
相关背景:
文章开始前,先聊聊相关的背景知识,我们知道64位处理器的虚拟地址已经支持到了64bit,但是64位处理器的物理地址总线实际位宽并没有达到64bit,常用的地址线宽有39bit和48bit,最新的ARMv8.2架构也已经可以支持到52bit了。那为什么没有支持到64bit呢?以常用的48bit地址线宽举例,其最大寻址能力是2^48 bytes(即256TB内存),对于当今的个人电脑或服务器来说都是足够用的。再加上增加地址总线的宽度会给芯片设计上带来不小的难度,所以并没有一步到位搞成64bit。
本文主要介绍ARM64位处理器地址空间的布局。前文已提到地址总线宽度有39bit、48bit以及52bit,且64位处理器又支持3级或4级页表,页大小也可以配置成4KB或64KB,组合起来的话情况太多,本文为了简化,就基于最常用组合展开叙述:48bit地址总线,4级页表,页面大小4KB。 且不包括ARM64虚拟化模式和安全模式下的情况。
新内核变动:
再来说一下写这篇文章时的感慨吧,kernel变化的真快,之前我记得4.x的内核的内核空间的线性映射区位于内核空间的高地址处的128TB,且当前的博客和一些书籍也都还是这样介绍。可翻了翻kernel的Documentation/arm64/memory.rst文档,发现最新的kernel已将这128TB移到了内核空间的最低地址处了。具体是2019年8月的一个commit,如下:
commit 14c127c957c1c6070647c171e72f06e0db275ebf
Author: Steve Capper <steve.capper@arm.com>
Date: Wed Aug 7 16:55:14 2019 +0100
arm64: mm: Flip kernel VA space
In order to allow for a KASAN shadow that changes size at boot time, one
must fix the KASAN_SHADOW_END for both 48 & 52-bit VAs and "grow" the
start address. Also, it is highly desirable to maintain the same
function addresses in the kernel .text between VA sizes. Both of these
requirements necessitate us to flip the kernel address space halves s.t.
the direct linear map occupies the lower addresses.
This patch puts the direct linear map in the lower addresses of the
kernel VA range and everything else in the higher ranges.
所以本文基于目前mainline的内核版本v5.9-rc2 展开叙述。
虚拟地址空间:
各体系架构处理器的虚拟地址空间的布局各不相同,下面是ARM64位处理器使用48位虚拟地址,4级页表,页面大小4KB时的layout:
Start End Size Use
-----------------------------------------------------------------------
0000000000000000 0000ffffffffffff 256TB user
ffff000000000000 ffff7fffffffffff 128TB kernel logical memory map
ffff800000000000 ffff9fffffffffff 32TB kasan shadow region
ffffa00000000000 ffffa00007ffffff 128MB bpf jit region
ffffa00008000000 ffffa0000fffffff 128MB modules
ffffa00010000000 fffffdffbffeffff ~93TB vmalloc
fffffdffbfff0000 fffffdfffe5f8fff ~998MB [guard region]
fffffdfffe5f9000 fffffdfffe9fffff 4124KB fixed mappings
fffffdfffea00000 fffffdfffebfffff 2MB [guard region]
fffffdfffec00000 fffffdffffbfffff 16MB PCI I/O space
fffffdffffc00000 fffffdffffdfffff 2MB [guard region]
fffffdffffe00000 ffffffffffdfffff 2TB vmemmap
ffffffffffe00000 ffffffffffffffff 2MB [guard region]
注:以上layout来自内核文档Documentation/arm64/memory.rst。x86的位于Documentation/x86/x86_64/mm.txt
为了直观点,画了幅图:
地址空间的定义:
内核中划分的这么多区域,且都有自己对应的地址与大小,这些地址和大小在kernel中哪里定义着呢?具体位于:arch/arm64/include/asm/memory.h。以下是从中截取的片段:
#define PAGE_OFFSET (_PAGE_OFFSET(VA_BITS))
#define KIMAGE_VADDR (MODULES_END)
#define BPF_JIT_REGION_START (KASAN_SHADOW_END)
#define BPF_JIT_REGION_SIZE (SZ_128M)
#define BPF_JIT_REGION_END (BPF_JIT_REGION_START + BPF_JIT_REGION_SIZE)
#define MODULES_END (MODULES_VADDR + MODULES_VSIZE)
.....
挑其中几个宏定义聊一下:
- PAGE_OFFSET
内核线性映射区的起始地址,大小为128TB。 - KASAN_SHADOW_START
KASAN影子内存的起始虚拟地址,大小为32TB。为什么是32TB呢?因为KASAN通常使用1:8或1:16比例的内存来做影子内存,分别对应大小为256TB/8=32TB或256TB/16=16TB,这里表示的是1:8的情况所以是32TB。 - KIMAGE_VADDR
定义了内核镜像的链接地址,通过其定义"#define KIMAGE_VADDR (MODULES_END)"看出它整好位于modules区域的结尾处,即vmalloc区域的起始地址。vmlinux.ld.S文件设置链接地址时会用到它,start_kernel->paging_init->map_kernel会将内核镜像的各个段依次映射到该区域。 - VMALLOC_START
定义了vmalloc区域的起始地址,大小约等于93TB。记得之前ARM32可以通过bootargs去控制vmalloc区域的大小,不知道64还有没。但是有没有也没所谓了,毕竟64位的处理器上虚拟地址空间已不像32位处理器那么紧张。 - VMEMMAP_START
定义了vmemmap区域的起始地址,大小2TB。sparsemem内存模型中用来存放所有struct page的虚拟地址空间。
寄存器TTBR0和TTBR1:
本文讲到了内核地址空间和用户地址空间,这就不得不提一下ARM64相关的两个寄存器TTBR0和TTBR1。它们的功能类似于X86里的CR3寄存器用来存放进程的1级页表(PGD)的基地址。但不同的是ARM64使用了两个寄存器分别存放用户空间和内核空间的1级页表基地址。
我们知道所有进程的内核地址空间的页表是共用一套的,所以TTBR1中的内容不会改变,永远等于init_mm->swapper_pg_dir。但各个进程的用户空间的页表各自独立,那么TTBR0中的内容则等于各自进程的task_struct->mm_struct->pgd
最后提一下,处理器如何知道什么时候访问TTBR0,什么时候访问TTBR1呢?ARMv8手册中有提到,当CPU访问地址时,若地址的第63bit为1则自动使用TTBR1,为0则使用TTBR0。
from: https://zhuanlan.zhihu.com/p/207001939?utm_source=wechat_timeline