从零到负一

【LM03】FIXMAP和相关页表的创建

2023/01/15

接上篇笔记,在上一篇笔记ARMv8 MMU和Linux的启动中,我们完成了最初的几个页表的创建,接下来就要开始start_kernel的工作了。在进入本节主题之前,我们先来看看实验平台的虚拟内存空间。

虚拟内存空间

从上图我们可以看出几点,

  1. Linux内核的虚拟内存空间主要分为modules, vmalloc, fixed, PCI I/O, vmemmapmemory
  2. Linux内核的虚拟地址位于vmalloc内部;
  3. fixed区域已经不在vmalloc中,且其虚拟地址和内核的虚拟地址相距特别远;
  4. memory位于0xFFFF_0000_0000_00000xFFFF_FFFF_FFFF_FFFF的中间;
  5. PAGE_OFFSET的地址就是memory的地址。

除了上面几点,还有几点点需要注意,

  1. memory开始,VA->PA的映射都是线性的(Linear Mapping Region),其转换公式为 - PA = VA - PAGE_OFFSET + PHYS_OFFSET;用同样的方法,我们也可以计算VA
  2. 在之前的版本,内核的虚拟地址空间是位于memory中的。但后来为了让内核可以映射到任意的物理内存空间,内核的虚拟地址就放到了vmalloc中;
  3. 内核映射到物理内存空间也是线性的,但不是用1中的变换公式,而是用#define __kimg_to_phys(addr) ((addr) - kimage_voffset)kimage_voffset这个偏移是用MMU开启后代码的虚拟地址减去其物理地址得到的,具体请参考ARM64 Kernel Image Mapping的变化

初看FIXMAP

到目前为止,内核还不知道硬件有多少内存空间(也就是不知道上面的memory区域多大),也不知道其它硬件的信息。Linux内核通过DTS文件来识别硬件并获取相关的信息,其中就包括内存的大小。
下面这个例子来自LoyenWang的博客

1
2
3
4
5
// arch/arm64/boot/dts/freescale/fsl-ls208xa.dtsi
memory@80000000 {
device_type = "memory";
reg = <0x00000000 0x80000000 0 0x80000000>; /* DRAM space - 1, size : 2 GB DRAM */
};

有了这些信息,内核就知道内存的大小并且可以开始管理内存了。在进入FIXMAP之前,我们再来回顾下目前已有页表的情况。到目前为止,我们已经进行了内核的映射,也就是上面.text.bss区域的映射。同时,在head.S中,Linux为了节省空间,只用了3个物理页来创建这些页表,因此它能映射一个PGDentry(因为只有一个PUD)。而根据上图中的虚拟内存空间可知,fixed区域不可能和内核共用一个PGDentry,因此,我们还需要创建额外的页表。

回到FIXMAP上,要想获取DTS的信息,我们就需要将fixedDTS的物理地址进行映射,而FIXMAP就是用来完成这个映射的。

我们先来看看Linux内核是在哪里对FIXMAP进行初始化的 - start_kernel() -> setup_arch() -> early_fixmap_init()。接下来,我们来看看FIXMAP到底长什么样。在物理内存空间中,FIXMAP按照下面这个枚举的顺序排列的,FIX_HOLE在最上面,FIX_PGD在最下面,它的最小单位是PAGA_SIZE。从下面注释中可以看出,除了FDT,其它部分基本都只占一个或者几个页。要访问某个部分也很简单,直接用FIXADDR_TOP - idx * PAGE_SIZE即可,类似于数组的访问方式。

在下面的代码中我将fixed_addresses中的每部分都翻译成了数字,这很有助于理解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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
// ./arch/arm64/include/asm/fixmap.h

/*
* Here we define all the compile-time 'special' virtual
* addresses. The point is to have a constant address at
* compile time, but to set the physical address only
* in the boot process.
*
* These 'compile-time allocated' memory buffers are
* page-sized. Use set_fixmap(idx,phys) to associate
* physical memory with fixmap indices.
*
*/

