Linux 内存管理 (1): 内核镜像映射临时页表的建立

1. 前言

限于作者能力水平,本文可能存在谬误,因此而给读者带来的损失,作者不做任何承诺。

2. 分析背景

本文基于 ARMv7 架构,Linux 4.14 内核进行分析。

3. 内核镜像线性临时映射的建立过程

3.1 预备工作:内核解压缩

对于 ARM32 架构Linux 内核,在 BootLoader(如 U-BOOT) 将内核镜像 载入到内存之后,在进入内核入口 stext 之前,会有一个对压缩内核镜像(如zImage)的解压缩过程。在这里仅对这个解压缩过程做一个简单的描述,但不展开细节。先了解构建内核 zImage 的过程:

               编译+链接                   objcopy                      压缩(gzip,lzo,lzma,...)
1. linux源代码 ---------> vmlinux(elf文件) -------> arch/arm/boot/Image -----------------------> piggy_data
                                     编译
2. piggy.S(包含 piggy_data 压缩内核) ------> piggy.o
                                              链接                                   objcopy
3. (head.o,misc.o,decompress.o,...) + piggy.o ----> arch/arm/boot/compressed/vmlinux ------> arch/arm/boot/zImage

对于 ARM32 架构,内核解压缩代码位于目录 arch/arm/boot/compressed 目录下。从上面的 zImage 构建过程中,我们了解到:

1. 内核代码首先会被编译为 ELF 格式的 vmlinux ,然后通过 objcopy 去掉一些部分后生成 Image ,
   最后将 Image 压缩为文件 piggy_data 。
2. 将包含了整个 piggy_data 的 piggy.S 编译成 piggy.o 。
3. 将用于解压缩的 head.S, misc.c, decompress.c 代码编译为 .o ,再和包含压缩内核的 piggy.o 
   一起,链接为新的 vmlinux ,最后通过 objcopy 将新的 vmlinux 转换为 zImage 文件。

由于内核在编译时被压缩了,那自然的,加载内核会有一个解压缩过程。如果内核启动伴随有解压缩过程,会看到 Uncompressing Linux... 的内核日志。内核解压缩完成后,解压缩程序的使命就完成了,然后跳转到内核代码执行。
对于内核解压缩过程,就简单的介绍到这里,更多细节可参考博文 Linux:内核解压缩过程简析 。接下来,我们从内核入口 stext 开始,逐步分析内核镜像区域线性映射的建立过程。
顺便提一下,ARM64 架构下不再有内核解压缩这一过程,而是直接加载编译链接生成的 Image 镜像文件。

3.2 BOOT CPU: 建立内核镜像区域的临时映射页表

3.2.1 定位内核入口

先来说一下内核入口 stext ,我们是依据什么确定 stext 是内核入口的?答案是内核链接脚本 arch/arm/kernel/vmlinux.lds.S 。看如下内核链接脚本片段:

/* arch/arm/kernel/vmlinux.lds.S */

OUTPUT_ARCH(arm)
ENTRY(stext)
...

SECTIONS
{
	/* 舍弃部分,不被链接到内核 */
	/DISCARD/ : {
		...
	}
	. = PAGE_OFFSET + TEXT_OFFSET;
	.head.text : {
		_text = .; /* 内核镜像起始位置 链接地址 */
		HEAD_TEXT /* 内核镜像开始位置代码 */
	}
	
	...
}

从上面的链接脚本语句 ENTRY(stext) 了解到,内核入口为即为 stext ;我们还可以看到,内核镜像起始位置的代码为 HEAD_TEXT 代码段。再来看一下,这个 HEAD_TEXT 是何方神圣?

/* include/asm-generic/vmlinux.lds.h */

...

/* Section used for early init (in .S files) */
#define HEAD_TEXT  *(.head.text)

...

哈,原来 HEAD_TEXT.o 文件中,那些名为 .head.text 输入段(section);而名为 .head.text 输入段(section),只在 arch/arm/kernel/head.S 中有定义,只此一家,别无分号:

/* arch/arm/kernel/head.S */

.arm

/* 内核入口 */
	/*
	 * 在 inlcude/linux/init.h 定义了 __HEAD:
	 * #define __HEAD		.section	".head.text","ax"
	 */
	__HEAD
ENTRY(stext)
	...

对链接脚本不熟悉的读者,可查阅 ld 链接器文档
另外,顺便对汇编代码里面出现的 #include 做一下说明,其实 AS 汇编器并不认识 #include 指示符,它支持的是 .include, .incbin 等指示符,那为什么我们可以将 #include 写入到汇编代码,但没有出现编译错误呢? 答案是,在内核的编译过程中,会通过 C 预处理器,将汇编代码处理一遍,把这其中符合 C 宏处理规范的代码(如 #include)处理掉,这样最后 AS 汇编器并不会看到 #include 等这些 C 宏代码语句了,这可以作为一个编程技巧,存储到知识库里。

3.2.2 建立内核线性映射前的其它启动工作

内核的启动过程,并不仅仅是建立内核线性映射,还含有其它的工作,如 CPU 模式设定DTB 验证alternative SMP/UP 表修正PV表修正 等,下面将按这些工作发生的先后顺序,对它们一一进行分析说明。以下所有分析,均不讨论 ARM 虚拟化 扩展功能。

3.2.2.1 将 CPU 设为 SVC 模式,且禁用 IRQ + FIQ 中断
/* arch/arm/kernel/head.S */

	.arm

/* 内核入口 */
	__HEAD
ENTRY(stext)
	@ ensure svc mode and all interrupts masked
	/*
	 * . 将 BOOT CPU 设为 SVC 模式
	 * . 禁用 BOOT CPU 的 IRQ + FIQ
	 * r9 是  BOOT CPU CPSR 寄存器新设定的值。
	 */
	safe_svcmode_maskall r9
3.2.2.2 获取处理器类型数据

Linux 内核预定义了处理器类型数据 struct proc_info_list

/* arch/arm/include/asm/procinfo.h */

struct proc_info_list {
	unsigned int		cpu_val; /* 处理器 ID */
	unsigned int		cpu_mask; /* 处理器 ID 位掩码 */
	unsigned long		__cpu_mm_mmu_flags;	/* used by head.S */
	unsigned long		__cpu_io_mmu_flags;	/* used by head.S */
	unsigned long		__cpu_flush;		/* used by head.S */
	const char		*arch_name;
	const char		*elf_name;
	unsigned int		elf_hwcap;
	const char		*cpu_name;
	struct processor	*proc;
	struct cpu_tlb_fns	*tlb;
	struct cpu_user_fns	*user;
	struct cpu_cache_fns	*cache;
};

ARM32 架构 的处理器数据定义在 arch/arm/mm/proc-*.S 文件中,如 arch/arm/mm/proc-v7.S 定义 ARMv7 系列 CPU 的处理器数据,下面截取了一些处理器数据以及相关函数接口定义:

/* arch/arm/mm/proc-v7.S */

__v7_ca7mp_setup: /* Cortex A7 MP(多核) 处理器初始化接口 */
	...
	b	__v7_setup_cont

...

