复习下kernel内存管理相关内容,顺便写个内存管理的系列文章做记录。
U-Boot跳转到内核时候MMU是关闭的,所以在内核boot阶段需要开启MMU,开MMU之前就需要初始化页表。
boot阶段的页表创建是通过函数__create_page_table来完成的,代码位于arch/arm/kernel/head.S中
##基本宏变量
- PAGE_OFFSET
这个变量一般被定义为0xC000 0000, 但在tiny4412平台上被定义为0x8000 0000。这个地址表示内核在虚拟地址空间中的起始地址。 - TEXT_OFFSET
定义在arch/arm/Makefile中,具体数值为:0x00008000。内核相对起始地址的偏移,也就是真实内核并
不是从起始地址0x8000 0000开始,而是从0x8000 0000 + 0x0000 8000开始. - PG_DIR_SIZE
size = 0x4000 = 16KB - 内核链接地址
-ld在链接过程还指定了一个链接地址,具体摘vmlinux.lds中。
518 . = 0x80000000 + 0x00008000;
519 .head.text : {
520 _text = .;
521 *(.head.text)
522 }
可以发现链接的时候,就是虚拟起始地址(PAGE_OFFSET) + text偏移(TEXT_OFFSET),和代码中设置的一样了。
##页表初始化
在正式执行页表初始化之前,内核会首先初始化两个寄存器r4 & r8
108 #ifndef CONFIG_XIP_KERNEL
109 adr r3, 2f
110 ldmia r3, {r4, r8}
111 sub r4, r3, r4 @ (PHYS_OFFSET - PAGE_OFFSET)
112 add r8, r8, r4 @ PHYS_OFFSET
113 #else
114 ldr r8, =PLAT_PHYS_OFFSET @ always constant in this case
115 #endif
... ...
147 #ifndef CONFIG_XIP_KERNEL
148 2: .long .
149 .long PAGE_OFFSET
150 #endif
因为adr命令获取的是相对PC的地址,并且此时MMU是关闭的所以PC对应的就是物理地址。
所以r3通过adr命令取到的也是物理地址。
r4通过ldmia命令拿到的是链接地址。
链接地址在vmlinux.lds中已经指定,是从0x8000 0000开始。
所以sub r4, r3, r4 得到的是物理地址和虚拟地址之间的OFFSET,这个虚拟地址也是开启MMU之后的运行地址。
最终,
r4 存放的是kernel当前运行的物理地址和开启MMU之后kernel运行的虚拟地址之间的OFFSET。
r8 存放的是kernel当前运行的物理起始地址不含TEXT_OFFSET。
接着正式分析页表的创建过程。
54 .macro pgtbl, rd, phys
55 add \rd, \phys, #TEXT_OFFSET
56 sub \rd, \rd, #PG_DIR_SIZE
57 .endm
163 __create_page_tables:
164 pgtbl r4, r8 @ page table address
165
166 /*
167 * Clear the swapper page table
168 */
169 mov r0, r4
170 mov r3, #0
171 add r6, r0, #PG_DIR_SIZE
172 1: str r3, [r0], #4
173 str r3, [r0], #4
174 str r3, [r0], #4
175 str r3, [r0], #4
176 teq r0, r6
177 bne 1b
可以看到在,pgtbl就是设置页表的起始地址,并将其存放在r4寄存器中,页表位于TEXT_OFFSET之前。最后通过循环初始化这块RAM。
##页表填充
页表初始化就可以准备填充了。kernel首先会将函数__turn_mmu_on和__turn_mmu_on_end之间的内容填入页表。正好也可以通过这个函数管中窥豹,了解下具体转换过程。
- 段映射
kernel在boot阶段使用的页表是通过段映射实现的,也就是按照1M为单位,将4GB的地址空间写入到16KB大小的页表中。一项需要4Byte,16KB可以提供4KB项页表条目。4KB条目,每条对应1M,所以16KB的页表正好能放下4GB的地址空间。 - 页表项举例
例如,我们打算给第1MB和第2MB填充到页表中
0x0000 0000 ~ 0x 000F FFFF [0M ~ 1M)
0x00010 0000 ~ 0x 001F FFFF [1M~2M)
因为页表起始地址是0x8000 4000,每一个条目占4Byte,所以页表形式应该如下:
0x8000 4000 --> 0M~1M (0x000X XXXX)
0x8000 4004 --> 1M~2M (0x001X XXXX)
0x8000 4008 --> 2M~3M (0x002X XXXX)
第1 MB右移20bit之后基址是0x000,直接存放到页表基址。
第 2MB 右移20bit之后变成0x001,怎么计算应该存放在哪个页表条目呢?
可以直接将基址<< 2然后加到页表基址,最后就变成0x8000 4000 + 1<<2 = 0x8000 4004
所以第N兆内存应该填入第(0x80004000 + N << 2)项。
因为是分段,段基址只占了12bit,剩余的20bit被MMU拿来存放一些flag,比如页表描述符访问权限/映射类型(分段or分页)等。
PS:MMU将虚拟地址转回物理地址的过程参考杜春雷《ARM体系结构与编程》 P180页。
有了这个基础之后再看代码就容易多了,代码就是按照这个思路写的。
207 ldr r7, [r10, #PROCINFO_MM_MMUFLAGS] @ mm_mmuflags
208
209 /*
210 * Create identity mapping to cater for __enable_mmu.
211 * This identity mapping will be removed by paging_init().
212 */
213 adr r0, __turn_mmu_on_loc
214 ldmia r0, {r3, r5, r6}
215 sub r0, r0, r3 @ virt->phys offset
216 add r5, r5, r0 @ phys __turn_mmu_on
217 add r6, r6, r0 @ phys __turn_mmu_on_end
218 mov r5, r5, lsr #SECTION_SHIFT
219 mov r6, r6, lsr #SECTION_SHIFT
220
221 1: orr r3, r7, r5, lsl #SECTION_SHIFT @ flags + kernel base
222 str r3, [r4, r5, lsl #PMD_ORDER] @ identity mapping
223 cmp r5, r6
224 addlo r5, r5, #1 @ next section
225 blo 1b
这边只是把__turn__mmu_on到__turn_mmu_on_end代码之间的地址填入页表,这么点代码肯定可以填满一个页表项,所以这里只进行一次循环就够了。
接着就是将整个内核image所用的物理地址空间全部初始化到页表项中。
244 /*
245 * Map our RAM from the start to the end of the kernel .bss section.
246 */
247 add r0, r4, #PAGE_OFFSET >> (SECTION_SHIFT - PMD_ORDER)
248 ldr r6, =(_end - 1)
249 orr r3, r8, r7
250 add r6, r4, r6, lsr #(SECTION_SHIFT - PMD_ORDER)
251 1: str r3, [r0], #1 << PMD_ORDER
252 add r3, r3, #1 << SECTION_SHIFT
253 cmp r0, r6
254 bls 1b
这里_end符号可以在vmlinux.lds中找到,存放的是整个image链接结束地址。所以可以知道这段代码映射的是整个kernel image的地址空间。
映射过程和之前分析的是一样的,需要注意的是str 那条指令,这条指令执行完之后会更新r0寄存器的值。根据代码逻辑也能推出来必须更新r0,不然这个循环没法执行 — —!
##开启MMU
页表映射好了,接着就可以准备开启MMU了。r4寄存器中存放的是页表的基址。
在正式开启MMU之前还需要对MMU进行一些设置。源码注释中已经提到MMU的初始化设置操作是在arch/arm/mm/proc-*.S中。如果是exynos4412,对应的就是proc-v7.S文件。MMU寄存器初始化主要包括设置页表基址到MMU c2寄存器,设置MMU控制寄存器的flag,清理cache等。最后跳转到__enbale_mmu。
130 /*
131 * The following calls CPU specific code in a position independent
132 * manner. See arch/arm/mm/proc-*.S for details. r10 = base of
133 * xxx_proc_info structure selected by __lookup_processor_type
134 * above. On return, the CPU will be ready for the MMU to be
135 * turned on, and r0 will hold the CPU control register value.
136 */
137 ldr r13, =__mmap_switched @ address to jump to after
138 @ mmu has been enabled
139 adr lr, BSYM(1f) @ return (PIC) address
140 mov r8, r4 @ set TTBR1 to swapper_pg_dir
141 ldr r12, [r10, #PROCINFO_INITFUNC]
142 add r12, r12, r10
143 ret r12
144 1: b __enable_mmu
__enable_mmu的过程也比较简单,设置MMU一些和domain控制相关的flags,然后是能MMU。使能之后便跳转到了__mmap_switched函数。
mmap_switch回去清下bss以及重定位data段,最后便跳转到了C代码的入口start_kernel函数。
需要注意的是,此页表只是在初始化阶段用到。内核稍后会开启分页机制。。。