/*
FIX_HOLE = 0
FIX_FDT_END = 1
FIX_FDT = 4MB / 4KB - 1 = 1023
FIX_EARLYCON_MEM_BASE = 1024
FIX_TEXT_POKE0 = 1025
FIX_APEI_GHES_IRQ = 1026
FIX_APEI_GHES_NMI = 1027
FIX_ENTRY_TRAMP_DATA = 1028
FIX_ENTRY_TRAMP_TEXT = 1029
__end_of_permanent_fixed_addresses = 1030 <----------------- 1030 * 4KB,下面的一些映射都是临时的,
使用后一般都会被撤销掉
FIX_BTMAP_END = 1030
FIX_BTMAP_BEGIN = 1030 + TOTAL_FIX_BTMAPS - 1
FIX_PTE = FIX_BTMAP_BEGIN + 1
FIX_PMD = FIX_PTE + 1
FIX_PUD = FIX_PMD + 1
FIX_PGD = FIX_PUD + 1
*/
enum fixed_addresses {
FIX_HOLE,

/*
* Reserve a virtual window for the FDT that is 2 MB larger than the
* maximum supported size, and put it at the top of the fixmap region.
* The additional space ensures that any FDT that does not exceed
* MAX_FDT_SIZE can be mapped regardless of whether it crosses any
* 2 MB alignment boundaries.
*
* Keep this at the top so it remains 2 MB aligned.
*/
#define FIX_FDT_SIZE (MAX_FDT_SIZE + SZ_2M)
FIX_FDT_END,
FIX_FDT = FIX_FDT_END + FIX_FDT_SIZE / PAGE_SIZE - 1,

FIX_EARLYCON_MEM_BASE,
FIX_TEXT_POKE0,

#ifdef CONFIG_ACPI_APEI_GHES
/* Used for GHES mapping from assorted contexts */
FIX_APEI_GHES_IRQ,
FIX_APEI_GHES_NMI,
#endif /* CONFIG_ACPI_APEI_GHES */

#ifdef CONFIG_UNMAP_KERNEL_AT_EL0
FIX_ENTRY_TRAMP_DATA,
FIX_ENTRY_TRAMP_TEXT,
#define TRAMP_VALIAS (__fix_to_virt(FIX_ENTRY_TRAMP_TEXT))
#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,

/*
* Used for kernel page table creation, so unmapped memory may be used
* for tables.
*/
FIX_PTE,
FIX_PMD,
FIX_PUD,
FIX_PGD,

__end_of_fixed_addresses
};

#define FIXADDR_SIZE (__end_of_permanent_fixed_addresses << PAGE_SHIFT)
#define FIXADDR_START (FIXADDR_TOP - FIXADDR_SIZE)

FDT映射的建立

FDT也就是Flattened Device Tree,它存放着我们需要的内存以及其它硬件的信息。通过它的初始化,我们可以大致明白 FIXMAP以及其映射是如何实现工作的。

我们先回到setup_arch()来看看这里有几个相关函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// ./arch/arm64/kernel/setup.c