__v7_setup_cont:
	...
	/* 返回到 head.S: stext 中 1:	b	__enable_mmu 处 */
	ret	lr				@ return to head.S:__ret

	/*
	 * Standard v7 proc info content
	 */
.macro __v7_proc name, initfunc, mm_mmuflags = 0, io_mmuflags = 0, hwcaps = 0, proc_fns = v7_processor_functions
	/* proc_info_list::__cpu_mm_mmu_flags */
	ALT_SMP(.long	PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
			PMD_SECT_AF | PMD_FLAGS_SMP | \mm_mmuflags)
	ALT_UP(.long	PMD_TYPE_SECT | PMD_SECT_AP_WRITE | PMD_SECT_AP_READ | \
			PMD_SECT_AF | PMD_FLAGS_UP | \mm_mmuflags)
	/* proc_info_list::__cpu_io_mmu_flags */
	.long	PMD_TYPE_SECT | PMD_SECT_AP_WRITE | \
		PMD_SECT_AP_READ | PMD_SECT_AF | \io_mmuflags
	/*
	 * 详见 arch/arm/mm/proc-macros.S
	 * .macro	initfn, func, base
	 * 		.long	\func - \base
	 * .endm
	 * 此处对应 proc_info_list::__cpu_flush 成员, 
	 * 其值定义为 initfunc - &proc_info_list 地址差值。
	 */
	/* proc_info_list::__cpu_flush */
	initfn	\initfunc, \name
	/* proc_info_list::arch_name */
	.long	cpu_arch_name
	/* proc_info_list::elf_name */
	.long	cpu_elf_name
	/* proc_info_list::elf_hwcap */
	.long	HWCAP_SWP | HWCAP_HALF | HWCAP_THUMB | HWCAP_FAST_MULT | \
		HWCAP_EDSP | HWCAP_TLS | \hwcaps
	/* proc_info_list::cpu_name */
	.long	cpu_v7_name
	/* proc_info_list::proc (struct processor	*proc;) */
	.long	\proc_fns
	/* proc_info_list::tlb (struct cpu_tlb_fns	*tlb;) */
	.long	v7wbi_tlb_fns
	/* proc_info_list::user (struct cpu_user_fns	*user;) */
	.long	v6_user_fns
	/* proc_info_list::cache (struct cpu_cache_fns	*cache;) */
	.long	v7_cache_fns
.endm
	
...
	
	/*
	 * ARM Ltd. Cortex A7 processor.
	 */
	/* Cortex A7 处理器对象定义
	 * 详见: struct proc_info_list (arch/arm/include/asm/procinfo.h)
	 */
	.type	__v7_ca7mp_proc_info, #object
__v7_ca7mp_proc_info:
	.long	0x410fc070 /* proc_info_list::cpu_val */
	.long	0xff0ffff0 /* proc_info_list::cpu_mask */
	__v7_proc __v7_ca7mp_proc_info, __v7_ca7mp_setup
	.size	__v7_ca7mp_proc_info, . - __v7_ca7mp_proc_info

启动阶段,通过读取 CPU ID 寄存器,来获取 CPU ID,然后通过 CPU ID 匹配 Linux 内核预定义的处理器类型数据 struct proc_info_list :如果找到匹配的数据,说明内核当前支持该处理器类型;否则说明内核不支持该处理器类型,系统将陷入死循环(即启动失败)。看一下具体代码:

/* arch/arm/kernel/head.S */

	/*
	 * 读取处理器 ID 到 r9
	 * 参考 ARM Architecture Reference Manual.pdf, P687
	 */
	mrc	p15, 0, r9, c0, c0		@ get processor id
	/* 通过 r9 中的处理器 ID ,查找对应的系统预定义的处理器信息数据 proc_info_list ,
	 * 从 r5 寄存器返回。
	 * r5: 处理器信息指针, 
	 *      类型为 struct proc_info_list *, 
	 *      定义在 arch/arm/include/asm/procinfo.h
	 */
	bl	__lookup_processor_type		@ r5=procinfo r9=cpuid
	movs	r10, r5				@ invalid processor (r5=0)? /* r10: 处理器信息指针 */
 THUMB( it	eq )		@ force fixup-able long branch encoding
	beq	__error_p			@ yes, error 'p' /* 出错,没找到匹配的处理器数据,系统将进入死循环 */
3.2.2.3 建立线性映射临时页表

在进入代码细节前,我们先泛泛而谈一下内存管理工作基本流程:简而言之一句话,内存管理就是将虚拟地址转换为物理地址(不考虑虚拟化场景)。那为什么要这么做?为什么要引入操作系统这个内存管理者?直接以物理地址访问不行吗?我们假设不使用操作系统的内存管理,直接允许应用自己来使用系统中的内存,那大家都可以多吃多占,那谁该多用谁该少占?彼此之间的内存区间彼此覆盖又该怎么办(它们彼此缺少隔离机制)?因此,我们需要一个公共的内存管理者操作系统,来统一分配系统中的内存,这个管理者既尽量满足大家的需求,又尽量保持公平,同时避免应用彼此内存区间的覆盖。引入操作系统作为内存管理者,看起来解决了所有的问题,但事实并不然,虽然操作系统内存管理可以分配彼此不覆盖的内存区间给不同应用,但是如果分配的内存是以物理地址来访问,仍然没法阻止应用彼此之间内存覆写的情形。应用A把应用B给整崩溃了,应用B何辜?以现在系统中包含动辄几十上百个应用的情形,如果大家彼此都可以覆写对方的内存区间,那就乱成一锅粥了。为此,引入了虚拟地址的概念,系统中每个应用都有着相同的虚拟地址空间(如32位应用的 0x00000000~0xFFFFFFFF),每个应用的虚拟地址空间可以映射到相同或不同的物理内存,这就是地址空间隔离,防止应用彼此覆写对方的物理内存(共享内存情形除外)。于是,操作系统这个内存管理者,具备了两个功能:第一,集中管理系统中的内存;第二,为系统中的应用建立独立隔离的虚拟地址空间。
回到我们的分析场景:ARM32 架构 下的 Linux 内存管理。要了解内存管理,首先需要了解硬件架构内存管理硬件的相关知识。在进入建立线性映射临时页表的代码分析之前,先来看看 ARMv7 架构(代码是针对ARMv7架构进行分析)的内存管理单元(MMU)硬件中和本文分析相关的部分。
先看下ARM32架构下的内存管理地址翻译的基本流程,如下图:
在这里插入图片描述
上图是ARM32架构下,使用2级(最多2级)页表进行映射的地址翻译(将虚拟地址转换为物理地址)流程,这里对该图进行一下简要说明:

1. TTBR0,TTBR1 寄存器存有第1级页表(First-level table)的物理基址;
2. 第1级页表(First-level table)的页表项,可能是下列3种类型中一种:
   . 指向 1MB 大小 section 的物理基址
   . 指向第2级页表(Second-level table)的物理基址
   . 指向 16MB 大小 Supersection 的物理基址
3. 第2级页表(Second-level table)的页表项,可能是下列2种类型中一种:
   . 指向 64KB 大小 Large page 的物理基址
   . 指向 4KB 大小 Small page 的物理基址
