Linux 内存管理(四)虚拟内存管理
页表寻址过程
这里就是 cpu 将一个虚拟地址转换为物理地址的过程,整个过程是比较繁琐耗时的,因此这个过程一般在一个专门的硬件中完成就是 MMU、MMU 中为了可以加快速度引入了 TLB cache 快速查找 pte。我们需要注意到一点就是到了 PTE 这一级需要提供的是物理地址的页起始地址,这个地址是以 page size 为单位的,对于 4k 的 page,那么 pte 的低 12bit 就是没有使用的,而这 12bit 就正好用来作为 page 的权限控制位,对于 ARM64(39bit va)其高位 [40-63] 也没有使用因此也可以用来做一些其他事情。这里来看看 arm-v8 对低 12bit 的定义:
《Architecture Reference Manual ARMv8, for ARMv8-A architecture profile》里的 D4.3.2 ARMv8 translation table level 3 descriptor formats 一节中有相关描述:
ARM-v8 支持 5 种 L3 页表格式,其中我们关心的是 Page 4K granule 这一种,对此有如下说明:
- Bit 0: Value bit,1 is valued and 0 is invaled
- Bit 1: identifies the descriptor type; 0 is Reserved, invalid and 1 is Page
4KB translation granule
Bits[47:12] are bits[47:12] of the output address for a page of memory.
16KB translation granule
Bits[47:14] are bits[47:14] of the output address for a page of memory.
64KB translation granule
Bits[47:16] are bits[47:16] of the output address for a page of memory.
Lower attributes:(Attribute fields in stage 1 VMSAv8-64 Block and Page descriptors)
UXN or XN, bit[54] (页面在用户模式下不能执行)
This bit is called UXN (Unprivileged execute never) in the EL1&0 translation regime, where it only determines whether execution at EL0 of instructions fetched from the region is permitted. In the other translation regimes the bit is called XN (Execute never).
**PXN, bit53
The Privileged execute-never bit. Determines whether the region is executable at EL1。
Contiguous, bit[52] (表示当前页表项处在一个连续物理页面集合中,可以使用一个单一的 TLB 表项进行优化)
A hint bit indicating that the translation table entry is one of a contiguous set or entries, that might be cached in a single TLB entry。
nG, bit11
The not global bit. Determines whether the TLB entry applies to all ASID values, or only to the current ASID value。
**AF, bit10
The Access flag。
SH 内存缓存共享属性
Shareability field
- 00:没有共享
- 01:保留
- 10:Outer Shareable
- 11:Inner Shareable
- AP[2:1]
- AP[1]: 表示该内存允许用户权限 (EL0) 还是更高权限的特权异常等级 (EL1) 来访问。
- 1:表示可以被 EL0 以及更高特权的异常等级访问
- 0:表示不能被 EL0 访问,但是可以被 EL1 访问
- AP[2]: 只读权限还是可读可写权限
- 1:表示只读
- 0:表示可读可写
Data Access Permissions bits
- AP[1]: 表示该内存允许用户权限 (EL0) 还是更高权限的特权异常等级 (EL1) 来访问。
NS, bit[5] (非安全比特位。当处于安全模式时用来指定访问的内存地址是安全映射的还是非安全映射的)
Non-secure bit. For memory accesses from Secure state, specifies whether the output address is in the Secure or Non-secure address map
AttrIndx[2:0], bits4:2
Stage 1 memory attributes index field
- 0:DEVICE_nGnRnE
- 1:DEVICE_nGnRE
- 2:DEVICE_GRE
- 3:NORMAL_NC
- 4:NORMAL
- 5:NORMAL_WT
在 linux 下的定义
1 | <arch/arm64/include/asm/pgtable-hwdef.h> |
整体初始化流程
idmp 映射(恒等映射,为了开启 MMU)
text:__idmap_text_start~__idmap_text_end
data:idmap_pg_dir~idmap_pg_end
一旦启动 MMU 就需要使用虚拟地址,现代处理器大多数是多级流水线,处理器会提前预取多条指令到流水线中,打开 MMU 时,这些指令都是物理地址预取的;在 MMU 开启后,将以虚拟地址访问,这样就会出错,所以引入了“恒等映射”,即在过渡阶段的代码,虚拟地址和物理地址相等。恒等映射完成后,就启动 MMU,进入虚拟地址访问阶段。恒等映射的代码在 __idmap_text_start~__idmap_text_end,可以从 System.map 文件中查询到。
1 | ffffffc081206000 T __idmap_text_start |
恒等映射目的就是为__idmap_text_start~__idmap_text_end 这段代码创建一个映射页表,使其虚拟地址和物理地址是相等的。在 vmlinux.lds.S 中,事先已经分配了 IDMAP_DIR_SIZE 的空间用于存储页表,通常页表为 3 个连续的 4KB 页面,分别对于 PGD,PUD,PMD 页表,这里没有使用 PTE,所以粒度是 2MB 的大小。
1 | init_idmap_pg_dir = .; |
粗粒度的内核映像映射(为了进入内核空间)
text: kernel_text
data:init_pg_dir~init_pg_end(定义在 arch/arm64/kernel/vmlinux.lds.S 链接文件中)
1 | init_pg_dir = .; |
之所以要创建第二个页表,是因为 cpu 刚启动时,物理内存一般都在低地址(不会超过 256TB),恒等映射的地址实际也在用户空间,即 MMU 启用后 idmap_pg_dir 会填入 TTBR0,而内核空间链接地址(虚拟地址)都是在高地址,需要填入 TTBR1,因此需要再创建一张表,映射整个内核镜像,且虚拟地址空间是在高地址 0xffff xxxx xxxx xxxxarch/arm64/kernel/head.S:
1 | adrp x1, _text |
流程
fixmap 映射
Linux 内核要访问物理内存,一旦开启 MMU 后,就只能通过虚拟地址查询页表找到物理地址进行访问,上一章节中建立恒等映射和粗粒度内核映像映射的页表,因此只能保证内核镜像正常访问。如果要解析 DTB,访问设备 I/O 等依然是无法访问的,因为查询不到对应的页表。因此内核引入了 fixmap 机制,就是事先分配一段虚拟地址空间,然后给定其虚拟地址创建好页表,页表中的表项最后一级指向的物理页帧号先不填充,等到实际要访问那段物理内存后再将其填充,然后通过 fixmap 这段虚拟地址范围就可以通过查询页表访问到物理内存。 Fixmap 最关键要实现的目的就是将一段空间的虚拟地址与物理地址对应上,linux 内核通过虚拟地址访问到物理空间,那既然是通过虚拟地址访问到物理地址,那必须构建填充这段虚拟地址到物理地址的页表,这样 Linux 内核经过 MMU 利用查找页表找到对应的物理地址进行访问。
fixmap 虚拟地址空间又被平均分成两个部分 permanent fixed addresses 和 temporary fixed addresses。permanent fixed addresses 是永久映射,temporary fixed addresses 是临时映射。永久映射是指在建立的映射关系在 kernel 阶段不会改变,仅供特定模块一直使用。临时映射就是模块使用前创建映射,使用后解除映射。
fixmap 区域又被继续细分,分配给不同模块使用。kernel 中定义枚举类型作为 index,根据 index 可以计算该模拟在 fixmap 区域的虚拟地址。
1 | enum fixed_addresses { |
Fixmap 映射流程
fixmap 初始化操作在 early_fixmap_init 函数中完成。主要是建立 PGD/PUD/PMD 页表。fixmap 映射调用栈如下:
1 | start_kernel |
early ioremap 利用 slot 管理映射,最多支持 FIX_BTMAPS_SLOTS 个映射,每个映射最大支持映射 256KB。slot_virt 数组存储每个 slot 的虚拟地址首地址。prev_map 数组用来记录已经分配出去的虚拟地址,数组值为 0 代表没有分配。prev_size 记录映射的 size。 其中 bm_pud、bm_pmd 和 bm_pte 都在内核 image 的全局数组中,也就是在。data 段中,在 idmp 映射阶段对内核做一个粗粒度的映射,在这个映射后就可以通过 mmu 访问内核数据了。这里正好利用这个建立 fixmap 映射。
1 |
|
Fixmap 虚拟地址空间视图
Fixmap 寻址流程
以上就是 Fixmap 的地址空间和寻址流程。其中比较重要的是 BITMAP 区域始于 IO 映射区域,而 FDT 属于设备树映射区域,经过 FixedMap 映射后就 kernel 就可以访问 dtb 和 io 了。
线性映射
构建 PGD 映射表
页目录直接使用的是 swapper_pg_dir,一个条目映射的空间本身就很大,一个 entry 对应范围有 512GB。
1 | arch/arm64/kernel/vmlinux.lds.S |
1 | <arch/arm64/include/asm/pgtable.h> |
fixmap 区域可以想象成一块内存以页为单位被平均分成__end_of_permanent_fixed_addresses 块。而这些枚举值就是这块内存的 index。因此虚拟地址可以根据 index 进行计算。 通过__pa_symbol 先将 swapper_pg_dir 转化为物理地址,然后通过 pgd_set_fixmap 对其进行映射,映射工作主要在__set_fixmap 里,映射完成后返回 FIX_PGD 的虚拟地址+swapper_pg_dir 物理地址的 offset 部分作为 pgdp。
有了 pgdp 之后就可以对 kernel、memblock 等进行下一步的映射了。
两个重要的宏__pa 和__va ### __pa 宏分析 1
2
3
4
5
6
7
8
9
<arch/arm64/include/asm/memory.h>
1 | <arch/arm64/include/asm/memory.h> |
__tag_reset 宏 __tag_reset(x) 是去掉虚拟地址中的 tag(如果有 tag 的话).
__is_lm_address(用于判断虚拟地址 addr 是否在 arm64 的虚拟地址空间的线性地址区域) 1
2
3
4
5
6
/*
* Check whether an arbitrary address is within the linear map, which
* lives in the [PAGE_OFFSET, PAGE_END) interval at the bottom of the
* kernel's TTBR1 address range.
*/
1 | /* |
先看 PAGE_OFFSET 定义:
1 | .config 里 |
再看 PAGE_END 定义:
1 | CONFIG_ARM64_VA_BITS=39 |
线性地址空间为 [0xFFFFFF80 00000000, 0xFFFFFFC0 00000000].
__lm_to_phys(线性地址空间内虚拟地址转物理地址) 1
1 |
这里主要是 PHYS_OFFSET 宏的定义:
1 |
memstart_addr 在 arch/arm64/mm/init.c 里设置 (DRAM 的起始地址):
1 | void __init arm64_memblock_init(void) |
这里打印出来就是 0. 到这里就是分析清楚。
__kimg_to_phys(内核 image 物理地址转虚拟地址) 1
2
3
4
/* the offset between the kernel virtual and physical mappings */
extern u64 kimage_voffset;
1 | /* the offset between the kernel virtual and physical mappings */ |
这里知道 kimage_voffset 是内核镜像的虚拟地址和物理地址之间的偏移就可以了,暂时用不到不用分析,基本上就是内核加载到内存中后在内存的物理地址与链接地址之间的偏差。
__va 宏分析 1
2
3
分析完__pa 之后看__va 就简单多了,这里只做线性区域的映射,且 PHYS_OFFSET 和 PAGE_OFFSET 都已经分析过了,这里不做过多说明了
1 |
映射过程(mem_map)
1 | map_mem(pgdp) |
其中__create_pgd_mapping 的调用流程如下:
通过__create_pgd_mapping 函数建立线性映射的页表,其中参数虚拟地址通过 __phys_to_virt(start) 来转换,而 __phys_to_virt 就是__va 宏的实现对物理地址做一个固定的偏移 PAGE_OFFSET( 0xFFFFFF80 00000000)。有了虚拟地址和物理地址后剩下的就是建立虚拟地址到物理地址的映射就可以了,就是填充 pud、pmd、pte 页表。
线性映射地址空间
整个 ddr 空间中除了 secmon、kernel image 外其他都是线性映射范围。
VMALLOC 映射
vmalloc() 函数
- vmalloc 用于分配虚拟地址连续(物理地址不连续)的内存空间,vzmalloc 相对于 vmalloc 多了个 0 初始化
- vmalloc/vzmalloc 分配的虚拟地址范围在 VMALLOC_START/VMALLOC_END 之间
- linux 管理 vmalloc 分别有两个数据结构:vm_struct, vm_area_struct;前者是内核虚拟地址空间的映射,后者是应用进程虚拟地址空间映射
- 内核 vmalloc 区具体地址空间的管理是通过 vmap_area 管理的,该结构体记录整个区间的起始和结束
- vmalloc 在申请内存时逐页分配,确保在物理内存有严重碎片的情况下,vmalloc 仍然可以工作
- vmalloc () 函数的工作方式与 kmalloc() 类似,只不过它分配的内存只是虚拟连续的,而不一定物理连续。用户空间分配函数的工作方式如下:malloc() 返回的页面在处理器的虚拟地址空间中是连续的,但不能保证它们在物理 RAM 中实际上是连续的。kmalloc () 函数保证页面在物理上是连续的(并且虚拟连续)。vmalloc () 函数仅确保页面在虚拟地址空间中是连续的。它通过分配可能不连续的物理内存块并“修复”页表以将内存映射到逻辑地址空间的连续块来实现这一点。
数据组织形式
初始化流程如下:
vmap() 和 vmalloc()
vmalloc 流程与 vmap 差不多,只是 page 结构体在 vmap 中是通过参数传递过去的,而 vmalloc 是通过 page_alloc 巷 buddy system 申请的。
VMEMMAP 映射
vmemmap 是内核中 page 数据的虚拟地址,针对 sparse 内存模型。内核申请 page 获取的 page 地址从此开始。vmemmap 区域是一块起始地址是 VMEMMAP_START,范围是 2TB 的虚拟地址区域,位于 kernel space。以 section 为单位来存放 strcut page 结构的虚拟地址空间,然后线性映射到物理内存。详细过程可参考如下连接:
附录一:宏定义快查
config 配置
1 | CONFIG_PGTABLE_LEVELS=3 |
PAGE 定义
1 | <arch/arm64/include/asm/page-def.h> |
PAGE 占 12bit,一页大小是 2^12=4096 = 4k
PTE 定义:
1 |
PTRS_PER_PTE = 1 << (12 - 3) = 512
PMD 定义
因为 CONFIG_PGTABLE_LEVELS 定义为 3 所以有如下定义:
1 | /* |
- PMD_SHIFT = (PAGE_SHIFT - 3) * (4 - 2) + 3 = 21
- PTRS_PER_PMD = 1 << (12 - 3) = 512
PUD 定义
只有 4 级页表才有 PUD,三级页表没有 PUD:
1 | /* |
- PUD_SHIFT = (PAGE_SHIFT - 3) * (4 - 1) + 3 = 30
- PTRS_PER_PUD = 1 << (12 - 3) = 512
PGD 定义
1 | /* |
- PGDIR_SHIFT = (PAGE_SHIFT - 3) * (4 - 1) + 3 = 30
- PTRS_PER_PGD = 1 << (39 - 30) = 512
附录二:相关数据类型定义
表项数据类型定义
1 | <arch/arm64/include/asm/pgtable-types.h> |
获取表项索引值
1 | <include/linux/pgtable.h> |
获取表项地址
1 | static inline pgd_t *pgd_offset_pgd(pgd_t *pgd, unsigned long address) |
通过 pgd_offset_pgd 就可以找到一个虚拟内存地址在 PGD 中的页目录项了。
说明:这里的 PGD、PUD、PMD 为什么都是使用 9bit,这个是因为在 64bit 系统下,每个地址要占 8 字节,而对于 page size=4k 大小的页面,一个页面所能容纳的地址数量正好是 4096 / 8 = 512,而要寻址这 512 个索引需要 2^9=512 也就是需要 9bit。这里也是使用 9bit 的原因,如果用更大的 page size 则更合理的做法是相应的增大 PGD、PUD、PMD 占的 bit 数,这样可以更充分的利用空间。
参考文献
《奔跑吧 Linux 内核》