void __init setup_arch(char **cmdline_p)
{
init_mm.start_code = (unsigned long) _text;
init_mm.end_code = (unsigned long) _etext;
init_mm.end_data = (unsigned long) _edata;
init_mm.brk = (unsigned long) _end;

*cmdline_p = boot_command_line;

early_fixmap_init();
early_ioremap_init();

setup_machine_fdt(__fdt_pointer);

parse_early_param();

其中我这里要讨论的是early_fixmap_init()setup_machine_fdt(),其它两个函数因为和内存管理没有直接关系,这里就略去了。

early_fixmap_init()

前面我已经提到对于FIXMAP的映射,我们不能用现有的页表,而需要额外的页表。这个额外的页表就是静态分配的bm_pxx,其定义如下。

1
2
3
4
5
6
// ./arch/arm64/mm/mmu.c

// 这三个页表都有512个entry
static pte_t bm_pte[PTRS_PER_PTE] __page_aligned_bss;
static pmd_t bm_pmd[PTRS_PER_PMD] __page_aligned_bss __maybe_unused;
static pud_t bm_pud[PTRS_PER_PUD] __page_aligned_bss __maybe_unused;

通过这个函数,我们将建立FIXADDR_START的映射,我们来看看它是如何实现的。

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
// ./arch/arm64/mm/mmu.c

/*
* The p*d_populate functions call virt_to_phys implicitly so they can't be used
* directly on kernel symbols (bm_p*d). This function is called too early to use
* lm_alias so __p*d_populate functions must be used to populate with the
* physical address from __pa_symbol.
*/
void __init early_fixmap_init(void)
{
pgd_t *pgdp, pgd;
pud_t *pudp;
pmd_t *pmdp;
// ----------------------------------------------------------------------------------------------------- (1)
// #define VMEMMAP_START (PAGE_OFFSET - VMEMMAP_SIZE)
// #define PCI_IO_END (VMEMMAP_START - SZ_2M)
// #define PCI_IO_START (PCI_IO_END - PCI_IO_SIZE)
// #define FIXADDR_TOP (PCI_IO_START - SZ_2M)
//
// hi_addr
// | - PCI_IO_END
// | |
// | |
// | |
// | |- PCI_IO_START
// | ||
// | SZ_2M||
// | ||
// | |- FIXADDR_TOP -|
// | | ||
// | | ||
// | | ||
// | | ||
// | | ||4MB < FIXADDR_SIZE < 6MB
// | | ||
// | | ||
// | | ||
// | | ||
// \\|/ - FIXADDR_START -|
// lo_addr |
// | ...
// |
// - FIX_PTE
// |
// - FIX_PMD
// |
// - FIX_PUD
// |
// - FIX_PGD
// |
// | ...
//
// #define FIXADDR_SIZE (__end_of_permanent_fixed_addresses << PAGE_SHIFT)
// #define FIXADDR_START (FIXADDR_TOP - FIXADDR_SIZE)
//
// FIXADDR_START and FIX_PGD should be in the same PMD, otherwise we cannot use
// one PTE(bm_pte) to map them
//
unsigned long addr = FIXADDR_START; // 0XFFFF7DFFFE7F9000

// ----------------------------------------------------------------------------------------------------- (2)
// get PGD entry virtual address
pgdp = pgd_offset_k(addr); // 0XFFFF00001223C7D8
pgd = READ_ONCE(*pgdp); // pgd.pgd = 0X421AA003

// ----------------------------------------------------------------------------------------------------- (3)
// if this entry is not NULL, just use it --> this is passed in live run
if (CONFIG_PGTABLE_LEVELS > 3 &&
!(pgd_none(pgd) || pgd_page_paddr(pgd) == __pa_symbol(bm_pud))) {
/*
* We only end up here if the kernel mapping and the fixmap
* share the top level pgd entry, which should only happen on
* 16k/4 levels configurations.
*/
BUG_ON(!IS_ENABLED(CONFIG_ARM64_16K_PAGES));
pudp = pud_offset_kimg(pgdp, addr);
} else {
// ------------------------------------------------------------------------------------------------- (4)
// if this entry is NULL, need to map it first
if (pgd_none(pgd))
__pgd_populate(pgdp, __pa_symbol(bm_pud), PUD_TYPE_TABLE);

// ------------------------------------------------------------------------------------------------- (5)
// fixmap_pxd() 1) uses PGD/PUD/PMD to get physical base address of the next level table
// 2) uses addr to get table index and uses it to get entry's physical address
// 3) add offset to get entry's virtual address
pudp = fixmap_pud(addr); // 0XFFFF0000121AAFF8
// pudp.pud = 0X421A9003
}

// ----------------------------------------------------------------------------------------------------- (6)
// map PUD and PMD
if (pud_none(READ_ONCE(*pudp)))
__pud_populate(pudp, __pa_symbol(bm_pmd), PMD_TYPE_TABLE);
pmdp = fixmap_pmd(addr); // 0XFFFF0000121A9F98
// pmdp.pmd = 0X421A8003
__pmd_populate(pmdp, __pa_symbol(bm_pte), PMD_TYPE_TABLE);

/*
* The boot-ioremap range spans multiple pmds, for which
* we are not prepared:
*/
BUILD_BUG_ON((__fix_to_virt(FIX_BTMAP_BEGIN) >> PMD_SHIFT)
!= (__fix_to_virt(FIX_BTMAP_END) >> PMD_SHIFT));

if ((pmdp != fixmap_pmd(fix_to_virt(FIX_BTMAP_BEGIN)))
|| pmdp != fixmap_pmd(fix_to_virt(FIX_BTMAP_END))) {
WARN_ON(1);
pr_warn("pmdp %p != %p, %p\\n",
pmdp, fixmap_pmd(fix_to_virt(FIX_BTMAP_BEGIN)),
fixmap_pmd(fix_to_virt(FIX_BTMAP_END)));
pr_warn("fix_to_virt(FIX_BTMAP_BEGIN): %08lx\\n",
fix_to_virt(FIX_BTMAP_BEGIN));
pr_warn("fix_to_virt(FIX_BTMAP_END): %08lx\\n",
fix_to_virt(FIX_BTMAP_END));

pr_warn("FIX_BTMAP_END: %d\\n", FIX_BTMAP_END);
pr_warn("FIX_BTMAP_BEGIN: %d\\n", FIX_BTMAP_BEGIN);
}
}

上面这段代码牵涉较多宏定义,看上去比较麻烦,但实质确很简单 - 就是使用bm_pxdinit_mm.pgd建立页表的映射。通过(5)我们可以方便地理解如何寻找、建立页表。
(5) fixmap_pxx()的工作原理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 这类函数都是
// 1. 通过虚拟地址先定位上一级页表,比如这个函数就是定位PGD;
// 2. 然后通过它找到PUD的物理地址;
// 3. 再利用地址中PUD的索引找到PUD中对应的entry的物理地址;
// 4. 因为bm_pxx是静态分配的变量,它位于Linux内核所在的虚拟内存空间,因此可以用内核PA->VA的转换获得该PUD entry的虚拟地址并返回
static inline pud_t * fixmap_pud(unsigned long addr)
{
pgd_t *pgdp = pgd_offset_k(addr);
pgd_t pgd = READ_ONCE(*pgdp);

BUG_ON(pgd_none(pgd) || pgd_bad(pgd));

return pud_offset_kimg(pgdp, addr);
}

这里插一句,init_mm的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/*
* For dynamically allocated mm_structs, there is a dynamically sized cpumask
* at the end of the structure, the size of which depends on the maximum CPU
* number the system can see. That way we allocate only as much memory for
* mm_cpumask() as needed for the hundreds, or thousands of processes that
* a system typically runs.
*
* Since there is only one init_mm in the entire system, keep it simple
* and size this cpu_bitmask to NR_CPUS.
*/
struct mm_struct init_mm = {
.mm_rb = RB_ROOT,
.pgd = swapper_pg_dir,
.mm_users = ATOMIC_INIT(2),
.mm_count = ATOMIC_INIT(1),
.mmap_sem = __RWSEM_INITIALIZER(init_mm.mmap_sem),
.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),
.user_ns = &init_user_ns,
.cpu_bitmap = { [BITS_TO_LONGS(NR_CPUS)] = 0},
INIT_MM_CONTEXT(init_mm)
};