4. VA 指32位虚拟地址:
   . 1级页表映射(1MB section 或 16MB Supersection),将虚拟地址拆分为2部分,
     高位部分用来索引第1级(First-level table)的页表项,低位部分是相对于 section
     或 Supersecion 内的偏移
   . 2级页表映射(64KB Large page 或 4KB Small page),将虚拟地址拆分为3部分,
     高位部分用来索引(First-level table)的页表项,中间位部分用来索引
     (Second-level table)的页表项,低位部分是相对于 Large page 或 Small page
     内的偏移

从上面了解到,页表项可以是不同的类型,那是通过什么来决定页表项的类型?第1级页表(First-level table)的页表项第2级页表(Second-level table)的页表项 有着不同的定义。首先看第1级页表(First-level table)的页表项的定义:
在这里插入图片描述
我们看到,第1级页表(First-level table)的页表项,每个页表项为32位长度(并非所有情形,后面会加以补充说明),最低2位决定了页表项的类型

0b00 :非法页表项
0b01 :页表项指向第2级页表(Second-level table)的物理基址
0b10 :页表项指向 Section(Bit[18]0)Supersection(Bit[18]1) 的基址
0b11 :保留类型

再看第2级页表(Second-level table)的页表项的定义:
在这里插入图片描述
我们看到,第2级页表(Second-level table)的页表项,每个页表项为32位长度(并非所有情形,后面会加以补充说明),最低2位决定了页表项的类型

0b00 :非法页表项
0b01 :页表项指向 64KB Large page 的物理基址
0b1x :页表项指向 4KB Small page 的物理基址
       最低位 XN==1 ,表示页表包含的数据不具备可执行权限

你可能注意到,上面的页表项说明中,都有一个 Short-descriptor 标记,这是什么意思?我们先跑下题,再回头来解答这个问题。一个系统能支持的最大内存,由硬件的物理地址位数决定,如果硬件系统只支持32位物理地址,那么系统最多只能管理 2^32 = 4GB 的物理内存,如果32位系统下,要支持超过4GB的内存,又或者,某些机器虽然只支持4GB内存,但是内存的起始物理地址不是0,那么最大物理地址也超过了32位物理地址能表达的范围,这时候该怎么办?为此,硬件引入了物理地址扩展,以能访问超过4G内存地址的物理内存区间。在ARM32架构下,这个特性为 LPAE(Large Physical Address Extension),它将物理地址扩展到40位,因此能访问物理地址区间也扩展到 2^40 = 1024GB 。回到我们的问题,LPAE扩展了物理地址范围,页表管理也随着发生了变化,引入了和 短描述符(Short-descriptor) 对应的 长描述符(Long-descriptor)
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
长描述符(Long-descriptor) 方式下,页表映射最多可达3级,页表项的长度也变为了64位,更多关于 长描述符(Long-descriptor) 的细节本文不再展开,感兴趣的读者可参考 ARM 官方手册 《DDI0406C_d_armv7ar_arm.pdf》
经过前面的铺垫,现在可以进入到代码细节的分析了:

/* arch/arm/kernel/head.S */

	/*
	 * 检查系统内存管理硬件是否支持 LPAE 特性。
	 *
	 * ARMv7-A: Large Physical Address Extension, 4GB => 1024GB.
	 * 转换 32 位虚拟地址 为 40 位物理地址,这需要系统提供页表长描述符
	 * (long descriptor)的支持.
	 * 参考 DDI0406C_d_armv7ar_arm.pdf, P1615
	 */
#ifdef CONFIG_ARM_LPAE
	mrc	p15, 0, r3, c0, c1, 4		@ read ID_MMFR0
	and	r3, r3, #0xf			@ extract VMSA support
	cmp	r3, #5				@ long-descriptor translation table format?
 THUMB( it	lo )				@ force fixup-able long branch encoding
	blo	__error_lpae			@ only classic page table format
#endif

#ifndef CONFIG_XIP_KERNEL
	adr	r3, 2f /* r3 = 标号 2 的物理地址 */
	ldmia	r3, {r4, r8}/* r4 = 标号 2 的链接虚拟地址, r8 = PAGE_OFFSET */
	sub	r4, r3, r4			@ (PHYS_OFFSET - PAGE_OFFSET) /* r4 = 标号 2 的物理地址 - 标号 2 的链接虚拟地址 */
	add	r8, r8, r4			@ PHYS_OFFSET /* r8 = PAGE_OFFSET 对应的物理地址 */
#else
	ldr	r8, =PLAT_PHYS_OFFSET		@ always constant in this case
#endif

	/*
	 * r1 = machine no, r2 = atags or dtb,
	 * r8 = phys_offset, r9 = cpuid, r10 = procinfo
	 */
	bl	__vet_atags /* fdt/atags合法性验证 */
#ifdef CONFIG_SMP_ON_UP /* CONFIG_SMP_ON_UP: 允许在 单核处理器 上启动 支持 SMP 的内核 */
	/*
	 * 检测是否是单核系统,如果是,则将所有 .alt.smp.init 段中起始 9998 标号处
	 * 多核场景内容(SMP 内核 .alt.smp.init 处默认是多核场景使用的内容),替换为
	 * 单核场景下应使用的内容。 
	 */
	bl	__fixup_smp
#endif
#ifdef CONFIG_ARM_PATCH_PHYS_VIRT
	/*
	 * 物理/虚拟地址运行时动态转换功能: 修正 add/sub 转换指令的立即数部分。
	 * arch/arm/include/asm/memory.h
	 * __pv_stub() 建立的 .pv_table 段 (__phys_to_virt(), __virt_to_phys_nodebug())。
 	 */
	bl	__fixup_pv_table
#endif
	/* 创建 1MB(非 LPAE) 或 2MB(LPAE) Section 粒度的、内核镜像区间的 线性映射 临时页表 */
	bl	__create_page_tables
	
	/* 建立内核镜像线性映射后,启动 MMU 的工作,将在后面作出分析 */
	...

__create_page_tables 是我们的重头戏,由此建立了内核镜像区间的线性映射(临时页表),来看细节:

/* arch/arm/kernel/head.S */

/*
 * swapper_pg_dir is the virtual address of the initial page table.
 * We place the page tables 16K below KERNEL_RAM_VADDR.  Therefore, we must
 * make sure that KERNEL_RAM_VADDR is correctly set.  Currently, we expect
 * the least significant 16 bits to be 0x8000, but we could probably
 * relax this restriction to KERNEL_RAM_VADDR >= PAGE_OFFSET + 0x4000.
 */
#define KERNEL_RAM_VADDR	(PAGE_OFFSET + TEXT_OFFSET)
#if (KERNEL_RAM_VADDR & 0xffff) != 0x8000
#error KERNEL_RAM_VADDR must start at 0xXXXX8000
#endif

#ifdef CONFIG_ARM_LPAE
	/* LPAE requires an additional page for the PGD */
