页表寻址过程
这里就是 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]: 只读权限还是可读可写权限
Data Access Permissions bits
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 <arch/arm64/include/asm /pgtable-hwdef.h> #define PTE_VALID (_AT(pteval_t, 1) << 0) #define PTE_TYPE_MASK (_AT(pteval_t, 3) << 0) #define PTE_TYPE_PAGE (_AT(pteval_t, 3) << 0) #define PTE_TABLE_BIT (_AT(pteval_t, 1) << 1) #define PTE_USER (_AT(pteval_t, 1) << 6) #define PTE_RDONLY (_AT(pteval_t, 1) << 7) #define PTE_SHARED (_AT(pteval_t, 3) << 8) #define PTE_AF (_AT(pteval_t, 1) << 10) #define PTE_NG (_AT(pteval_t, 1) << 11) #define PTE_GP (_AT(pteval_t, 1) << 50) #define PTE_DBM (_AT(pteval_t, 1) << 51) #define PTE_CONT (_AT(pteval_t, 1) << 52) #define PTE_PXN (_AT(pteval_t, 1) << 53) #define PTE_UXN (_AT(pteval_t, 1) << 54)
整体初始化流程
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 ffffffc081206000 T __idmap_text_start ffffffc081206000 t enter_vhe ffffffc081206038 T cpu_resume ffffffc081206068 T primary_entry ffffffc0812060a8 T init_kernel_el ffffffc0812060b8 t init_el1 ffffffc0812060e0 t init_el2 ffffffc0812062f0 T secondary_holding_pen ffffffc081206318 t pen ffffffc081206330 T secondary_entry ffffffc081206340 t secondary_startup ffffffc081206368 T __enable_mmu ffffffc0812063b0 T __cpu_secondary_check52bitva ffffffc0812063b8 t __no_granule_support ffffffc0812063e0 t __relocate_kernel ffffffc081206430 t __primary_switch ffffffc0812064a0 T idmap_cpu_replace_ttbr1 ffffffc0812064d0 T idmap_kpti_install_ng_mappings ffffffc08120667c t __idmap_kpti_secondary ffffffc0812066c8 T __cpu_setup ffffffc0812067bc T __idmap_text_end
恒等映射目的就是为__idmap_text_start~__idmap_text_end
这段代码创建一个映射页表,使其虚拟地址和物理地址是相等的。在
vmlinux.lds.S 中,事先已经分配了 IDMAP_DIR_SIZE
的空间用于存储页表,通常页表为 3 个连续的 4KB 页面,分别对于 PGD,PUD,PMD
页表,这里没有使用 PTE,所以粒度是 2MB 的大小。
1 2 3 init_idmap_pg_dir = .; . += INIT_IDMAP_DIR_SIZE; init_idmap_pg_end = .;
粗粒度的内核映像映射(为了进入内核空间)
text: kernel_text
data:init_pg_dir~init_pg_end(定义在 arch/arm64/kernel/vmlinux.lds.S
链接文件中)
1 2 3 4 init_pg_dir = .; . += INIT_DIR_SIZE; init_pg_end = .; /* end of zero-init region */
之所以要创建第二个页表,是因为 cpu
刚启动时,物理内存一般都在低地址(不会超过
256TB),恒等映射的地址实际也在用户空间,即 MMU 启用后 idmap_pg_dir
会填入 TTBR0,而内核空间链接地址(虚拟地址)都是在高地址,需要填入
TTBR1,因此需要再创建一张表,映射整个内核镜像,且虚拟地址空间是在高地址
0xffff xxxx xxxx xxxxarch/arm64/kernel/head.S:
1 2 3 4 5 6 7 adrp x1, _text adrp x2, init_pg_dir adrp x3, init_pg_end bic x4, x2, #SWAPPER_BLOCK_SIZE - 1 mov_q x5, SWAPPER_RW_MMUFLAGS mov x6, #SWAPPER_BLOCK_SHIFT bl remap_region
流程
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 enum fixed_addresses { FIX_HOLE, FIX_FDT_END, FIX_FDT = FIX_FDT_END + DIV_ROUND_UP(MAX_FDT_SIZE, PAGE_SIZE) + 1 , FIX_EARLYCON_MEM_BASE, FIX_TEXT_POKE0, #ifdef CONFIG_ACPI_APEI_GHES FIX_APEI_GHES_IRQ, FIX_APEI_GHES_SEA, #ifdef CONFIG_ARM_SDE_INTERFACE FIX_APEI_GHES_SDEI_NORMAL, FIX_APEI_GHES_SDEI_CRITICAL, #endif #endif #ifdef CONFIG_UNMAP_KERNEL_AT_EL0 #ifdef CONFIG_RELOCATABLE FIX_ENTRY_TRAMP_TEXT4, #endif FIX_ENTRY_TRAMP_TEXT3, FIX_ENTRY_TRAMP_TEXT2, FIX_ENTRY_TRAMP_TEXT1, #define TRAMP_VALIAS (__fix_to_virt(FIX_ENTRY_TRAMP_TEXT1)) #endif __end_of_permanent_fixed_addresses, #define NR_FIX_BTMAPS (SZ_256K / PAGE_SIZE) #define FIX_BTMAPS_SLOTS 7 #define TOTAL_FIX_BTMAPS (NR_FIX_BTMAPS * FIX_BTMAPS_SLOTS) FIX_BTMAP_END = __end_of_permanent_fixed_addresses, FIX_BTMAP_BEGIN = FIX_BTMAP_END + TOTAL_FIX_BTMAPS - 1 , FIX_PTE, FIX_PMD, FIX_PUD, FIX_P4D, FIX_PGD, __end_of_fixed_addresses };
Fixmap 映射流程
fixmap 初始化操作在 early_fixmap_init 函数中完成。主要是建立
PGD/PUD/PMD 页表。fixmap 映射调用栈如下:
1 2 3 4 5 6 7 8 9 start_kernel -->setup_arch -->early_fixmap_init -->early_fixmap_init_pud -->early_fixmap_init_pmd -->early_fixmap_init_pte -->early_ioremap_init -->early_ioremap_setup -->slot_virt[i] = __fix_to_virt(FIX_BTMAP_BEGIN - NR_FIX_BTMAPS*i)
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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 #define pgd_index(a) (((a) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1)) static inline pgd_t *pgd_offset_pgd (pgd_t *pgd, unsigned long address) { return (pgd + pgd_index(address)); }; #define pgd_offset(mm, address) pgd_offset_pgd((mm)->pgd, (address)) struct mm_struct init_mm = { .mm_mt = MTREE_INIT_EXT(mm_mt, MM_MT_FLAGS, init_mm.mmap_lock), .pgd = swapper_pg_dir, .mm_users = ATOMIC_INIT(2 ), .mm_count = ATOMIC_INIT(1 ), .write_protect_seq = SEQCNT_ZERO(init_mm.write_protect_seq), MMAP_LOCK_INITIALIZER(init_mm) .page_table_lock = __SPIN_LOCK_UNLOCKED(init_mm.page_table_lock), .arg_lock = __SPIN_LOCK_UNLOCKED(init_mm.arg_lock), .mmlist = LIST_HEAD_INIT(init_mm.mmlist), #ifdef CONFIG_PER_VMA_LOCK .mm_lock_seq = 0 , #endif .user_ns = &init_user_ns, .cpu_bitmap = CPU_BITS_NONE, #ifdef CONFIG_IOMMU_SVA .pasid = IOMMU_PASID_INVALID, #endif INIT_MM_CONTEXT(init_mm) }; #define pgd_offset_k(address) pgd_offset(&init_mm, (address))
Fixmap 虚拟地址空间视图
Fixmap 寻址流程
以上就是 Fixmap 的地址空间和寻址流程。其中比较重要的是 BITMAP 区域始于
IO 映射区域,而 FDT 属于设备树映射区域,经过 FixedMap 映射后就 kernel
就可以访问 dtb 和 io 了。
线性映射
构建 PGD 映射表
页目录直接使用的是 swapper_pg_dir,一个条目映射的空间本身就很大,一个
entry 对应范围有 512GB。
1 2 3 4 5 arch/arm64/kernel/vmlinux.lds.S swapper_pg_dir = .; . += PAGE_SIZE; swapper_pg_dir 是实现分配的一段空间,处于内核镜像的 data 段。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <arch/arm64/include/asm /pgtable.h> #define __fix_to_virt(x) (FIXADDR_TOP - ((x) << PAGE_SHIFT)) #define __virt_to_fix(x) ((FIXADDR_TOP - ((x)&PAGE_MASK)) >> PAGE_SHIFT) static __always_inline unsigned long fix_to_virt (const unsigned int idx) { BUILD_BUG_ON(idx >= __end_of_fixed_addresses); return __fix_to_virt(idx); #define __set_fixmap_offset(idx, phys, flags) \ ({ \ unsigned long ________addr; \ __set_fixmap(idx, phys, flags); \ ________addr = fix_to_virt(idx) + ((phys) & (PAGE_SIZE - 1)); \ ________addr; \ }) #define set_fixmap_offset(idx, phys) \ __set_fixmap_offset(idx, phys, FIXMAP_PAGE_NORMAL) #define pgd_set_fixmap(addr) ((pgd_t *)set_fixmap_offset(FIX_PGD, addr)) pgd_t *pgdp = pgd_set_fixmap(__pa_symbol(swapper_pg_dir));
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> #define __virt_to_phys_nodebug(x) ({ \ phys_addr_t __x = (phys_addr_t)(__tag_reset(x)); \ __is_lm_address(__x) ? __lm_to_phys(__x) : __kimg_to_phys(__x); \ }) #define __virt_to_phys(x) __virt_to_phys_nodebug(x) #define __pa(x) __virt_to_phys((unsigned long)(x))
__tag_reset 宏
__tag_reset(x) 是去掉虚拟地址中的 tag(如果有 tag 的话).
__is_lm_address(用于判断虚拟地址
addr 是否在 arm64 的虚拟地址空间的线性地址区域)
1 2 3 4 5 6 #define __is_lm_address(addr) (((u64)(addr) - PAGE_OFFSET) < (PAGE_END - PAGE_OFFSET)
先看 PAGE_OFFSET 定义:
1 2 3 4 5 6 7 8 9 .config 里 CONFIG_ARM64_VA_BITS=39 #define VA_BITS (CONFIG_ARM64_VA_BITS) #define _PAGE_OFFSET(va) (-(UL(1) << (va))) #define PAGE_OFFSET (_PAGE_OFFSET(VA_BITS)) PAGE_OFFSET = (UL(-1 ) << 39 )
再看 PAGE_END 定义:
1 2 3 4 5 6 7 8 9 CONFIG_ARM64_VA_BITS=39 #define VA_BITS (CONFIG_ARM64_VA_BITS) #define VA_BITS_MIN (VA_BITS) #define _PAGE_END(va) (-(UL(1) << ((va) - 1))) #define PAGE_END (_PAGE_END(VA_BITS_MIN)) PAGE_END = (UL(-1 ) << (39 - 1 ))
线性地址空间为 [0xFFFFFF80 00000000, 0xFFFFFFC0 00000000].
__lm_to_phys(线性地址空间内虚拟地址转物理地址)
1 #define __lm_to_phys(addr) (((addr) - PAGE_OFFSET) + PHYS_OFFSET)
这里主要是 PHYS_OFFSET 宏的定义:
1 #define PHYS_OFFSET ({ VM_BUG_ON(memstart_addr & 1); memstart_addr; })
memstart_addr 在 arch/arm64/mm/init.c 里设置 (DRAM 的起始地址):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 void __init arm64_memblock_init (void ) { ... memstart_addr = round_down(memblock_start_of_DRAM(), ARM64_MEMSTART_ALIGN); ... if (memstart_addr + linear_region_size < memblock_end_of_DRAM()) { memstart_addr = round_up(memblock_end_of_DRAM() - linear_region_size, ARM64_MEMSTART_ALIGN); memblock_remove(0 , memstart_addr); } ... }
这里打印出来就是 0. 到这里就是分析清楚。
__kimg_to_phys(内核
image 物理地址转虚拟地址)
1 2 3 4 extern u64 kimage_voffset;#define __kimg_to_phys(addr) ((addr) - kimage_voffset)
这里知道 kimage_voffset
是内核镜像的虚拟地址和物理地址之间的偏移就可以了,暂时用不到不用分析,基本上就是内核加载到内存中后在内存的物理地址与链接地址之间的偏差。
__va 宏分析
1 2 3 #define __phys_to_virt(x) ((unsigned long)((x) - PHYS_OFFSET) | PAGE_OFFSET) #define __va(x) ((void *)__phys_to_virt((phys_addr_t)(x)))
分析完__pa 之后看__va 就简单多了,这里只做线性区域的映射,且
PHYS_OFFSET 和 PAGE_OFFSET 都已经分析过了,这里不做过多说明了
映射过程(mem_map)
1 2 3 4 5 map_mem(pgdp) -->__map_memblock(pgdp, start, __phys_to_virt(start), end - start, prot, early_pgtable_alloc, flags) -->__create_pgd_mapping(pgdp, start, __phys_to_virt(start), end - start, prot, early_pgtable_alloc, flags)
其中__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 2 3 4 5 6 7 8 9 10 11 CONFIG_PGTABLE_LEVELS=3 CONFIG_ARM64_VA_BITS_39=y CONFIG_ARM64_VA_BITS=39 CONFIG_ARM64_PA_BITS_48=y CONFIG_ARM64_PA_BITS=48 CONFIG_ARM64_4K_PAGES=y CONFIG_HZ_250=y CONFIG_HZ=250 CONFIG_ARM64_PAGE_SHIFT=12 CONFIG_ARM64_CONT_PTE_SHIFT=4 CONFIG_ARM64_CONT_PMD_SHIFT=4
PAGE 定义
1 2 3 4 5 <arch/arm64/include/asm /page-def.h> #define PAGE_SHIFT CONFIG_ARM64_PAGE_SHIFT #define PAGE_SIZE (_AC(1, UL) << PAGE_SHIFT) #define PAGE_MASK (~(PAGE_SIZE-1))
PAGE 占 12bit,一页大小是 2^12=4096 = 4k
PTE 定义:
1 #define PTRS_PER_PTE (1 << (PAGE_SHIFT - 3))
PTRS_PER_PTE = 1 << (12 - 3) = 512
PMD 定义
因为 CONFIG_PGTABLE_LEVELS 定义为 3 所以有如下定义:
1 2 3 4 5 6 7 8 9 #if CONFIG_PGTABLE_LEVELS > 2 #define PMD_SHIFT ARM64_HW_PGTABLE_LEVEL_SHIFT(2) #define PMD_SIZE (_AC(1, UL) << PMD_SHIFT) #define PMD_MASK (~(PMD_SIZE-1)) #define PTRS_PER_PMD (1 << (PAGE_SHIFT - 3)) #endif
PMD_SHIFT = (PAGE_SHIFT - 3) * (4 - 2) + 3 = 21
PTRS_PER_PMD = 1 << (12 - 3) = 512
PUD 定义
只有 4 级页表才有 PUD,三级页表没有 PUD:
1 2 3 4 5 6 7 8 9 #if CONFIG_PGTABLE_LEVELS > 3 #define PUD_SHIFT ARM64_HW_PGTABLE_LEVEL_SHIFT(1) #define PUD_SIZE (_AC(1, UL) << PUD_SHIFT) #define PUD_MASK (~(PUD_SIZE-1)) #define PTRS_PER_PUD (1 << (PAGE_SHIFT - 3)) #endif
PUD_SHIFT = (PAGE_SHIFT - 3) * (4 - 1) + 3 = 30
PTRS_PER_PUD = 1 << (12 - 3) = 512
PGD 定义
1 2 3 4 5 6 7 8 #define PGDIR_SHIFT ARM64_HW_PGTABLE_LEVEL_SHIFT(4 - CONFIG_PGTABLE_LEVELS) #define PGDIR_SIZE (_AC(1, UL) << PGDIR_SHIFT) #define PGDIR_MASK (~(PGDIR_SIZE-1)) #define PTRS_PER_PGD (1 << (VA_BITS - PGDIR_SHIFT))
PGDIR_SHIFT = (PAGE_SHIFT - 3) * (4 - 1) + 3 = 30
PTRS_PER_PGD = 1 << (39 - 30) = 512
附录二:相关数据类型定义
表项数据类型定义
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 <arch/arm64/include/asm /pgtable-types.h> typedef u64 pteval_t ;typedef u64 pmdval_t ;typedef u64 pudval_t ;typedef u64 p4dval_t ;typedef u64 pgdval_t ;typedef struct { pteval_t pte; } pte_t ;#define pte_val(x) ((x).pte) #define __pte(x) ((pte_t) { (x) } ) #if CONFIG_PGTABLE_LEVELS > 2 typedef struct { pmdval_t pmd; } pmd_t ;#define pmd_val(x) ((x).pmd) #define __pmd(x) ((pmd_t) { (x) } ) #endif #if CONFIG_PGTABLE_LEVELS > 3 typedef struct { pudval_t pud; } pud_t ;#define pud_val(x) ((x).pud) #define __pud(x) ((pud_t) { (x) } ) #endif typedef struct { pgdval_t pgd; } pgd_t ;#define pgd_val(x) ((x).pgd) #define __pgd(x) ((pgd_t) { (x) } ) typedef struct { pteval_t pgprot; } pgprot_t ;#define pgprot_val(x) ((x).pgprot) #define __pgprot(x) ((pgprot_t) { (x) } )
获取表项索引值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 <include/linux/pgtable.h> #define pgd_index(a) (((a) >> PGDIR_SHIFT) & (PTRS_PER_PGD - 1)) #ifndef pud_index static inline unsigned long pud_index (unsigned long address) { return (address >> PUD_SHIFT) & (PTRS_PER_PUD - 1 ); } #define pud_index pud_index #endif #ifndef pmd_index static inline unsigned long pmd_index (unsigned long address) { return (address >> PMD_SHIFT) & (PTRS_PER_PMD - 1 ); } #define pmd_index pmd_index #endif static inline unsigned long pte_index (unsigned long address) { return (address >> PAGE_SHIFT) & (PTRS_PER_PTE - 1 ); }
获取表项地址
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 static inline pgd_t *pgd_offset_pgd (pgd_t *pgd, unsigned long address) { return (pgd + pgd_index(address)); }; #ifndef pgd_offset #define pgd_offset(mm, address) pgd_offset_pgd((mm)->pgd, (address)) #endif #ifndef pmd_offset static inline pmd_t *pmd_offset (pud_t *pud, unsigned long address) { return pud_pgtable(*pud) + pmd_index(address); } #define pmd_offset pmd_offset #endif #ifndef pud_offset static inline pud_t *pud_offset (p4d_t *p4d, unsigned long address) { return p4d_pgtable(*p4d) + pud_index(address); } #define pud_offset pud_offset #endif
通过 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 内核》