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

  • 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>
/*
* Level 3 descriptor (PTE).
*/
#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) /* AP[1] */
#define PTE_RDONLY (_AT(pteval_t, 1) << 7) /* AP[2] */
#define PTE_SHARED (_AT(pteval_t, 3) << 8) /* SH[1:0], inner shareable */
#define PTE_AF (_AT(pteval_t, 1) << 10) /* Access Flag */
#define PTE_NG (_AT(pteval_t, 1) << 11) /* nG */
#define PTE_GP (_AT(pteval_t, 1) << 50) /* BTI guarded */
#define PTE_DBM (_AT(pteval_t, 1) << 51) /* Dirty Bit Management */
#define PTE_CONT (_AT(pteval_t, 1) << 52) /* Contiguous range */
#define PTE_PXN (_AT(pteval_t, 1) << 53) /* Privileged XN */
#define PTE_UXN (_AT(pteval_t, 1) << 54) /* User XN */

整体初始化流程

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,

/*
* Reserve a virtual window for the FDT that is a page bigger than the
* maximum supported size. The additional space ensures that any FDT
* that does not exceed MAX_FDT_SIZE can be mapped regardless of
* whether it crosses any page boundary.
*/
FIX_FDT_END,
FIX_FDT = FIX_FDT_END + DIV_ROUND_UP(MAX_FDT_SIZE, PAGE_SIZE) + 1, // 映射设备树使用。在 ARM64 架构,大小是 4M

FIX_EARLYCON_MEM_BASE, // console 使用,大小 1 页。1 页虚拟地址空间完全够了
FIX_TEXT_POKE0,

#ifdef CONFIG_ACPI_APEI_GHES
/* Used for GHES mapping from assorted contexts */
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 /* CONFIG_ACPI_APEI_GHES */

#ifdef CONFIG_UNMAP_KERNEL_AT_EL0
#ifdef CONFIG_RELOCATABLE
FIX_ENTRY_TRAMP_TEXT4, /* one extra slot for the data page */
#endif
FIX_ENTRY_TRAMP_TEXT3,
FIX_ENTRY_TRAMP_TEXT2,
FIX_ENTRY_TRAMP_TEXT1,
#define TRAMP_VALIAS (__fix_to_virt(FIX_ENTRY_TRAMP_TEXT1))
#endif /* CONFIG_UNMAP_KERNEL_AT_EL0 */
__end_of_permanent_fixed_addresses,

/*
* Temporary boot-time mappings, used by early_ioremap(),
* before ioremap() is functional.
*/
#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, // early_ioremap() 接口使用,这部分属于动态映射。大小是 7 × 256KB

/*
* Used for kernel page table creation, so unmapped memory may be used
* for tables.
*/
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 // 页表存在 bm_pud
-->early_fixmap_init_pmd // 页表存在 bm_pmd
-->early_fixmap_init_pte // 页表存在 bm_pte
-->early_ioremap_init // kernel 启动早期不能使用 ioremap 操作,我们必须借助 early_ioremap 接口
-->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)) // PGDIR_SHIFT = 30  PTRS_PER_PGD = 512

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);

/* Return a pointer with offset calculated */
#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
/*
* 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.
*/
#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) // 0xFFFFFF80 00000000

再看 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)) // 0xFFFFFFC0 00000000

线性地址空间为 [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)
{
...
/*
* Select a suitable value for the base of physical memory.
*/
memstart_addr = round_down(memblock_start_of_DRAM(),
ARM64_MEMSTART_ALIGN);
...
/* 如果线性地址空间小于实际物理地址需要调整 memstart_addr */
if (memstart_addr + linear_region_size < memblock_end_of_DRAM()) {
/* ensure that memstart_addr remains sufficiently aligned */
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
/* the offset between the kernel virtual and physical mappings */
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>
/* PAGE_SHIFT determines the page size */
#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
/*
* PMD_SHIFT determines the size a level 2 page table entry can map.
*/
#if CONFIG_PGTABLE_LEVELS > 2
#define PMD_SHIFT ARM64_HW_PGTABLE_LEVEL_SHIFT(2) //21
#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
/*
* PUD_SHIFT determines the size a level 1 page table entry can map.
*/
#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
/*
* PGDIR_SHIFT determines the size a top-level page table entry can map
* (depending on the configuration, this level can be 0, 1 or 2).
*/
#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;

/*
* These are used to make use of C type-checking..
*/
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));
};

/*
* a shortcut to get a pgd_t in a given mm
*/
#ifndef pgd_offset
#define pgd_offset(mm, address) pgd_offset_pgd((mm)->pgd, (address))
#endif

/* Find an entry in the second-level page table.. */
#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 内核》