#define PG_DIR_SIZE	0x5000 /* 0x4000 + 0x1000 = 16KB + 4KB */
#define PMD_ORDER	3 /* 页表项大小: 1<<PMD_ORDER = 1<<3 = 8 字节 */
#else
#define PG_DIR_SIZE	0x4000 /* 16KB */
#define PMD_ORDER	2 /* 页表项大小: 1<<PMD_ORDER = 1<<2 = 4 字节 */
#endif

	.globl	swapper_pg_dir
	.equ	swapper_pg_dir, KERNEL_RAM_VADDR - PG_DIR_SIZE

	/*
	 * @phys: PAGE_OFFSET 对应的物理地址
	 * 从 @rd 返回第一级页表物理地址: phys + TEXT_OFFSET - PG_DIR_SIZE
	 */
	.macro	pgtbl, rd, phys
	add	\rd, \phys, #TEXT_OFFSET
	sub	\rd, \rd, #PG_DIR_SIZE
	.endm

...

/*
 * r8 = PAGE_OFFSET 对应的物理地址
 * 返回:r4 = 指向第一级页表物理地址,也即页表物理首地址 
 */
__create_page_tables:
	/* r4 = 内核初始页表物理地址, 16KB / 20KB(LPAE) 大小(swapper_pg_dir) */
	/* 
	 * 目前物理地址空间分布如图:
	 * (假定内核空间起始物理地址为 0x60000000, 即 PHYS_OFFSET == 0x60000000,TEXT_OFFSET = 0x408000)
	 * 
	 *                物理地址空间
	 *    高  |                        |
	 * 地  ^  |          ...           |
	 *     |  |                        |
	 * 址  |  |------------------------|
	 *     |  |                        |
	 * 增  |  |      Kernel Image      |
	 *     |  |                        |
	 * 长  |  |------------------------| <--- PHYS_OFFSET + TEXT_OFFSET(0x60000000 + 0x408000 = 0x60408000)
	 *     |  |   swapper_pg_dir(16K)  |
	 * 方  |  |------------------------| <--- r4 = PHYS_OFFSET + TEXT_OFFSET - 0x4000(0x60404000)
	 *     |  |                        |
	 * 向  |  |                        |     
	 *    低  |------------------------| <--- PHYS_OFFSET(0x60000000): 内核空间物理起始地址
	 *        |          ...           |
	 */
	pgtbl	r4, r8				@ page table address

	/*
	 * Clear the swapper page table
	 */
	/* kernel 16KB (!CONFIG_ARM_LPAE) 或 16KB + 4KB (CONFIG_ARM_LPAE) 一级(L1)页表(swapper_pg_dir)全部清0 */ 
	mov	r0, r4
	mov	r3, #0
	add	r6, r0, #PG_DIR_SIZE
1:	str	r3, [r0], #4
	str	r3, [r0], #4
	str	r3, [r0], #4
	str	r3, [r0], #4
	teq	r0, r6
	bne	1b

#ifdef CONFIG_ARM_LPAE
	/*
	 * Build the PGD table (first level) to point to the PMD table. A PGD
	 * entry is 64-bit wide.
	 */
	/*
	 * 建立 LPAE 第1级长描述页表:
	 * 4个页表项,每个页表项指向【第2级4个页表】的物理首地址。
	 *           /   -----------------
	 *          | 0 | PGD Table Entry |--------------+
	 *          |   |-----------------|              |
	 *          | 1 | PGD Table Entry |----------+   |
	 *          |   |-----------------|          |   |
	 *    4KB  /  2 | PGD Table Entry |------+   |   |
	 *   512项  \   |-----------------|      |   |   |
	 *          | 3 | PGD Table Entry |--+   |   |   |
	 *          |   |-----------------|  |   |   |   |
	 *          |   |     ......      |  |   |   |   |
	 *          |   |                 |  |   |   |   |
	 *           \  |-----------------|<-|---|---|---+
	 *            / | PGD Table       |  |   |   |
	 *           /  |-----------------|<-|---|---+
	 *          |-- | PGD Table       |  |   |
	 *   4*4KB  /   |-----------------|<-|---+
	 * 4*512项 |----| PGD Table       |  |
	 *         \    |-----------------|<-+
	 *          |-- | PGD Table       |
	 *            \ |-----------------|
	 *              |     ......      |
	 */

	/* 参考文档: DDI0406C_d_armv7ar_arm.pdf, P1334 */ 
	/* r4: 内核页表起始物理地址 */
	mov	r0, r4
	/*
	 * r3: 第一个 PGD 页表物理地址(即 第二级页表 起始物理地址:所有二级页表放在地址连续的物理内存空间)
	 * 
	 * 每个长描述符页表项是 64 位长度,所以 0x1000 = 4096 长度空间,包含的
	 * 长描述符页表项个数为 4096 / 8 = 512 ,但仅使用了前 4 个表项。
	 * 详见 DDI0406C_d_armv7ar_arm.pdf, P1336
	 */
	add	r3, r4, #0x1000			@ first PMD table address
	/* 
	 * 长描述符页表项低 2 位表示页表项类型: 类型 3 表示页表项指向下一级页表.
	 * 详见 DDI0406C_d_armv7ar_arm.pdf, P1336
	 */
	orr	r3, r3, #3			@ PGD block type
	/* 长描述符 第一级页表 仅包含 4 个页表项 */
	mov	r6, #4				@ PTRS_PER_PGD
	/* 
	 * 软件标记。用来标记 长描述符页表项 指向的是 swapper 进程的页表。
	 * bit[55], 因为 长描述符页表项 用 64-bit 存储,而且分为低 32-bit
	 * 和 高 32-bit ,所以 bit[55] 在高 32-bit 的第 (55 - 32) 位,因此
	 * 有: #1 << (55 - 32) 。
	 * 为什么是 bit[55] 呢?因为当页表项类型为 3 时,页表项的 bit[58:52]
	 * 被硬件忽略,详见 DDI0406C_d_armv7ar_arm.pdf, P1336
	 */ 
	mov	r7, #1 << (55 - 32)		@ L_PGD_SWAPPER
1:
	/* 设置长描述符页表项低 32-bit: 下一级页表地址 */
	str	r3, [r0], #4			@ set bottom PGD entry bits
	/* 设置长描述符页表项高 32-bit: L_PGD_SWAPPER */
	str	r7, [r0], #4			@ set top PGD entry bits
	/* r3: 下一个 PGD 页表物理地址(即 第二级页表 物理地址)。
	 * 每个第二级页表大小为 4KB(0x1000) 
	 */
	add	r3, r3, #0x1000			@ next PMD table
	subs	r6, r6, #1
	bne	1b

	/* r4 = 第2级 4个 页表物理首地址 */
	add	r4, r4, #0x1000			@ point to the PMD tables
