引言:迫于对于Linux新版本内存管理的渴望,我开启了Linux新版本的游荡,Linux 0.11版本明显不够味,Linux 0.99以下均是如此,内存管理和文件目录架构均没有太大的变化,而市面上唯一找到的便属这本《ARM Linux内核源码剖析》了,在它的基础上,我在本篇对该版本的arm架构代码进行词词解析,对!就是词词解析!我觉得把他解析完我写一本书都不为过了!来!启程!

参考资料:

内核构建过程和ARM处理器

内核构建过程

即可启动二进制文件zImage的生成过程,通过内核初始化、内核配置、内核构建和内核安装过程。

# 内核初始化
make distclean
make mrproper
# 内核配置
kconfig
make menuconfig, gconfig, xconfig
# 内核构建 vmlinux, head.S, misc.S => zImage
# vmlinux->gzip->piggy.gz->piggy.o 内核文件 head.S 内核初始化程序 misc.S 解压缩
kbuild
make all, zImage, modules
# 内核安装
make install, modules_install

为何要压缩成zImage文件?由于嵌入式系统具有的资源十分有限,为了提高这种环境下内核内存的负载率和执行效率,使用压缩后的内核二进制文件zImage

ARM处理器

ARM的意思是Advanced RISC Machine。ARM处理器以Berkeley RISC架构为基础。RISC ( Reduced Instruction Set Computer,精简指令系统计算机)比CISC ( Complex Instruction Set Computer,复杂指令系统计算机)指令结构更简单,可在一小时内快速处理指令。ARM采用RISC方式降低指令复杂度,利用管道提高指令处理速度,由此提升性能。ARM中适用的RISC特征如下。

  • 指令:ARM使用的指令相对较少。这些指令将提供一个循环周期内能够执行的简单指令通过具有一定长度的指令可实现管道。
  • 管道:指令在管道中是并列执行的。通过管道执行时,一边解码(decode)当前指令,一边获取( fetch )下一指令。
  • 负载存储((load-store)结构:ARM处理器会执行寄存器储存的指令。将内存中的数据读取到寄存器时使用load命令,将寄存器中的数据输入内存时使用store命令。

处理器架构和核心

ARM主要架构如下

  • ARMv4架构:使用32位地址区域,可运行32位的ISA ( Instruction Set Architecture,指令集架构)。ARMv4T架构还具有16位Thumb指令包。
  • ARMv5TE架构:向ARMISA添加了已改善的Thumb架构和Enhanced DSP指令包。它将改善ARM/Thumb彼此间的运行,包含了程序兼容,提高了性能。
  • ARMv6架构:改善了对内存系统、异常处理、多进程环境的支持等内容,并包含了支持运行SIMD ( Single Instruction Multiple Data,单指令多数据)的媒体指令。
  • ARMv7-A架构:支持Linux、Linux第三方( Montavista、QNX、风河系统)、Symbian ,Windows CE等大部分操作系统。具有ARM、Thumb、Thumb-2、Jazelle、DSP指令包,并包含Advanced SIMD扩展指令——NEON的软件多媒体处理功能。

命名规则

符号 意义
x 处理器族
y MMU/MPU
z 缓存
T Thumb 16位指令解码器
D JTAG调试器
M 扩展高速乘子
l 嵌入式ICE微极化池( microcell)
E DSP扩展指令
J Jazelle Java加速功能
F 浮点数处理装置
s 综合版本

ARM处理器架构中,ARM7采用冯·诺依曼体系结构,ARM9采用修正后的哈佛体系结构

处理器内部结构

ARM处理器的运行模式如下

  • User模式:执行普通用户应用程序时的处理器运行模式
  • System模式:使用与User模式相同的寄存器,但可完全读写CPSR ( Current Program StatusRegister,当前程序状态寄存器)的特殊模式。
  • FIQ(Fast Interrupt Request,快速中断请求)模式:为处理快速中断而执行的处理器运行模式。
  • IRQ (Interrupt Request,中断请求)模式:为处理通用中断而执行的处理器运行模式。
  • SVC (Supervisor)模式:操作系统内核运行的通用处理器运行模式,发生Reset或软件中断(sWI)时执行。
  • Abort模式:内存访问失败时执行的处理器运行模式。
  • Undefined模式:处理器要执行未定义的指令时执行的处理器运行模式。

几个特殊寄存器

  • r13:作为栈指针(sp)使用,存储当前处理器模式的栈顶端地址值。
  • r14:作为链接寄存器( lr )使用,储存调用子程序时的返回地址。
  • r15:处理器读取指令后,通过程序计数器( pc)储存下一指令地址。
  • CPSRSPSR:程序状态保存寄存器,其中CPSR显示程序执行时的状态,CPSR显示程序执行时的状态。

处理器异常

处理器发生异常或中断时,在程序计数器( pc)中输入特定存储器地址以处理异常/中断,位置的指令处理器将中断当前执行的程序,并读取向量表中相应

在处理器中可将向量表设置为高位( high )地址使用,微软操作系统即具备该特征。设置为Low向量表时,分为0x00000000;设置为High向量表时,分为0xFFFFO000。

异常分类

  • Reset:接通电源时首次使用的位置。
  • Undefined Instruction:执行未定义指令时使用。
  • Software Interrupt:执行SWI指令时调用,并作为系统核心使用。
  • Prefetch Abort:发生于无法读取内存指令时。
  • Data Abort:发生于无法读写数据内存时。
  • IRQ:为中断处理器当前执行流程,由外部硬件使用。
  • FIQ:和IRQ类似,但由需要快速响应时间的硬件使用。

硬件拓展功能

缓存
内存管理
协处理器:CP15,控制缓存和MMU

内核的启动

head.S文件(因为我的逻辑是按照ARM版本解析的,且有先后次序,所以复制的顺序和结构可能会和源码不一致,望见谅)

进入启动加载后结束首个启动-start标签

start:
		b	1f @跳转指令,跳转到标号1的位置执行
		...
1:		mov	r7, r1			@ r7保存架构ID
		mov	r8, r2			@ r8保存atags信息
		...
#ifndef __ARM_ARCH_2__   @ ARM 2
		...
		mrs	r2, cpsr		@ 读状态寄存器指令 cpsr为状态寄存器
		tst	r2, #3			@ 比较指令 是否为user模式
		bne	not_angel       @ 跳转指令
		...
not_angel:                        @ 禁用中断
		mrs	r2, cpsr		      @ 读状态寄存器指令 cpsr为状态寄存器
		orr	r2, r2, #0xc0		  @ 将0xc0写入到r2寄存器,即1100 00007位和8位设置关闭FIQ和IRQ
		msr	cpsr_c, r2            @ 反映到cpsr并关闭中断
		...
#else  @ARM 3
		teqp	pc, #0x0c000003		@ 关闭中断

		.text
		adr	r0, LC0
		ldmia	r0, {r1, r2, r3, r4, r5, r6, ip, sp} @ 将r0指向的地址的多个字赋值给后面的寄存器
		subs	r0, r0, r1		@ calculate the delta offset

						@ if delta is zero, we are
		beq	not_relocated		@ 跳转指令
						@ were linked at.
						
LC0:		.word	LC0			@ r1
		.word	__bss_start		@ r2
		.word	_end			@ r3
		.word	zreladdr		@ r4
		.word	_start			@ r5
		.word	_got_start		@ r6
		.word	_got_end		@ ip
		.word	user_stack+4096		@ sp

BSS系统域初始化-not_relocated标签

解压zImage这一压缩内核的准备工作有:初始化BSS区域,激活缓存以及设置动态内存区域。

not_relocated:	
		mov	r0, #0
1:		str	r0, [r2], #4		@ clear bss str为将寄存器r0的数据保存到内存[r2]+4
		str	r0, [r2], #4
		str	r0, [r2], #4
		str	r0, [r2], #4
		cmp	r2, r3    @ 因为bss位于__bss_start到__end之间,前面对bss区域进行初始化为0,此处检验是否整个bss段全部0
		blo	1b

		bl	cache_on  @ 激活缓存,将用于解压的动态内存空间的起始地址user_stack+4KB和最终地址user_stack+4KB+64KB存入寄存器r1和r2,至此完成解压内核的所有准备

		mov	r1, sp			    @ malloc space above stack  分配栈空间位置
		add	r2, sp, #0x10000	@ 64k max 分配栈空间大小

		@ 检查是否覆盖
		cmp	r4, r2
		bhs	wont_overwrite
		sub	r3, sp, r5		    @ > compressed kernel size
		add	r0, r4, r3, lsl #2	@ allow for 4x expansion
		cmp	r0, r5
		bls	wont_overwrite
		
cache_on:	
		mov	r3, #8			@ 将常数8存入寄存器r3
		b	call_cache_fn   @ 跳转指令
		 
call_cache_fn:	
		adr	r12, proc_types  @ 特殊结构,用r12指向pro_types
#ifdef CONFIG_CPU_CP15       @ 协处理器coprocessor1 CP15是否存在
		@ p15指令操作的协处理器名,一般是p0~p15
		@ 0为为特定操作码
		@ r6为目的寄存器
		@ c0为存放第一个操作数的协处理器
		@ c0为存放第二个操作数的协处理器
		mrc	p15, 0, r6, c0, c0	@ 获得处理器 ID ,mrc指令为协处理器到ARM寄存器到的数据传送指令
#else
		ldr	r6, =CONFIG_PROCESSOR_ID @否则从宏定义获取处理器ID
#endif
1:		ldr	r1, [r12, #0]		@ 处理器ID
		ldr	r2, [r12, #4]		@ 得到mask码
		eor	r1, r1, r6		    @ (real ^ match)  异或运算
		tst	r1, r2			    @       & mask 进行比较
		addeq	pc, r12, r3		@ call cache function 如果cpsr的z位为1,则将pc=r12+r3 r3前面赋值为8,用作偏移量
		add	r12, r12, #4*5      @ 如果不一致, 在r12加上20,一直比较直到找到该处理器型号
		b	1b                  @ 跳转指令,进行循环调用,直到找到符合该版本的缓存指令

proc_types:  @ 该结构管理打开缓存(cache_on),清除缓存(cache flush),关闭缓存(cache off)的功能,这些功能根据`ARM`处理器的版本而不同,常数8用作偏移量,指向proc_types列表各项的“打开缓存”子程序的起始地址
		.word	0x41560600		@ ARM6/610
		.word	0xffffffe0
		b	__arm6_mmu_cache_off	@ works, but slow
		b	__arm6_mmu_cache_off
		mov	pc, lr
@		b	__arm6_mmu_cache_on		@ untested
@		b	__arm6_mmu_cache_off
@		b	__armv3_mmu_cache_flush

		.word	0x00000000		@ old ARM ID
		.word	0x0000f000
		mov	pc, lr
		mov	pc, lr
		mov	pc, lr

		.word	0x41007000		@ ARM7/710
		.word	0xfff8fe00
		b	__arm7_mmu_cache_off
		b	__arm7_mmu_cache_off
		mov	pc, lr

__armv4_mmu_cache_on:
		mov	r12, lr      @ 将返回地址存入r12
		bl	__setup_mmu  @跳转指令到__setup_mmu
		mov	r0, #0       @r0寄存器清0
		mcr	p15, 0, r0, c7, c10, 4	@ drain write buffer 将写缓冲的内容更新到内存
		mcr	p15, 0, r0, c8, c7, 0	@ flush I,D TLBs  清除指令缓存 数据缓存和TLB
		mrc	p15, 0, r0, c1, c0, 0	@ read control reg  读控制寄存器

		orr	r0, r0, #0x5000		@ I-cache enable, RR cache replacement 设置控制寄存器的指令缓存激活位,round robin缓存交替策略激活位
		orr	r0, r0, #0x0030

		bl	__common_mmu_cache_on  @ 跳转指令

		mov	r0, #0 @ r0寄存器清0
		mcr	p15, 0, r0, c8, c7, 0	@ flush I,D TLBs 清除指令缓存 数据缓存和TLB
		
		mov	pc, r12 #并返回

__setup_mmu:	                @用于初始化解压内核所需的页目录项
		sub	r3, r4, #16384		@ Page directory size r4-16384
		bic	r3, r3, #0xff		@ Align the pointer 位清除指令 r3& (0xff的反码)
		bic	r3, r3, #0x3f00     

		mov	r0, r3 @r3=r0      @ r3=0x50004000,这是zImage加载的物理地址 16KB,也是页目录的起始地址

		mov	r9, r0, lsr #18
		mov	r9, r9, lsl #18		@ start of RAM
		add	r10, r9, #0x10000000	@ a reasonable RAM size
		mov	r1, #0x12
		orr	r1, r1, #3 << 10
		add	r2, r3, #16384
1:		cmp	r1, r9			@ if virt > start of RAM
		orrhs	r1, r1, #0x0c		@ set cacheable, bufferable
		cmp	r1, r10			@ if virt > end of RAM
		bichs	r1, r1, #0x0c		@ clear cacheable, bufferable
		str	r1, [r0], #4		@ 1:1 mapping
		add	r1, r1, #1048576
		teq	r0, r2
		bne	1b
		
		mov	r1, #0x1e
		orr	r1, r1, #3 << 10
		mov	r2, pc, lsr #20
		orr	r1, r1, r2, lsl #20
		add	r0, r3, r2, lsl #2
		str	r1, [r0], #4
		add	r1, r1, #1048576
		str	r1, [r0]
		mov	pc, lr
ENDPROC(__setup_mmu)

__common_mmu_cache_on: @指令缓存激活及缓存策略适用
#ifndef DEBUG
		orr	r0, r0, #0x000d		@ Write buffer, mmu
#endif
		mov	r1, #-1 @r1=-1
		mcr	p15, 0, r3, c2, c0, 0	@ load page table pointer 将r3内保存的页目录地址值村润cp15的TTBR
		mcr	p15, 0, r1, c3, c0, 0	@ load domain access control
		b	1f
		.align	5			@ cache line aligned
1:		mcr	p15, 0, r0, c1, c0, 0	@ load control register 循环设置缓存策略、指令缓存激活设置
		mrc	p15, 0, r0, c1, c0, 0	@ and read it back to
		sub	pc, lr, r0, lsr #32	@ properly flush pipeline

proc_type结构

  • CPU ID match
  • CPU ID mask
  • cache on subroutine
  • cache off subroutine
  • cache flush subroutine

cp15

对于zImage加载的物理地址各结构都不同,可以在arch/arm/$(MACH)/Makefile.boot中找到,如

# mach-aaec2000
zreladdr-y := 0xf0008000
# mach-at91
ifeq ($(CONFIG_ARCH_AT91CAP9),y)
   zreladdr-y	:= 0x70008000
params_phys-y	:= 0x70000100
initrd_phys-y	:= 0x70410000
else
   zreladdr-y	:= 0x20008000
params_phys-y	:= 0x20000100
initrd_phys-y	:= 0x20410000
endif

__setup_mmu中将页目录中的4096项进行初始化,将访问权限设为可读/写,而且,对其中256项设置为cacheablebufferable(页目录以1MB管理内存单元)

我们之前通过not_relocatedcache_on做好了解压之前的准备工作:初始化BSS区域、设置动态内存区域、打开缓存等。现在正式对压缩后的内核映像zlmage进行解压,并为内核初始化进行设置。

解压内核并避免覆盖-wont_overwrite

@ 检查是否覆盖
		cmp	r4, r2
		bhs	wont_overwrite
		sub	r3, sp, r5		@ > compressed kernel size
		add	r0, r4, r3, lsl #2	@ allow for 4x expansion
		cmp	r0, r5
		bls	wont_overwrite 

wont_overwrite:	 @解压内核并避免覆盖
		mov	r0, r4            @ r0为内核起始地址
		mov	r3, r7            @ r3=r7 设备ID
		bl	decompress_kernel @ 调用解压内核的子程序
		b	call_kernel       @ 没有问题的话直接跳转到call_kernel指令

call_kernel:	
		bl	cache_clean_flush @ 刷新缓存 根据处理器ID找到相关处理器型号的缓存框架即可
		bl	cache_off         @ 缓存禁用 同上
		mov	r0, #0			  @ must be zero r0=0
		mov	r1, r7			  @ restore architecture number r1=r7
		mov	r2, r8			  @ restore atags pointer r2=r8
		mov	pc, r4			  @ call kernel pc=r4

ulg
decompress_kernel(ulg output_start, ulg free_mem_ptr_p, ulg free_mem_ptr_end_p,
		  int arch_id)
{
	output_data		= (uch *)output_start;	/* Points to kernel start */
	free_mem_ptr		= free_mem_ptr_p;
	free_mem_end_ptr	= free_mem_ptr_end_p;
	__machine_arch_type	= arch_id;

	arch_decomp_setup();

	makecrc();
	putstr("Uncompressing Linux...");
	gunzip();
	putstr(" done, booting the kernel.\n");
	return output_ptr;
}

cache_off:	mov	r3, #12			@ cache_off function
		b	call_cache_fn

cache_clean_flush:  @ 缓存清理
		mov	r3, #16
		b	call_cache_fn

call_cache_fn:	
		adr	r12, proc_types  @特殊结构
#ifdef CONFIG_CPU_CP15  @协处理器coprocessor1 CP15是否存在
		@ p15指令操作的协处理器名,一般是p0~p15
		@ 0为为特定操作码
		@ r6为目的寄存器
		@ c0为存放第一个操作数的协处理器
		@ c0为存放第二个操作数的协处理器
		mrc	p15, 0, r6, c0, c0	@ 获得处理器 ID ,mrc指令为协处理器到ARM寄存器到的数据传送指令
#else
		ldr	r6, =CONFIG_PROCESSOR_ID @否则从宏定义获取处理器ID
#endif
1:		ldr	r1, [r12, #0]		@ 处理器ID
		ldr	r2, [r12, #4]		@ 得到mask码
		eor	r1, r1, r6		    @ (real ^ match)  异或运算
		tst	r1, r2			    @       & mask 进行比较
		addeq	pc, r12, r3		@ call cache function 如果cpsr的z位为1,则将pc=r12+r3 r3前面赋值为8,用作偏移量
		add	r12, r12, #4*5      @ 如果不一致, 在r12加上20
		b	1b                  @ 跳转指令,进行循环调用,直到找到符合该版本的缓存指令

内核的初始化

通过引导加载项加载内核后,首先执行的部分就是stext,执行该标签时要求如下状态,此时在stext中,首先转换为SVC模式,并禁用IRQ。然后调用多个检查程序,查找处理器和机器信息,并检查atag信息,朱家设置页表后启动MMU

MMU = off
D_Cache = off
r0 = 0
r1 = machine number
r2 = stags pointer
ENTRY(stext)
	msr	cpsr_c, #PSR_F_BIT | PSR_I_BIT | SVC_MODE         @ ensure svc mode 写状态寄存器指令 转换成SVC模式,禁用IRQ
						                                  @ and irqs disabled
	mrc	p15, 0, r9, c0, c0		                          @ get processor id 得到处理器ID
	bl	__lookup_processor_type		                      @ r5=procinfo r9=cpuid 寻找CPU信息
	movs	r10, r5				                          @ invalid processor (r5=0)?  检查CPU信息是否找到的程序
	beq	__error_p			                              @ yes, error 'p' 
	bl	__lookup_machine_type		                      @ r5=machinfo 寻找机器信息
	movs	r8, r5				                          @ invalid machine (r5=0)?
	beq	__error_a			                              @ yes, error 'a' 检查机器信息是否找到的程序
	bl	__vet_atags                                       @ 检查atag信息
	bl	__create_page_tables  
	
	@ 对虚拟内存内存创建
	ldr	r13, __switch_data		     @ r13=__switch_data
						             @ mmu has been enabled
	adr	lr, __enable_mmu		     @ lr=__enable_mmu
	add	pc, r10, #PROCINFO_INITFUNC  @ 进程初始化函数 r10存储了proc_info_list结构体的起始地址,加上PROCINFO_INITFUNC可调用__v6_setup
	                                 @ proc_info_list结构体保存着处理器信息,一般通过proc_info_begin和proc_info_end得到
ENDPROC(stext)

为了让内核正常运行,必须把握内核要执行的处理器信息和机器信息。

__lookup_processor_type:       @ 此时要查找的处理器的CPU ID保存至r9中
	adr	r3, 3f                 @ r3=3f 
	ldmda	r3, {r5 - r7}      @ ldmda r3, {r5 - r7}意思是r3指示的内存数据依次加载到寄存器r7,r6,r5中去 r7=. r6=__proc_info_end,r5=__proc_info_begin
	sub	r3, r3, r7			   @ 得到__proc_info_end和__proc_info_begin的物理地址
	add	r5, r5, r3			
	add	r6, r6, r3			
1:	ldmia	r5, {r3, r4}	   @ __proc_info_begin到__proc_info_end保存着所有的处理器信息,循环依次输入,用proc_info_list结构体保存
	and	r4, r4, r9			   @ 并比较cpu_val和CPU ID的值
	teq	r3, r4
	beq	2f
	add	r5, r5, #PROC_INFO_SZ		
	cmp	r5, r6
	blo	1b
	mov	r5, #0				
2:	mov	pc, lr
ENDPROC(__lookup_processor_type)   @ 注册到符号表

ENTRY(lookup_processor_type) @ 查找处理器类型的汇编代码的C API版本
	stmfd	sp!, {r4 - r7, r9, lr}
	mov	r9, r0
	bl	__lookup_processor_type
	mov	r0, r5
	ldmfd	sp!, {r4 - r7, r9, pc}
ENDPROC(lookup_processor_type)

	.long	__proc_info_begin
	.long	__proc_info_end
3:	.long	.
	.long	__arch_info_begin
	.long	__arch_info_end

在MMU禁用状态下将虚拟地址转换为物理地址。此时禁用了MMU。无法通过虚拟地址访问内存,但是,保存处理器信息的区域地址__proc_info_begin__proc_info_end是 编译内核指定的,且都是虚拟地址,因此,只有将这些虚拟地址变更为物理地址,才能访问具有处理器信息的proc_info_list结构体

sub r3, r3, r7
add r5, r5, r3
add r6, r6, r3

r3 <- label 3 物理地址
r5 <- __proc_info_begin   虚拟地址
r6 <- __proc_info_end     虚拟地址
r7 <- .(location counter) 虚拟地址

对虚拟内存进行创建:对KERNEL_RAM_PADDR相距0x4000的位置(KERNEL_RAM_PADDR-0x4000)到KERNEL_RAM_PADDR的所有页表项执行循环,并初始化为0,对相当于内核区域的项设置节区基址和cacheablebufferable值。

KERNEL_RAM_PADDR=0x50008000

__create_page_tables:
	pgtbl	r4		     @ page table address 页表起始地址

	mov	r0, r4           @ r0=0x50004000
	mov	r3, #0           @ r3=0
	add	r6, r0, #0x4000  @ r6=0x50004000+0x4000=0x50008000
1:	str	r3, [r0], #4     @ 0x50004000=0
	str	r3, [r0], #4     @ 0x50004004=0
	str	r3, [r0], #4     @ 0x50004008=0
	str	r3, [r0], #4     @ 0x5000400f=0
	teq	r0, r6           @ 全部初始化为0
	bne	1b

	ldr	r7, [r10, #PROCINFO_MM_MMUFLAGS] @ r10保存着proc_info_list的地址,令r7=_cpu_mm_mmu_flag 

	mov	r6, pc, lsr #20			@ start of kernel section
	orr	r3, r7, r6, lsl #20		@ flags + kernel base
	str	r3, [r4, r6, lsl #2]		@ identity mapping
	@ KERNEL_START = 0xc0008000 KERNEL_END = _end
	add	r0, r4,  #(KERNEL_START & 0xff000000) >> 18  @r0=r4+0x3000
	str	r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]! @[r0 + 0]
	ldr	r6, =(KERNEL_END - 1)

	add	r0, r0, #4
	add	r6, r4, r6, lsr #18
1:	cmp	r0, r6
	add	r3, r3, #1 << 20
	strls	r3, [r0], #4
	bls	1b

	mov	pc, lr
ENDPROC(__create_page_tables)

__v6_setup设置核心

运行完__create_page_tables设置后,运行__v6_setup程序设置当前处理器。

根据不同的ARM架构,处理器初始化函数执行过程也不一样,v6时调用__v6_setupv7时调用__v7_setup,调用后即执行初始化任务。

打开MMU并使用虚拟地址-__enable_mmu/__trun_mmu_on

运行完__v6_setup之后,依次运行__enable_mmu__trun_mmu_on

为了控制MMU的运行,使用协处理器15的各个寄存器,控制寄存器c1寄存器将履行MMU系统控制中默认寄存器的作用

__enable_mmu:
	mov	r5, #(domain_val(DOMAIN_USER, DOMAIN_MANAGER) | \
		      domain_val(DOMAIN_KERNEL, DOMAIN_MANAGER) | \
		      domain_val(DOMAIN_TABLE, DOMAIN_MANAGER) | \
		      domain_val(DOMAIN_IO, DOMAIN_CLIENT))
	mcr	p15, 0, r5, c3, c0, 0		@ load domain access register
	mcr	p15, 0, r4, c2, c0, 0		@ load page table pointer
	b	__turn_mmu_on
ENDPROC(__enable_mmu)

__turn_mmu_on:
	mov	r0, r0
	mcr	p15, 0, r0, c1, c0, 0		@ write control reg
	mrc	p15, 0, r3, c0, c0, 0		@ read id reg
	mov	r3, r3
	mov	r3, r3
	mov	pc, r13
ENDPROC(__turn_mmu_on)

跳转到kernel_start-__mmap_switched

从此处开始,MMU处于激活状态。从kernel_start函数开始,代码由C语言编写而成,因此需要__switch_data标签中的databssstack值,并调用start_kernel后要使用到的信息设置到__switch_data

__switch_data:
	.long	__mmap_switched
	.long	__data_loc			@ r4
	.long	_data				@ r5
	.long	__bss_start			@ r6
	.long	_end				@ r7
	.long	processor_id			@ r4
	.long	__machine_arch_type		@ r5
	.long	__atags_pointer			@ r6
	.long	cr_alignment			@ r7
	.long	init_thread_union + THREAD_START_SP @ sp
	
__mmap_switched:
	adr	r3, __switch_data + 4

	ldmia	r3!, {r4, r5, r6, r7}
	cmp	r4, r5				@ Copy data segment if needed
1:	cmpne	r5, r6
	ldrne	fp, [r4], #4
	strne	fp, [r5], #4
	bne	1b

	mov	fp, #0				@ Clear BSS (and zero fp)
1:	cmp	r6, r7
	strcc	fp, [r6],#4
	bcc	1b

	ldmia	r3, {r4, r5, r6, r7, sp}  @ 此处若执行start_kernel时,则运行静态声明的init线程,在sp寄存器中设置该线程的栈指针
	str	r9, [r4]			@ Save processor ID
	str	r1, [r5]			@ Save machine type
	str	r2, [r6]			@ Save atags pointer
	bic	r4, r0, #CR_A			@ Clear 'A' bit
	stmia	r7, {r0, r4}			@ Save control register values
	b	start_kernel   @ 开始运行
ENDPROC(__mmap_switched)

内核的执行

主要是start_kernel函数

asmlinkage void __init start_kernel(void)
{
	smp_setup_processor_id();
	
	lockdep_init();
	debug_objects_early_init();

	boot_init_stack_canary();

	cgroup_init_early();

	local_irq_disable();
	early_boot_irqs_off();
	early_init_irq_lock_class();

	lock_kernel();  // 大内核锁

	tick_init();  // 注册针对时钟事件的处理器
	
	boot_cpu_init();  // 在CPU位图中注册当前运行CPU
	page_address_init();  // 初始化高地址HIGHMEM管理
	
	setup_arch(&command_line);  // 设置架构
	mm_init_owner(&init_mm, &init_task);
	setup_command_line(command_line);
	setup_per_cpu_areas();
	setup_nr_cpu_ids();
	smp_prepare_boot_cpu();	

	sched_init();

	preempt_disable();
	build_all_zonelists();
	page_alloc_init();
	printk(KERN_NOTICE "Kernel command line: %s\n", boot_command_line);
	parse_early_param();
	parse_args("Booting kernel", static_command_line, __start___param,
		   __stop___param - __start___param,
		   &unknown_bootoption);
	if (!irqs_disabled()) {
		printk(KERN_WARNING "start_kernel(): bug: interrupts were "
				"enabled *very* early, fixing it\n");
		local_irq_disable();
	}
	sort_main_extable();
	trap_init();
	rcu_init();

	early_irq_init();
	init_IRQ();
	pidhash_init();
	init_timers();
	hrtimers_init();
	softirq_init();
	timekeeping_init();
	time_init();
	sched_clock_init();
	profile_init();
	if (!irqs_disabled())
		printk(KERN_CRIT "start_kernel(): bug: interrupts were "
				 "enabled early\n");
	early_boot_irqs_on();
	local_irq_enable();

	console_init();
	if (panic_later)
		panic(panic_later, panic_param);

	lockdep_info();

	locking_selftest();

#ifdef CONFIG_BLK_DEV_INITRD
	if (initrd_start && !initrd_below_start_ok &&
	    page_to_pfn(virt_to_page((void *)initrd_start)) < min_low_pfn) {
		printk(KERN_CRIT "initrd overwritten (0x%08lx < 0x%08lx) - "
		    "disabling it.\n",
		    page_to_pfn(virt_to_page((void *)initrd_start)),
		    min_low_pfn);
		initrd_start = 0;
	}
#endif
	vmalloc_init();
	vfs_caches_init_early();
	cpuset_init_early();
	page_cgroup_init();
	mem_init();
	enable_debug_pagealloc();
	cpu_hotplug_init();
	kmem_cache_init();
	kmemtrace_init();
	debug_objects_mem_init();
	idr_init_cache();
	setup_per_cpu_pageset();
	numa_policy_init();
	if (late_time_init)
		late_time_init();
	calibrate_delay();
	pidmap_init();
	pgtable_cache_init();
	prio_tree_init();
	anon_vma_init();
#ifdef CONFIG_X86
	if (efi_enabled)
		efi_enter_virtual_mode();
#endif
	thread_info_cache_init();
	cred_init();
	fork_init(num_physpages);
	proc_caches_init();
	buffer_init();
	key_init();
	security_init();
	vfs_caches_init(num_physpages);
	radix_tree_init();
	signals_init();

	page_writeback_init();
#ifdef CONFIG_PROC_FS
	proc_root_init();
#endif
	cgroup_init();
	cpuset_init();
	taskstats_init_early();
	delayacct_init();

	check_bugs();

	acpi_early_init(); 

	ftrace_init();
	
	rest_init();
}

一个个来看,此处我主要集中点在内存管理上。

smp_setup_processor_id();
lockdep_init();  // 用于调试
debug_objects_early_init();  // 用于调试

void __init __weak smp_setup_processor_id(void)
{
	// 不执行
}
// 栈溢出感应,在ARM中未实现
boot_init_stack_canary();
// 初始化提供进程集成方法的cgroup
cgroup_init_early();

// 禁用IRQ
local_irq_disable();
// 协助调试
early_boot_irqs_off();
early_init_irq_lock_class();
// 初始化大内核锁
lock_kernel();  
boot_cpu_init();  // 在CPU位图中注册当前运行CPU
page_address_init();  // 初始化高地址HIGHMEM管理

在包含热插拔信息的位图上添加执行init_task的CPU-boot_cpu_init()

内核中有位图,用来维护系统内CPU的状态信息,其中有cpu_possible_mapcpu_online_mapcpu_present_map,位与CPU 1:1映射。

  • cpu_possible_map:对系统中可执行当前热插拔的CPU的位图。该位图针对启动时系统支持的CPU数量,一旦设置了位,则不可添加或删除
  • cpu_online_map:将联机(使用中)的所有CPU的位设置位1的位图。若已完成从内核调度或设备接收中断的准备,则设置该位。包括中断在内的所有OS服务移动到其他CPU时,该位被删除。
  • cpu_present_map:显示系统内CPU的位图,但并非所有CPU均处于联机状态。
static void __init boot_cpu_init(void)
{
	int cpu = smp_processor_id();
	/* Mark the boot cpu "present", "online" etc for SMP and UP case */
	set_cpu_online(cpu, true);
	set_cpu_present(cpu, true);
	set_cpu_possible(cpu, true);
}

管理高端内存-page_address_init

内核通过page_address_pool访问HIGHMEM,通过page_address_maps[]进行管理。高端内存区域是不能直接地址化的内存页,因此,使用kmap()映射高端内存,用散列表page_address_htable另行管理HIGHMEM中分配的内存。

整体指向—setup_arch

void __init setup_arch(char **cmdline_p)
{
	struct tag *tags = (struct tag *)&init_tags;
	struct machine_desc *mdesc;
	char *from = default_command_line;

	unwind_init();// 发生异常时,unwind信息判断将控制权移到何处。

	setup_processor(); // 设置正在指向内核的处理器信息
	mdesc = setup_machine(machine_arch_type);  // 查找机器信息结构体并返回
	machine_name = mdesc->name;

	if (mdesc->soft_reboot)
		reboot_setup("s");

	if (__atags_pointer)
		tags = phys_to_virt(__atags_pointer);
	else if (mdesc->boot_params)
		tags = phys_to_virt(mdesc->boot_params);

	/*
	 * If we have the old style parameters, convert them to
	 * a tag list.
	 */
	if (tags->hdr.tag != ATAG_CORE)
		convert_to_tag_list(tags);
	if (tags->hdr.tag != ATAG_CORE)
		tags = (struct tag *)&init_tags;

	if (mdesc->fixup)
		mdesc->fixup(mdesc, tags, &from, &meminfo);

	if (tags->hdr.tag == ATAG_CORE) {
		if (meminfo.nr_banks != 0)
			squash_mem_tags(tags);
		save_atags(tags);
		parse_tags(tags);
	}

	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;

	memcpy(boot_command_line, from, COMMAND_LINE_SIZE);
	boot_command_line[COMMAND_LINE_SIZE-1] = '\0';
	parse_cmdline(cmdline_p, from); // 分析并处理命令行参数
	paging_init(mdesc); // 执行分页相关准备工作
	request_standard_resources(&meminfo, mdesc); // 将不依赖平台并共同管理的源信息构建成树状结构

#ifdef CONFIG_SMP
	smp_init_cpus();// 在系统内置CPU位图cpu_possible_map中对各核心做标记
#endif

	cpu_init(); // 按IRQ、ABORT、SVC、UND模式指定要使用的栈空间

	/*
	 * Set up various architecture-specific pointers
	 */
	init_arch_irq = mdesc->init_irq;
	system_timer = mdesc->timer;
	init_machine = mdesc->init_machine;

#ifdef CONFIG_VT
#if defined(CONFIG_VGA_CONSOLE)
	conswitchp = &vga_con;
#elif defined(CONFIG_DUMMY_CONSOLE)
	conswitchp = &dummy_con;
#endif
#endif
	// 为了调用中断及异常代码,将各处理器代码及helper代码复制到异常向量表基址,并设置CPU域
	early_trap_init(); 
}

unwind_init:在C语言中,发生异常时,用unwind信息判断应当将控制权移到何处,它还能在重复调用函数而发生异常的情况下对栈进行回溯,从而获得调用自身的各函数信息,这种unwind信息由异常处理列表保管

static inline int __init unwind_init(void)
{
	return 0;
}

准备内存分页—page_init()

该函数的作用是设置页表、激活启动时的内存分配器并最终生成零页。该函数中设置的页表由内核使用,无法在用户空间使用

void __init paging_init(struct machine_desc *mdesc)
{
	void *zero_page;

	build_mem_type_table(); // 构建mem_type列表
	sanity_check_meminfo(); // 对保存内存信息的meminfo结构体的值进行有效性检查
	prepare_page_table(); // 准备页表,将对应于内核映像下方及内核看见的页目录项的pmd短均清空为0
	bootmem_init(); // 激活启动使用的内存分配器,直到激活slab分配器
	devicemaps_init(mdesc); // 对向量表进行映射,还执行使用设备时必须的操作
	kmap_init(); // 对高端内存进行支持

	top_pmd = pmd_off_k(0xffff0000);

	/*
	 * allocate the zero page.  Note that this always succeeds and
	 * returns a zeroed result.
	 */
	zero_page = alloc_bootmem_low_pages(PAGE_SIZE); // 生成初始化为0的零页
	empty_zero_page = virt_to_page(zero_page); // 从生成的零页的虚拟地址获得page结构体
	flush_dcache_page(empty_zero_page); // 清除零页,确保是缓存数据一致性
}

build_mem_type_table:设置内存类型表

内存类型:根据内存使用目的的不同,内存类型对是否使用缓存、是否使用写缓冲、是否共享、域等信息的定义了不同设置,通过mem_type全局结构体数组进行管理

prepare_page_table

  • 将对应于内核映像下方的页目录各项的pmd ( 与pgd相同)段全部清空为0;
  • 将对应于内核空间的页目录各项的pmd段全部清空为0。
static inline void prepare_page_table(void)
{
	unsigned long addr;

	/*
	 * Clear out all the mappings below the kernel image.
	 */
	for (addr = 0; addr < MODULES_VADDR; addr += PGDIR_SIZE)
		pmd_clear(pmd_off_k(addr));

#ifdef CONFIG_XIP_KERNEL
	/* The XIP kernel is mapped in the module area -- skip over it */
	addr = ((unsigned long)_etext + PGDIR_SIZE - 1) & PGDIR_MASK;
#endif
	for ( ; addr < PAGE_OFFSET; addr += PGDIR_SIZE)
		pmd_clear(pmd_off_k(addr));

	/*
	 * Clear out all the kernel space mappings, except for the first
	 * memory bank, up to the end of the vmalloc region.
	 */
	for (addr = __phys_to_virt(bank_phys_end(&meminfo.bank[0]));
	     addr < VMALLOC_END; addr += PGDIR_SIZE)
		pmd_clear(pmd_off_k(addr));
}
Logo

立足具身智能前沿赛道,致力于搭建全球化、开源化、全栈式技术交流与实践共创平台。

更多推荐