init_mm.pgd实际上等于INIT_MM_CONTEXT(init_mm)也就是init_pg_dir而不是swapper_pg_dir。在之后的paging_init()中,init_mm.pgd会用swapper_pg_dir取代init_pg_dir

下面看看在实验平台上这几个页表的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
FIXADDR_START        = 0XFFFF7DFFFE7F9000
// -----------------------------------------------
PGD Entry VA = 0XFFFF00001223C7D8
PGD Entry Value (PA) = 0X421AA003
// -----------------------------------------------
PUD Entry VA = 0XFFFF0000121AAFF8
PUD Entry Value (PA) = 0X421A9003
// -----------------------------------------------
PMD Entry VA = 0XFFFF0000121A9F98
PMD Entry Value (PA) = 0X421A8003
// -----------------------------------------------
// 这个就是上面的PGD Entry,可以看出,FIXMAP的映射和内核映射用了不同的PGD entry
init_mm.pgd VA = 0XFFFF00001223C000
init_mm.pgd[0] = 0X4223D003
init_mm.pgd[X] = 0X421AA0030

页表映射完成

early_fixmap_init()函数结束时,PUD, PMD, PTE页表已经更新完毕,其中PUD, PMD只更新了一个入口,而PTE则完全是空白。虽然PTE是空白,但映射已经建立,只要是映射到同一个PMD entry的地址都可以通过PTE建立映射关系。