#endif /* CONFIG_ARM_LPAE */

	/*
	 * r10 = proc_info_list *
	 * r7 = proc_info_list::__cpu_mm_mmu_flags
	 */
	ldr	r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags

	/*
	 * Create identity mapping to cater for __enable_mmu.
	 * This identity mapping will be removed by paging_init().
	 */
	/* 建立   CPU MMU 启用函数 __enable_mmu 的等同映射,这是基于 ARM 官方手册的建议 */ 
	adr	r0, __turn_mmu_on_loc /* r0 = __turn_mmu_on_loc 的当前物理地址 */
	/*
	 * r3 = __turn_mmu_on_loc 的链接虚拟地址
	 * r5 = __turn_mmu_on 的链接虚拟地址
	 * r6 = __turn_mmu_on_end 的链接虚拟地址
	 */
	ldmia	r0, {r3, r5, r6}
	/* r0 = __turn_mmu_on_loc (当前物理地址 - 链接虚拟地址) */
	sub	r0, r0, r3			@ virt->phys offset
	/* r5 = __turn_mmu_on 的当前物理地址 */
	add	r5, r5, r0			@ phys __turn_mmu_on
	/* r6 = __turn_mmu_on_end 的当前物理地址 */
	add	r6, r6, r0			@ phys __turn_mmu_on_end
	/*
	 * 如果启用了 LPAE, 则为 3 级页表: 
	 * . r5 = __turn_mmu_on 当前物理地址 >> 21 (高 11 位)
	 * . r6 = __turn_mmu_on_end 当前物理地址 >> 21 (高 11 位)
	 * 详见 DDI0406C_d_armv7ar_arm.pdf, P
	 *
	 * 如果没启用 LPAE, 则为 2 级页表: 
	 * . r5 = __turn_mmu_on 当前物理地址 >> 20 (高 12 位)
	 * . r6 = __turn_mmu_on_end 当前物理地址 >> 20 (高 12 位)
	 * 详见 DDI0406C_d_armv7ar_arm.pdf, P1323
	 */
	mov	r5, r5, lsr #SECTION_SHIFT
	mov	r6, r6, lsr #SECTION_SHIFT

	/*
	 * 启用了 LPAE: r3 = proc_info_list::__cpu_mm_mmu_flags | 
	 *                   区间 [__turn_mmu_on,__turn_mmu_on_end] 当前页面地址高 11 位
	 *
	 * 没启用 LPAE: r3 = proc_info_list::__cpu_mm_mmu_flags | 
	 *                   区间 [__turn_mmu_on,__turn_mmu_on_end] 当前页面地址高 12 位
	 * r7 = proc_info_list::__cpu_mm_mmu_flags
	 */
1:	orr	r3, r7, r5, lsl #SECTION_SHIFT	@ flags + kernel base
	/*
	 * 所谓 identity mapping ,是指传递给 MMU 的【输入地址】,不管是 【物理地址(PA)】,
	 * 还是 【虚拟地址(VA)】 ,它们得到 【同一个输出地址】 。
	 *
	 * 假设已知 __turn_mmu_on 的链接虚拟地址为 0xC0200000 ,
	 * 同时假设 0xC0000000 对应的物理地址为 0x60000000 .
	 * 那么可以推得 __turn_mmu_on 的当前物理地址为:
	 * 0xC0200000 - 0xC0000000 + 0x60000000 = 0x60200000
	 * (1) 如果启用了 LPAE
	 *     r5 = 0x60200000 >> 21
	 *     r3 = 0x60200000 | mm_mmuflags
	 * (2) 如果没启用 LPAE
	 *     r5 = 0x60200000 >> 20
	 *     r3 = 0x60200000 | mm_mmuflags
	 * 此处用 __turn_mmu_on 的【物理地址】 作为 【输入地址】,然后将其 【输出地址】
	 * 配置为 __turn_mmu_on 的【物理地址】,即 【输入地址】 和 【物理地址】 都是
	 * __turn_mmu_on 的【物理地址】 。
	 * 后面的内核镜像映射,会将 __turn_mmu_on 的虚拟地址,映射为其物理地址。
	 * 这两者结合起来,就是 identity mapping ,也就是 __turn_mmu_on 有两个
	 * 页表映射项:
	 * . 一个以 __turn_mmu_on 的【物理地址】 作为 【输入地址】
	 * . 一个以 __turn_mmu_on 的【虚拟地址】 作为 【输入地址】
	 * 这两个映射表项的 【输出地址】 都是 __turn_mmu_on 的【物理地址】 。
	 *
	 * 配置 identity mapping 的目的是为启用 MMU 的代码片段工作正确。
	 */
	str	r3, [r4, r5, lsl #PMD_ORDER]	@ identity mapping
	cmp	r5, r6
	addlo	r5, r5, #1			@ next section
	blo	1b

	/*
	 * Map our RAM from the start to the end of the kernel .bss section.
	 */
	/*
	 * 建立区间 [内核空间起始位置虚拟地址 PAGE_OFFSET, 内核镜像结束位置虚拟地址) 的页表。
	 * 要注意的是, 这个映射区间包括了页表 swapper_pg_dir 本身在内。
	 */
	/* (1) 如果启用了 LPAE
	 *     r4 = 第2级 4个 页表物理首地址
	 *     r0 = r4 + (PAGE_OFFSET >> (21 - 3)) 
	 *        = 内核空间起始位置虚拟地址 PAGE_OFFSET 对应页表项的物理地址
	 * (2) 如果没启用 LPAE
	 *     r4 = 第1级页表物理首地址,也是所有页表的物理首地址
	 *     r0 = r4 + (PAGE_OFFSET >> (20 - 2))
	 *        = 内核空间起始位置虚拟地址 PAGE_OFFSET 对应页表项的物理地址
	 */
	add	r0, r4, #PAGE_OFFSET >> (SECTION_SHIFT - PMD_ORDER)
	ldr	r6, =(_end - 1) /* 内核镜像 结束位置 虚拟地址 */
	/* 
	 * r8 = PAGE_OFFSET 对应的物理地址
	 * r7 = proc_info_list::__cpu_mm_mmu_flags
	 *
	 * r3 = PAGE_OFFSET 对应的物理地址 | proc_info_list::__cpu_mm_mmu_flags
	 */
	orr	r3, r8, r7
	/* r6 = r4 + (r6 >> (SECTION_SHIFT - PMD_ORDER)) 
	 *    = 【内核镜像 结束位置 虚拟地址】 对应页表项的物理地址
	 */
	add	r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
	/* r3: 内核空间当前 section 映射的 物理地址
	 * r0: 内核空间当前 section 对应页表项的物理地址
	 */
	/* 
	 * (1) 建立内核空间当前 section 的页表映射。
	 *     如果开启了 LPAE, section 大小为 1<<SECTION_SHIFT = 2MB (SECTION_SHIFT == 21); 
	 *     否则 section 大小为 1MB = 1<<SECTION_SHIFT (SECTION_SHIFT == 20) 。
	 * (2) r0 更新为内核空间下一 section 的页表项物理地址: r0 += (1 << PMD_ORDER)
	 * (3) r3 更新为内核空间下一 section 要映射的物理地址
	 */ 
1:	str	r3, [r0], #1 << PMD_ORDER
	add	r3, r3, #1 << SECTION_SHIFT
	cmp	r0, r6 /* 到达内核镜像结尾了吗? */
	bls	1b /* 否的话就继续;是的话就结束了 */

	/*
	 * Then map boot params address in r2 if specified.
	 * We map 2 sections in case the ATAGs/DTB crosses a section boundary.
	 */
	/*
	 * 从下面的代码了解到,内核只为 DTB 建立 2个 section 的映射,
	 * 这意味着 DTB 最大只能是 2个 section 的大小:
	 * . 如果开启了 LAPE , 2 * 2MB = 4MB
	 * . 如果没开启 LAPE , 2 * 1MB = 2MB
	 */ 
	/*
	 * r2 = atags or dtb 物理地址
	 * r8 = PAGE_OFFSET 的物理地址
	 */
	/* r0 = DTB section 物理基地址 */
	mov	r0, r2, lsr #SECTION_SHIFT
	movs	r0, r0, lsl #SECTION_SHIFT
	/* r3 = DTB section 的链接虚拟地址 */
	subne	r3, r0, r8
	addne	r3, r3, #PAGE_OFFSET
	/* r3 = DTB section 页表项物理地址 */
	addne	r3, r4, r3, lsr #(SECTION_SHIFT - PMD_ORDER)
	/* r6 = DTB section 物理基地址 | proc_info_list::__cpu_mm_mmu_flags */
	orrne	r6, r7, r0
	/*
	 * 配置 DTB section 页表项: VA -> PA
	 * . [r3] = DTB section 物理基地址 | proc_info_list::__cpu_mm_mmu_flags
	 * . r3 += #1 << PMD_ORDER (即 r3 指向下一个 DTB section 的页表项物理地址)
	 */
	strne	r6, [r3], #1 << PMD_ORDER
	/* r6 = 下一 DTB section 物理基地址 | proc_info_list::__cpu_mm_mmu_flags */
	addne	r6, r6, #1 << SECTION_SHIFT
	/* 配置下一 DTB section 页表项: VA -> PA */
	strne	r6, [r3]

#ifdef CONFIG_ARM_LPAE
	/* r4 = 指向第一级页表物理地址,也即页表物理首地址 */
	sub	r4, r4, #0x1000		@ point to the PGD table
#endif
	ret	lr
ENDPROC(__create_page_tables)

到此,已经建立了内核镜像区间,以及 DTB 的临时线性映射。为什么说是线性映射?因为这些映射,让虚拟地址和物理地址相差一个固定的值。接下来,我们剩下的工作,就是开启 MMU ,进入按虚拟地址的世界了。

3.2.2.4 启用 MMU:由 物理地址 转换为 虚拟地址 访问

在本小节之前的分析,都是使用物理地址在进行访问。但最终,内核要转换到使用虚拟地址进行访问,这是通过建立页表后再启用 MMU 后达成的。看代码细节:

/* arch/arm/kernel/head.S */

/* 内核入口 */
	__HEAD
ENTRY(stext)
	...

	bl	__create_page_tables

	/*
	 * r13 = __mmap_switched 的链接虚拟地址
	 * __enable_mmu 开启 MMU 后,将跳转到 __mmap_switched 执行。
	 */ 
	ldr	r13, =__mmap_switched		@ address to jump to after
						@ mmu has been enabled
	/* lr = CPU 初始化函数(如 __v7_ca7mp_setup)的返回地址 */
	badr	lr, 1f				@ return (PIC) address
#ifdef CONFIG_ARM_LPAE
	mov	r5, #0				@ high TTBR0
	/*
	 * r4 = 页表物理基址
	 * r8 = r4 >> 12 (4KB 物理页框号)
	 */
	mov	r8, r4, lsr #12			@ TTBR1 is swapper_pg_dir pfn
#else
	/* r8 = 页表物理基址 */
	mov	r8, r4				@ set TTBR1 to swapper_pg_dir
#endif
	/*
	 * r10 = proc_info_list *
	 * r12 = proc_info_list::__cpu_flush 其值为: 
	 *       &proc_info_list::proc->_proc_init - &proc_info_list
	 *       详见 proc-v7.S 中 __v7_proc 定义 和 proc-macros.S 中 initfn 定义。
	 */
	ldr	r12, [r10, #PROCINFO_INITFUNC]
	/* r12 = CPU 初始化函数 &proc_info_list::proc->_proc_init 指针 
	 * 如 __v7_ca7mp_setup (arch/arm/mm/proc-v7.S)
	 */
	add	r12, r12, r10
	ret	r12 /* CPU 初始化: 如 __v7_ca7mp_setup */
1:	b	__enable_mmu /* 开启 MMU */
ENDPROC(stext)

/*
 * Setup common bits before finally enabling the MMU.  Essentially
 * this is just loading the page table pointer and domain access
 * registers.  All these registers need to be preserved by the
 * processor setup function (or set in the case of r0)
 *
 *  r0  = cp#15 control register
 *  r1  = machine ID
 *  r2  = atags or dtb pointer
 *  r4  = TTBR pointer (low word)
 *  r5  = TTBR pointer (high word if LPAE)
 *  r9  = processor ID
 *  r13 = *virtual* address to jump to upon completion
 */
__enable_mmu:
#if defined(CONFIG_ALIGNMENT_TRAP) && __LINUX_ARM_ARCH__ < 6
	orr	r0, r0, #CR_A
#else
	bic	r0, r0, #CR_A
#endif
#ifdef CONFIG_CPU_DCACHE_DISABLE
	bic	r0, r0, #CR_C
#endif
#ifdef CONFIG_CPU_BPREDICT_DISABLE
	bic	r0, r0, #CR_Z
#endif
#ifdef CONFIG_CPU_ICACHE_DISABLE
	bic	r0, r0, #CR_I
#endif
	/* 
	 * 设置第1级页表基址。
	 * 细节参考 DDI0406C_d_armv7ar_arm.pdf, P1724
	 */
#ifdef CONFIG_ARM_LPAE
	/*
	 * r4 = 第1级页表基址 low word
	 * r5 = 第1级页表基址 high word
	 * 将 r4,r5 中的第1级页表基址写到 64-bit 寄存器 TTBR0 (LPAE 开启,TTBRx 为 64-bit)
	 */
	mcrr	p15, 0, r4, r5, c2		@ load TTBR0
#else
	/* 设置域访问控制 */
	mov	r5, #DACR_INIT
	mcr	p15, 0, r5, c3, c0, 0		@ load domain access register
	/*
	 * r4 = 第1级页表基址
	 * 将 r4 中的第1级页表基址写到 32-bit 寄存器 TTBR0 (LPAE 关闭,TTBRx 为 32-bit)
	 */
	mcr	p15, 0, r4, c2, c0, 0		@ load page table pointer
#endif
	b	__turn_mmu_on /* 在 identity mapping 代码中开启 MMU */
ENDPROC(__enable_mmu)

.align	5
	.pushsection	.idmap.text, "ax"
ENTRY(__turn_mmu_on)
	mov	r0, r0
	instr_sync
	/* 启用 当前 CPU 的 MMU */
	mcr	p15, 0, r0, c1, c0, 0		@ write control reg
	mrc	p15, 0, r3, c0, c0, 0		@ read id reg
	instr_sync
	mov	r3, r3
	/*
	 * BOOT CPU: r13 = __mmap_switched
	 * 非 BOOT CPU: r13 = __secondary_switched 的链接虚拟地址
	 */
	mov	r3, r13
	/*
	 * BOOT CPU: 返回到 __mmap_switched 处
	 * 非 BOOT CPU: 返回到 __secondary_switched 处 
	 */
	ret	r3
__turn_mmu_on_end:
ENDPROC(__turn_mmu_on)
	.popsection

.align	2
	.type	__mmap_switched_data, %object
__mmap_switched_data:
	.long	__data_loc			@ r4
	.long	_sdata				@ r5
	.long	__bss_start			@ r6
	.long	_end				@ r7
	.long	processor_id			@ r4
	.long	__machine_arch_type		@ r5
	.long	__atags_pointer			@ r6
#ifdef CONFIG_CPU_CP15
	/* CP15 控制寄存器值 */
	.long	cr_alignment			@ r7
#else
	.long	0				@ r7
#endif
	/* 当前 CPU 的 swapper 进程内核栈指针 */
	.long	init_thread_union + THREAD_START_SP @ sp
	.size	__mmap_switched_data, . - __mmap_switched_data

/*
 * The following fragment of code is executed with the MMU on in MMU mode,
 * and uses absolute addresses; this is not position independent.
 *
 *  r0  = cp#15 control register
 *  r1  = machine ID
 *  r2  = atags/dtb pointer
 *  r9  = processor ID
 */
	__INIT
__mmap_switched: /* 此处代码运行于 MMU 开启状况 */
	adr	r3, __mmap_switched_data /* r3 = __mmap_switched_data 虚拟地址 */

	/*
	 * r4 = __data_loc 链接虚拟地址 (内核数据段 .data 起始位置虚拟地址)
	 * r5 = _sdata 链接虚拟地址 (内核数据段 .data 起始位置虚拟地址)
	 * r6 = __bss_start 链接虚拟地址 (内核 bss 数据段其实位置虚拟地址)
	 * r7 = _end (内核结束位置虚拟地址)
	 *
	 * r3 += 4 * 4 => __mmap_switched_data.processor_id 的链接虚拟地址
	 */
	ldmia	r3!, {r4, r5, r6, r7}
	/*
	 * 初始化内核数据段 (.data): [__data_loc, __bss_start): 
	 * 当 .data 段的 LMA(加载地址) 和 VMA(运行时地址) 不同时,需要做数据拷贝。
	 */
	cmp	r4, r5				@ Copy data segment if needed
1:	cmpne	r5, r6 /* r5 < __bss_start ? */
	ldrne	fp, [r4], #4 /* fp = 内核数据段当前位置 [r4] 数据, r4 += 4 */
	strne	fp, [r5], #4 /* [r5] <= fp, r5 += 4 */
	bne	1b

	/* 内核 bss 数据段清 0 */
	mov	fp, #0				@ Clear BSS (and zero fp)
1:	cmp	r6, r7
	strcc	fp, [r6],#4
	bcc	1b

	/*
	 * r4 = &processor_id (arch/arm/kernel/setup.c)
	 * r5 = &__machine_arch_type (arch/arm/kernel/setup.c)
	 * r6 = &__atags_pointer (arch/arm/kernel/setup.c)
	 * r7 = &cr_alignment (arch/arm/kernel/entry-armv.S)
	 * sp = 当前 CPU 的 swapper 进程内核栈指针
	 */
 ARM(	ldmia	r3, {r4, r5, r6, r7, sp})
 THUMB(	ldmia	r3, {r4, r5, r6, r7}	)
 THUMB(	ldr	sp, [r3, #16]		)
 	/* processor_id = 处理器 ID */
	str	r9, [r4]			@ Save processor ID
	/*
	 * r1 = machine no
	 * __machine_arch_type = machine no
	 */
	str	r1, [r5]			@ Save machine type
	/* __atags_pointer = DTB 物理地址 */
	str	r2, [r6]			@ Save atags pointer
	cmp	r7, #0
	/* cr_alignment = CP15 控制寄存器值 */
	strne	r0, [r7]			@ Save control register values
	b	start_kernel /* 跳转到 start_kernel() 执行 */
ENDPROC(__mmap_switched)
3.2.2.5 小结

到此,对于内核镜像线性映射临时页表的建立过程,已经全部完成:对于开启了 LPAE 的情形,建立的是 2级页表、2MB 粒度的内核镜像区的映射;对于没有开启 LPAE 的情形,建立的是 1级页表、1MB 粒度的内核镜像区的映射。同时,也配置了 BOOT CPUswapper 进程(系统首进程)内核栈,进入到了内核的 C 代码入口 start_kernel() 。下面用3张图、从两个不同的视角,简单的总结当前内核镜像线性临时映射的情况:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
页表 swapper_pg_dir 中建立了内核镜像区域的线性映射(从前面的 __create_page_tables 的代码分析,可以知道 swapper_pg_dir 是指向页表虚拟首地址的)。注意,swapper_pg_dir 页表可以映射整个4G虚拟地址空间,但当前仅建立了内核镜像部分线性映射的页表项

3.3 非 BOOT CPU: 启用 MMU ,复用 BOOT CPU 建立的 swapper_pg_dir 页表

在多核场景下,目前为止,只启用了 BOOT CPU 的 MMU ,而 非 BOOT CPU 仍然使用物理地址进行访问,所以还需要建立 非 BOOT CPU 的页表,然后开启它们的 MMU 。来看代码细节:

kernel_init()
	kernel_init_freeable()
		smp_init()
			cpu_up(cpu)
				do_cpu_up(cpu, CPUHP_ONLINE)
					_cpu_up(cpu, 0, target)
						...
						bringup_cpu()
							__cpu_up(cpu, idle)
/* arch/arm/kernel/smp.c */

int __cpu_up(unsigned int cpu, struct task_struct *idle)
{
	/* 配置 非 BOOT @cpu 的首进程 swapper 的内核栈空间 */
	secondary_data.stack = task_stack_page(idle) + THREAD_START_SP;

#ifdef CONFIG_MMU
	/* 
	 * 配置 非 BOOT CPU @cpu 首进程 swapper 的页表。
	 */
	/*
	 * idmap_pgd 由 BOOT CPU 创建: init_static_idmap()
	 * 供 非 BOOT CPU 在启动期间,用于 MMU 启用代码 __turn_mmu_on 的等同映射页表物理基址。
	 */
	secondary_data.pgdir = virt_to_phys(idmap_pgd);
	/* 系统中所有 CPU 的 swapper 进程共享同一个页表 swapper_pg_dir */
	secondary_data.swapper_pg_dir = get_arch_pgd(swapper_pg_dir); /* swapper_pg_dir 页表物理页框号 */
#endif
	sync_cache_w(&secondary_data); /* cache 同步 */

	ret = smp_ops.smp_boot_secondary(cpu, idle); /* arch/arm/kernel/head.S: secondary_startup */
	...
}
/* arch/arm/kernel/head.S */

ENTRY(secondary_startup)
	/*
	 * . 将 非 BOOT CPU 设为 SVC 模式
	 * . 禁用 非 BOOT CPU 的 IRQ + FIQ
	 * r9 是 非 BOOT CPU CPSR 寄存器新设定的值。
	 */
	safe_svcmode_maskall r9

	/*
	 * 读取处理器 ID 到 r9
	 * 参考 ARM Architecture Reference Manual.pdf, P687
	 */
	mrc	p15, 0, r9, c0, c0		@ get processor id
	/* 通过 r9 中的处理器 ID ,查找对应的系统预定义的处理器信息数据 proc_info_list ,
	 * 从 r5 寄存器返回。
	 * r5: 处理器信息指针, 
	 *      类型为 struct proc_info_list *, 
	 *      定义在 arch/arm/include/asm/procinfo.h
	 */
	bl	__lookup_processor_type
	movs	r10, r5				@ invalid processor? /* r10: 处理器信息数据指针 */
	moveq	r0, #'p'			@ yes, error 'p'
 THUMB( it	eq )		@ force fixup-able long branch encoding
	beq	__error_p /* 出错,没找到匹配的处理器数据,系统将进入死循环 */

	/*
	 * 需要知道的是,MMU 是每 CPU 的内存管理硬件,包括 TTBRx 也是每 CPU 的。
	 * 所以即使 BOOT CPU 已经开启了 MMU ,但 非 BOOT CPU 仍然是处于 MMU
	 * 关闭状态,所以仍然以物理地址访问内存。
	 */

	/*
	 * Use the page tables supplied from  __cpu_up.
	 */
	adr	r4, __secondary_data /* r4 = __secondary_data 的当前物理地址 */
	/*
	 * r5 = __secondary_data 的链接虚拟地址
	 * r7 = secondary_data 的链接虚拟地址
	 * r12 = __secondary_switched 的链接虚拟地址
	 */
	ldmia	r4, {r5, r7, r12}		@ address to jump to after
	/* lr = __secondary_data 的当前物理地址 - __secondary_data 的链接虚拟地址 */
	sub	lr, r4, r5			@ mmu has been enabled
	add	r3, r7, lr /* r3 = secondary_data 的物理地址 */
	/*
	 * 非 BOOT CPU 启动过程中,设置了
	 * . secondary_data.stack (非 BOOT CPU 首进程 swapper 的内核栈)
	 * . secondary_data.pgdir
	 * . secondary_data.swapper_pg_dir
	 *
	 * __cpu_up()
	 *		// 配置 @cpu 的首进程 swapper 的内核栈空间
	 *		secondary_data.stack = task_stack_page(idle) + THREAD_START_SP;
	 * #ifdef CONFIG_MMU
	 * 		// idmap_pgd 由 BOOT CPU 创建: init_static_idmap()
	 * 		// 供 非 BOOT CPU 在启动启用期间,用于 MMU 启用代码的等同映射。
	 *		secondary_data.pgdir = virt_to_phys(idmap_pgd);
	 *		// 系统中所有 CPU 的 swapper 进程共享同一个页表 swapper_pg_dir
	 *		secondary_data.swapper_pg_dir = get_arch_pgd(swapper_pg_dir); // swapper_pg_dir 页表物理页框号
	 * #endif
	 *		...
	 *		secondary_startup // 进入此处
	 *
	 * r4 = secondary_data.pgdir (用于 非 BOOT CPU 启用 MMU 代码 __turn_mmu_on 的等同映射页表的物理基址)
	 */
	ldrd	r4, [r3, #0]			@ get secondary_data.pgdir

	/* r8 = secondary_data.swapper_pg_dir (swapper_pg_dir 页表物理页框号) */
	ldr	r8, [r3, #8]			@ get secondary_data.swapper_pg_dir
	/* 将 CPU 初始化接口(如 __v7_ca7mp_setup)的调用返回地址设置为 __enable_mmu */
	badr	lr, __enable_mmu		@ return address
	/* r13 = __secondary_switched 的链接虚拟地址, __enable_mmu 后跳转到此处执行 */
	mov	r13, r12			@ __secondary_switched address
	/*
	 * r10 = proc_info_list *
	 * r12 = proc_info_list::__cpu_flush 其值为: 
	 *       &proc_info_list::proc->_proc_init - &proc_info_list
	 *       详见 proc-v7.S 中 __v7_proc 定义 和 proc-macros.S 中 initfn 定义。
	 */
	ldr	r12, [r10, #PROCINFO_INITFUNC]
	/* r12 = CPU 初始化函数 &proc_info_list::proc->_proc_init 指针 
	 * 如 __v7_ca7mp_setup (arch/arm/mm/proc-v7.S)
	 */
	add	r12, r12, r10			@ initialise processor
						@ (return control reg)
	ret	r12 /* 跳转到 __v7_ca7mp_setup(), 然后返回到 __enable_mmu 处继续执行 */
ENDPROC(secondary_startup)

/* 非 BOOT CPU 启用 MMU */
__enable_mmu:
	...
	/* 
	 * 设置第1级页表基址。
	 * 细节参考 DDI0406C_d_armv7ar_arm.pdf, P1724
	 */
#ifdef CONFIG_ARM_LPAE
	/*
	 * 设置 非 BOOT CPU 的 页表首地址到 非 BOOT CPU 的 TTBR0 寄存器。
	 * r4 = 第1级页表基址 low word
	 * r5 = 第1级页表基址 high word
	 * 将 r4,r5 中的第1级页表基址写到 64-bit 寄存器 TTBR0 (LPAE 开启,TTBRx 为 64-bit)
	 */
	mcrr	p15, 0, r4, r5, c2		@ load TTBR0
#else
	/* 设置域访问控制 */
	mov	r5, #DACR_INIT
	mcr	p15, 0, r5, c3, c0, 0		@ load domain access register
	/*
	 * r4 = 第1级页表基址
	 * 将 r4 中的第1级页表基址写到 32-bit 寄存器 TTBR0 (LPAE 关闭,TTBRx 为 32-bit)
	 */
	mcr	p15, 0, r4, c2, c0, 0		@ load page table pointer
#endif
	b	__turn_mmu_on /* 在 identity mapping 代码中开启 MMU */
ENDPROC(__enable_mmu)

	.align	5
	.pushsection	.idmap.text, "ax"
ENTRY(__turn_mmu_on) /* 在等同映射下启用 非 BOOT CPU 的 MMU 开关 */
mov	r0, r0
	instr_sync
	/* 启用 当前 CPU 的 MMU */
	mcr	p15, 0, r0, c1, c0, 0		@ write control reg
	mrc	p15, 0, r3, c0, c0, 0		@ read id reg
	instr_sync
	mov	r3, r3
	/*
	 * BOOT CPU: r13 = __mmap_switched
	 * 非 BOOT CPU: r13 = __secondary_switched 的链接虚拟地址
	 */
	mov	r3, r13
	/*
	 * BOOT CPU: 返回到 __mmap_switched 处
	 * 非 BOOT CPU: 返回到 __secondary_switched 处 
	 */
	ret	r3
__turn_mmu_on_end:
ENDPROC(__turn_mmu_on)

	/*
	 * r6  = &secondary_data
	 */
ENTRY(__secondary_switched)
	ldr	sp, [r7, #12]			@ get secondary_data.stack
	mov	fp, #0
	b	secondary_start_kernel /* 跳转到 secondary_start_kernel() */
ENDPROC(__secondary_switched)

到此,非 BOOT CPU 的页表也建立好了,它们复用了 BOOT CPU 建立的 swapper_pg_dir 页表,然后也进入使能 MMU 状态。更多关于非 BOOT CPU 的启动流程细节可参考 Linux: 多核CPU启动流程简析

4. 参考资料

《ARM Architecture Reference Manual.pdf》
《DDI0406C_d_armv7ar_arm.pdf》

下一篇:Linux内存管理 (2):memblock 子系统的建立

  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值