这里有一个小小的玄机,如果不注意,看这部分的时候会觉得很混乱:

  1. 到目前为止,这些页表都是静态分配的,因此我们只有一个PTE。也就是说FIXMAP如果要进行PTE的映射,其只能映射2MB的内存空间;
  2. 如果我们不进行PTE的映射,直接进行PMD的映射,那么我们就可以映射很多个2MB的内存块。

根据实验结果,

  1. FIX_FDTFIX_PGD(左开右闭)都映射到同一个PMD的入口,它们可以用PTE进行映射;
  2. FIX_FDT2MB对齐的地址,因此它可以直接映射到一个新的PMD入口,其大小是小于4MB的,因此最多用两个PMD的入口(这就说明了为什么只有一个PTE也能完成FIXMAP的映射)。

setup_machine_fdt()

early_fixmap_init()完成后,Linux内核会调用setup_machine_fdt()函数。这个函数主要是处理DT相关的内容,对于内存管理,它会获取内存信息然后使用memblock来进行管理。这里我主要关心的是内存管理相关的内容,因此只关注fixmap_remap_fdt()early_init_dt_scan()

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
// ./arch/arm64/kernel/setup.c

static void __init setup_machine_fdt(phys_addr_t dt_phys)
{
// ---------------------------------------------------------------------- (1)
void *dt_virt = fixmap_remap_fdt(dt_phys);
const char *name;

// ---------------------------------------------------------------------- (2)
if (!dt_virt || !early_init_dt_scan(dt_virt)) {
pr_crit("\n"
"Error: invalid device tree blob at physical address %pa (virtual address 0x%p)\n"
"The dtb must be 8-byte aligned and must not exceed 2 MB in size\n"
"\nPlease check your bootloader.",
&dt_phys, dt_virt);

while (true)
cpu_relax();
}

name = of_flat_dt_get_machine_name();
if (!name)
return;

pr_info("Machine model: %s\n", name);
dump_stack_set_arch_desc("%s (DT)", name);
}

(1) 完成DTFIXMAP的映射,并将相应区域添加入memblockreserve区域;
(2) 扫描DT并将其中内存空间添加入memblockmemoryreserve区域。

这里我之前有个疑问:bm_pxdFIX_PXD有什么关系?
通过分析这段代码可以知道FIX_PUD最终是映射的就是bm_pud的物理地址。同理,FIX_PMD映射的就是bm_pmd。这样做的好处是我们可以直接通过FIX_PXD +/- Offset来访问bm_pxd不同的入口了。这里要注意两点,1. FIX_PGD没有进行映射;2. 这个映射是临时的,在函数调用完后会取消这个映射。

到这里,FIXMAP就告一段落了,这里面还牵涉不少页表映射相关的函数和宏定义,这里我就不做过多分析了。在FIXMAPDT的扫描过程中,我们用到了memblock,在下一篇笔记中,我们就来看看memblock这个启动阶段临时的内存管理器是如何工作的。

参考资料

  1. 【原创】(二)Linux物理内存初始化
  2. Linux内核固定虚拟地址映射
  3. ARM64 Kernel Image Mapping的变化
CATALOG
  1. 1. 虚拟内存空间
  2. 2. 初看FIXMAP
  3. 3. FDT映射的建立
    1. 3.1. early_fixmap_init()
    2. 3.2. 页表映射完成
    3. 3.3. setup_machine_fdt()
  4. 4. 参考资料