第一部分:汇编部分
Linux启动之linux-rk3288-tchip/kernel/arch/arm/boot/compressed/ head.S分析
这段代码是linux boot后执行的第一个程序,完成的主要工作是解压内核,然后跳转到相关执行地址。这部分代码在做驱动开发时不需要改动,但分析其执行流程对是理解android的第一步
开头有一段宏定义这是gnu arm汇编的宏定义。关于GUN的汇编和其他编译器,在指令语法上有很大差别,具体可查询相关GUN汇编语法了解
另外此段代码必须不能包括重定位部分。因为这时一开始必须要立即运行的。所谓重定位,比如当编译时某个文件用到外部符号是用动态链接库的方式,那么该文件生成的目标文件将包含重定位信息,在加载时需要重定位该符号,否则执行时将因找不到地址而出错
#ifdef DEBUG//开始是调试用,主要是一些打印输出函数,不用关心
#if defined(CONFIG_DEBUG_ICEDCC)
……具体代码略
#endif
宏定义结束之后定义了一个段,
.section ".start", #alloc, #execinstr
这个段的段名是 .start,#alloc表示Section contains allocated data, #execinstr表示Section contains executable instructions.
生成最终映像时,这段代码会放在最开头
.align
start:
.type start,#function /*.type指定start这个符号是函数类型*/
.rept 8
mov r0, r0 //将此命令重复8次,相当于nop,这里是为中断向量保存空间
.endr
b 1f
.word 0x016f2818 @ Magic numbers to help the loader
.word start @ absolute load/run zImage
//此处保存了内核加载和运行的地址,实质上也是本函数的运行地址
address
.word _edata @ 内核结束地址
//注意这些地址在顶层vmlixu.lds(具体在/kernel文件夹里)里进行了定义,是链接的地址,加载内核后可能会进行重定位
1: mov r7, r1 @ 保存architecture ID,这里是从bootload传递进来的
mov r8, r2 @ 保存参数列表 atags指针
r1和r2中分别存放着由bootloader传递过来的architecture ID和指向标记列表的指针。这里将这两个参数先保存。
#ifndef __ARM_ARCH_2__
/*
* Booting from Angel - need to enter SVC mode and disable
* FIQs/IRQs (numeric definitions from angel arm.h source).
* We only do this if we were in user mode on entry.
*/
读取cpsr并判断是否处理器处于supervisor模式——从bootload进入kernel,系统已经处于SVC32模式;而利用angel进入则处于user模式,还需要额外两条指令。之后是再次确认中断关闭,并完成cpsr写入
Angel 是 ARM的调试协议,一般用的是MULTI-ICE。ANGLE需要在板子上有驻留程序,然后通过串口就可以调试了。用过的AXD或trace调试环境的话,对此应该比较熟悉。
not_angel: //若不是通过angel调试进入内核
mrs r2, cpsr @ turn off interrupts to
orr r2, r2, #0xc0 @ prevent angel from running
msr cpsr_c, r2 //这里将cpsr中I、F位分别置“1”,关闭IRQ和FIQ
#else
teqp pc, #0x0c000003 @ turn off interrupts
常用 TEQP PC,#(新模式编号)来改变模式
#endif
另外链接器会把一些处理器相关的代码链接到这个位置,也就是arch/arm/boot/compressed/head-xxx.S文件中的代码。在高通平台下,这个文件是head-msm.S连接脚是compress/vmlinux.lds,其中部分内容大致如下,在连接时,连接器根据每个文件中的段名将相同的段合在一起,比如将head.S和head-msm.S的.start段合在一起
SECTIONS
{
. = TEXT_START;
_text = .;
.text : {
_start = .;
*(.start)
*(.text)
*(.text.*)
*(.fixup)
*(.gnu.warning)
*(.rodata)
*(.rodata.*)
*(.glue_7)
*(.glue_7t)
*(.piggydata)
. = ALIGN(4);
}
_etext = .;
}
下面即进入.text段
.text
adr r0, LC0 //当前运行时LC0符号所在地址位置,注意,这里用的是adr指令,这个指令会根据目前PC的值,计算符号相对于PC的位置,是个相对地址。之所以这样做,是因为下面指令用到了绝对地址加载ldmia指令,必须要调整确定目前LC0的真实位置,这个位置也就是用adr来计算
ldmia r0, {r1, r2, r3, r4, r5, r6, ip, sp}
subs r0, r0, r1 @ //这里获得当前LCD0实际地址与链接地址差值
//r1即是LC0的连接地址,也即由vmlinux.lds定位的地址
//差值存入r0中。
beq not_relocated //如果相等不需要重定位,因为已经在正确的//地址运行了。重定位的原因是,MMU单元未使能,不能进行地址映射,必须要手工重定位。
下面举个简单例子说明:
如果连接地址是0xc0000000,那么LC0的连接地址假如连接为0xc0000010,那么LC0相对于连接起始地址的差为0x10,当此段代码是从0xc0000000运行的话,那么执行adr r0,LC0的值实际上按下面公式计算:
R0=PC+0x10,由于PC=连接处的值,可知,此时是在ram中运行,同理如果是在不是在连接处运行,则假设是在0x00000000处运行,则R0=0x00000000+0x10,可知,此时不是在ram的连接处运行。
上面这几行代码用于判断代码是否已经重定位到内存中,LC0这个符号在head.S中定义如下,实质上相当于c语言的全局数据结构,结构的每个域存储的是一个指针。指针本身的值代表不同的代码段,已经在顶层连接脚本vmlinux.lds里进行了赋值,比如_start是内核开始的地址
.type LC0, #object
LC0: .word LC0 @ r1 //这个要加载到r1中的LC0是链接时LC0的地址
.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
通过当前运行时LC0的地址与链接器所链接的地址进行比较判断。若相等则是运行在链接的地址上。
如果不是运行在链接的地址上,则下面的代码必须修改相关地址,进行重新运行
/*
* r5 - zImage base address
* r6 - GOT start
* ip - GOT end
*/
//修正实际运行的位置,否则跳转指令就找不到相关代码
add r5, r5, r0 //修改内核映像基地址
add r6, r6, r0
add ip, ip, r0 //修改got表的起始和结束位置
#ifndef CONFIG_ZBOOT_ROM
/*若没有定义CONFIG_ZBOOT_ROM,此时运行的是完全位置无关代码
位置无关代码,也就是不能有绝对地址寻址。所以为了保持相对地址正确,
需要将bss段以及堆栈的地址都进行调整
* r2 - BSS start
* r3 - BSS end
* sp - stack pointer
*/
add r2, r2, r0
add r3, r3, r0
add sp, sp, r0
//全局符号表的地址也需要更改,否则,对全局变量引用将会出错
1: ldr r1, [r6, #0] @ relocate entries in the GOT
add r1, r1, r0 @ table. This fixes up the
str r1, [r6], #4 @ C references.
cmp r6, ip
blo 1b
#else //若定义了CONFIG_ZBOOT_ROM,只对got表中在bss段以外的符号进行重定位
1: ldr r1, [r6, #0] @ relocate entries in the GOT
cmp r1, r2 @ entry < bss_start ||
cmphs r3, r1 @ _end < entry
addlo r1, r1, r0 @ table. This fixes up the
str r1, [r6], #4 @ C references.
cmp r6, ip
blo 1b
#endif
如果运行当前运行地址和链接地址相等,则不需进行重定位。直接清除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
cmp r2, r3
blo 1b
之后跳转到cache_on处
bl cache_on
cache_on定义
.align 5
cache_on: mov r3, #8 @ cache_on function
b call_cache_fn
把r3的值设为8。这是一个偏移量,也就是索引proc_types中的操作函数。
然后跳转到call_cache_fn。这个函数的定义如下:
call_cache_fn:
adr r12, proc_types //把proc_types的相对地址加载到r12中
#ifdef CONFIG_CPU_CP15
mrc p15, 0, r6, c0, c0 @ get processor ID
#else
ldr r6, =CONFIG_PROCESSOR_ID
#endif
1: ldr r1, [r12, #0] @ get value
ldr r2, [r12, #4] @ get mask
eor r1, r1, r6 @ (real ^ match)
tst r1, r2 @是否和CPU ID匹配?
addeq pc, r12, r3 @ 用刚才的偏移量,查找//到cache操作函数,找到后就执行相关操作,比如执行b __armv7_mmu_cache_on
//
add r12, r12, #4*5 //如果不相等,则偏移到下个proc_types结构处
b 1b
addeq pc, r12, r3 @ call cache function
proc_type的定义如下 ,实质上还是一张数据结构表
.type proc_types,#object
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
.word 0x41807200 @ ARM720T (writethrough)
.word 0xffffff00
b __armv4_mmu_cache_on
b __armv4_mmu_cache_off
mov pc, lr
.word 0x41007400 @ ARM74x
.word 0xff00ff00
b __armv3_mpu_cache_on
b __armv3_mpu_cache_off
b __armv3_mpu_cache_flush
.word 0x41009400 @ ARM94x
.word 0xff00ff00
b __armv4_mpu_cache_on
b __armv4_mpu_cache_off
b __armv4_mpu_cache_flush
.word 0x00007000 @ ARM7 IDs
.word 0x0000f000
mov pc, lr
mov pc, lr
mov pc, lr
@ Everything from here on will be the new ID system.
.word 0x4401a100 @ sa110 / sa1100
.word 0xffffffe0
b __armv4_mmu_cache_on
b __armv4_mmu_cache_off
b __armv4_mmu_cache_flush
.word 0x6901b110 @ sa1110
.word 0xfffffff0
b __armv4_mmu_cache_on
b __armv4_mmu_cache_off
b __armv4_mmu_cache_flush
@ These match on the architecture ID
.word 0x00020000 @
.word 0x000f0000 //
b __armv4_mmu_cache_on
b __armv4_mmu_cache_on //指令的地址
b __armv4_mmu_cache_off
b __armv4_mmu_cache_flush
.word 0x00050000 @ ARMv5TE
.word 0x000f0000
b __armv4_mmu_cache_on
b __armv4_mmu_cache_off
b __armv4_mmu_cache_flush
.word 0x00060000 @ ARMv5TEJ
.word 0x000f0000
b __armv4_mmu_cache_on
b __armv4_mmu_cache_off
b __armv4_mmu_cache_flush
.word 0x0007b000 @ ARMv6
.word 0x0007f000
b __armv4_mmu_cache_on
b __armv4_mmu_cache_off
b __armv6_mmu_cache_flush
.word 0 @ unrecognised type
.word 0
mov pc, lr
mov pc, lr
mov pc, lr
.size proc_types, . - proc_types
找到执行的cache函数后,就用上面的 addeq pc, r12, r3直接跳转,例如执行下面这个处理器结构的cache函数
__armv7_mmu_cache_on:
mov r12, lr //注意,这里需要手工保存返回地址!!这样做的原因是下面的bl指令会覆盖掉原来的lr,为保证程序正确返回,需要保存原来lr的值
bl __setup_mmu
mov r0, #0
mcr p15, 0, r0, c7, c10, 4 @ drain write buffer
mcr p15, 0, r0, c8, c7, 0 @ flush I,D TLBs
mrc p15, 0, r0, c1, c0, 0 @ read control reg
orr r0, r0, #0x5000 @ I-cache enable, RR cache replacement
orr r0, r0, #0x0030
bl __common_mmu_cache_on
mov r0, #0
mcr p15, 0, r0, c8, c7, 0 @ flush I,D TLBs
mov pc, r12 //返回到cache_on
这个函数首先执行__setup_mmu,然后清空write buffer、I/Dcache、TLB.接着打开i-cache,设置为Round-robin replacement。调用__common_mmu_cache_on,打开mmu和d-cache.把页表基地址和域访问控制写入协处理器寄存器c2、c3. __common_mmu_cache_on函数数定义如下:
__common_mmu_cache_on:
#ifndef DEBUG
orr r0, r0, #0x000d @ Write buffer, mmu
#endif
mov r1, #-1 //-1的补码是ffff ffff,
mcr p15, 0, r3, c2, c0, 0 @ 把页表地址存于协处理器寄存器中
mcr p15, 0, r1, c3, c0, 0 @设置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
重点来看一下__setup_mmu这个函数,定义如下:
__setup_mmu: sub r3, r4, #16384 @ Page directory size
bic r3, r3, #0xff @ Align the pointer
bic r3, r3, #0x3f00
这里r4中存放着内核执行地址,将16K的一级页表放在这个内核执行地址下面的16K空间里,上面通过 sub r3, r4, #16384 获得16K空间后,又将页表的起始地址进行16K对齐放在r3中。即ttb的低14位清零。
//初始化页表,并在RAM空间里打开cacheable和bufferable位
mov r0, r3
mov r9, r0, lsr #18
mov r9, r9, lsl #18 @ start of RAM
add r10, r9, #0x10000000 @ a reasonable RAM size
上面这几行把一级页表的起始地址保存在r0中,并通过r0获得一个ram起始地址(每个页面大小为1M)然后映射256M ram空间,并把对应的描述符的C和B位均置”1”
mov r1, #0x12 //一级描述符的bit[1:0]为10,表示这是一个section描述符。也即分页方式为段式分页
orr r1, r1, #3 << 10 //一级描述符的access permission bits bit[11:10]为11.即
add r2, r3, #16384 //一级描述符表的结束地址存放在r2中。
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//下个1M物理空间,每个页框1M。
teq r0, r2
bne 1b
因为打开cache前必须打开mmu,所以这里先对页表进行初始化,然后打开mmu和cache。
上面这段就是对一级描述符表(页表)的初始化,首先比较这个描述符所描述的地址是否在那个256M的空间中,如果在则这个描述符对应的内存区域是cacheable ,bufferable。如果不在则noncacheable, nonbufferable.然后将描述符写入一个一级描述符表的入口,并将一级描述符表入口地址加4,而指向下一个1Msection的基地址。如果页表入口未初始化完,则继续初始化。
页表大小为16K,每个描述符4字节,刚好可以容纳4096个描述符,每个描述符映射1M ,那么4096*所以这里就映射了4096*1M = 4G的空间。因此16K的页完全可以把256M地址空间全部映射
mov r1, #0x1e
orr r1, r1, #3 << 10 //这两行将描述的bit[11:10] bit[4:1]置位,
//具体置位的原因,在ARM11的页表项描述符里有说明,由于没找到完整的文档,这里只给出图示:
mov r2, pc, lsr #20
orr r1, r1, r2, lsl #20 //将当前地址进1M对齐,并与r1中的内容结合形成一个描述当前指令所在section的描述符。
add r0, r3, r2, lsl #2 //r3为刚才建立的一级描述符表的起始地址。通过将当前地
//址(pc)的高12位左移两位(形成14位索引)与r3中的地址
// (低14位为0)相加形成一个4字节对齐的地址,这个
//地址也在16K的一级描述符表内。当前地址对应的
//描述符在一级页表中的位置
str r1, [r0], #4
add r1, r1, #1048576
str r1, [r0] //这里将上面形成的描述符及其连续的下一个section描述
//写入上面4字节对齐地址处(一级页表中索引为r2左移
//2位)
mov pc, lr //返回,调用此函数时,调用指令的下一语句mov r0, #0的地址保存在lr中
这里进行的是一致性的映射,物理地址和虚拟地址是一样。
__common_mmu_cache_on最后执行mov pc, r12返回cache_on,为何返回到的是cache_on呢?这就是上面解释保存lr的原因,因为原来的lr保存了执行
bl cache_on语句的下条指令,因此能正确返回!
下一条指令也即是下面开始
mov r1, sp @栈空间大小是4096字节,那//么在栈空间地址上面再分配64K字节空间
add r2, sp, #0x10000 @ 分配64k字节。
栈的分配如下:
.align
.section ".stack", "w"
user_stack: .space 4096//lc0对SP进行了定义 .word user_stack+4096 @ sp
由此可见sp是往下增长的
分配了解压缩用的缓冲区,那么接下来就判断这个数据区是否和我们目前运行的代码空间重叠,如果重叠则需调整
/*
* Check to see if we will overwrite ourselves.
* r4 = final kernel address
* r5 = start of this image
* r2 = end of malloc space (and therefore this image)
* We basically want:
* r4 >= r2 -> OK
* r4 + image length <= r5 -> OK
*/
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
缓冲区空间的起始地址和结束地址分别存放在r1、r2中。然后判断最终内核地址,也就是解压后内核的起始地址,是否大于malloc空间的结束地址,如果大于就跳到wont_overwrite执行,wont_overwrite函数后面会讲到。否则,检查最终内核地址加解压后内核大小,也就是解压后内核的结束地址,是否小于现在未解压内核映像的起始地址。小于也会跳到wont_owerwrite执行。如两这两个条件都不满足,则继续往下执行。
mov r5, r2 @ decompress after malloc space
mov r0, r5
mov r3, r7
bl decompress_kernel
这里将解压后内核的起始地址设为malloc空间的结束地址。然后后把处理器id(开始时保存在r7中)保存到r3中,调用decompress_kernel开始解压内核。这个函数的四个参数分别存放在r0-r3中,它在arch/arm/boot/compressed/misc.c中定义。解压的过程为先把解压代码放到缓冲区,然后从缓冲区在拷贝到最终执行空间。
add r0, r0, #127
bic r0, r0, #127 @ align the kernel length
/*
* r0 = decompressed kernel length
* r1-r3 = unused
* r4 = kernel execution address
* r5 = decompressed kernel start
* r6 = processor ID
* r7 = architecture ID
* r8 = atags pointer
* r9-r14 = corrupted
*/
add r1, r5, r0 @ end of decompressed kernel
adr r2, reloc_start
ldr r3, LC1
add r3, r2, r3
1: ldmia r2!, {r9 - r14} @ copy relocation code
stmia r1!, {r9 - r14}
ldmia r2!, {r9 - r14}
stmia r1!, {r9 - r14}
cmp r2, r3
blo 1b
这里首先计算出重定位段,也即reloc_start段,然后对它的进行重定位
bl cache_clean_flush
add pc, r5, r0 @ call relocation code
重定位结束后跳到解压后执行 b call_kernel,不再返回。call_kernel定义如下:
call_kernel:
bl cache_clean_flush
bl cache_off
mov r0, #0 @ must be zero
mov r1, r7 @ restore architecture number
mov r2, r8 @ restore atags pointer
mov pc, r4 @ call kernel
在运行解压后内核之前,先调用了
cache_clean_flush这个函数。这个函数的定义如下:
cache_clean_flush:
mov r3, #16
b call_cache_fn
其实这里又调用了call_cache_fn这个函数,注意,这里r3的值为16,上面对cache操作已经比较详细,不再讨论。
刷新cache后,则执行mov pc, r4跳入内核,开始进行下个阶段的处理。
====================================================================================
第二部分:汇编部分
Linux启动之linux-rk3288-tchip/kernel/arch/arm/kernel/head.S
整个代码流程如下:
当解压缩部分的head.S执行完后,就开始执行kernel/目录下真正的linux内核代码。在内核连接文件/kernel/vmlinux/lds里定义了这部分开始所处的段空间为.text.head,也即内核代码段的头
关键代码如下:
mrc p15, 0, r9, c0, c0 @ get processor id//读出CPUid
bl __lookup_processor_type @ r5=procinfo r9=cpuid
movs r10, r5 @ invalid processor (r5=0)?
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
bl __create_page_tables
大致流程为,寻找CPU类型查找机器信息,解析内核参数列表,创建内存分页机制
__lookup_processor_type,__lookup_machine_type,__vet_atags函数都在kernel/head-comm.S内,这个文件实际上是被包含在head.S内
Linux之所以把搜索机器类型和CPU类型独立出来,就是为了让内核尽可能的和bootload独立,增强移植性,把不同CPU的差异性处理减到最小。比如不同ARM架构的CPU处理中断的,打开MMU,cach操作是不同的,因此,在内核开始执行前需要定位CPU架构,比如高通利用的ARM11,Ti用的cortex-8架构
__lookup_machine_type 寻找的机器类型结构定义在arch/arm/include/asm/mach.h中
查询方法比较简单,利用bootloa传进来的参数依次查询上述结构表项
这个表项是在编译阶段将#define MACHINE_START(_type,_name)宏定义的结构体struct machine_desc连接到
__arch_info段,那么结构体开始和结束地址用__arch_info_begin和__arch_info_end符号引用
3: .long .
.long __arch_info_begin
.long __arch_info_end
//r1 = 机器架构代码 number,由bootload最后阶段传进来
.type __lookup_machine_type, %function
__lookup_machine_type:
adr r3, 3b
ldmia r3, {r4, r5, r6}
sub r3, r3, r4 @ 此时没有开MMU,因此需要确定放置__arch_info_begin的实际物理地址
add r5, r5, r3 @ 调整地址,找到__arch_info的实际地址(连接地址和物理地址不一定一样,因此需要调整)
add r6, r6, r3 @
1: ldr r3, [r5, #MACHINFO_TYPE] @ MACHINFO_TYPE=机器类型域的偏移量
teq r3, r1 @ 是否和bootload传进来的参数相同?
beq 2f @ 找到则跳出循环
add r5, r5, #SIZEOF_MACHINE_DESC @ 地址偏移至下个__arch_inf表项
cmp r5, r6
blo 1b
mov r5, #0 @ 未知的类型
2: mov pc, lr//返回
__lookup_processor_type的查询的结构为struct proc_info_list
机器类型确定后即开始解析(__vet_atags)内核参数列表,判断第一个参数类型是不是ATAG_CORE。
内核参数列表一般放在内核前面16K地址空间处。列表的表项由struct tag构成,每个struct tag有常见的以下类型:
:ATAG_CORE、ATAG_MEM、ATAG_CMDLINE、ATAG_RAMDISK、ATAG_INITRD等。
这些类型是宏定义,比如#define ATAG_CORE 0x54410001
arch/arm/include/asm/setup.h
struct tag_header {
__u32 size;
__u32 tag;
};
struct tag {
struct tag_header hdr;
union {
struct tag_core core;//有效的内核
struct tag_mem32 mem;
struct tag_videotext videotext;
struct tag_ramdisk ramdisk;//文件系统
struct tag_initrd initrd;//临时根文件系统
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;//命令行
} u;
};
接下来就是创建页表,因为要使能MMU进行虚拟内存管理,因此必须创建映射用的页表。页表就像一个函数发生器,保证访问虚拟地址时能从物理地址里取到正确代码
pgtbl r4 @ page table address
//页表放置的位置可由下面的宏确定,即在内核所在空间的前16K处
.macro pgtbl, rd
ldr /rd, =(KERNEL_RAM_PADDR - 0x4000)
.endm
mov r0, r4
mov r3, #0
add r6, r0, #0x4000//16K的空间,r6即是页表结束处
1: str r3, [r0], #4//清空页表项,页表项共有16K/4项
str r3, [r0], #4
str r3, [r0], #4
str r3, [r0], #4
teq r0, r6
bne 1b
ldr r7, [r10, #PROCINFO_MM_MMUFLAGS]
//从从差得的proc_info_list结构PROCINFO_MM_MMUFLAGS处获取MMU的信息
/*
为内核创建1M的映射空间,这里是按照1:1一致映射,即代码的基地址(高12bit)对应相同的物理块地址。这种映射关系只是在启动阶段,在跳进start_kernel后会被paging_init().移除。这种映射可以直接利用当前地址的高12bit作为基地址,这种方式很巧妙,因为当前的PC(加颜色处的地址)依然在1M空间内,因此,高12bit(段基地址)在1M空间内都是相同的。
*/
mov r6, pc, lsr #20 @内核映像的基地址
orr r3, r7, r6, lsl #20 @ 基地址偏移后再加上标示符,即可得一个页表项的值
str r3, [r4, r6, lsl #2] @将此表项按照页表项的索引存入对应的表项中。比如,若//基地址是0xc0001000,那么存入页表的第0xc00项中
//目前的映射依然是1:1的映射
//然后移到下个段基地址处,开始映射此KERNEL_START对应的空间
//这个空间映射的物理地址与上面的相同,也就是两个虚拟地址映射到了同一个物理地址空间
//r0+基地址组成//在第一级页表中索引到相关的项
add r0, r4, #(KERNEL_START & 0xff000000) >> 18
str r3, [r0, #(KERNEL_START & 0x00f00000) >> 18]!
ldr r6, =(KERNEL_END - 1)
add r0, r0, #4//移到下个表项
add r6, r4, r6, lsr #18//结束的基地址
1: cmp r0, r6
add r3, r3, #1 << 20//下个1M物理地址空间
strls r3, [r0], #4//建立映射表项,开始创建所有的内核空间页表项
bls 1b//
#ifdef CONFIG_XIP_KERNEL
/*
* Map some ram to cover our .data and .bss areas.
*/
orr r3, r7, #(KERNEL_RAM_PADDR & 0xff000000)
.if (KERNEL_RAM_PADDR & 0x00f00000)
orr r3, r3, #(KERNEL_RAM_PADDR & 0x00f00000)
.endif
add r0, r4, #(KERNEL_RAM_VADDR & 0xff000000) >> 18
str r3, [r0, #(KERNEL_RAM_VADDR & 0x00f00000) >> 18]!
ldr r6, =(_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
#endif
/*
* Then map first 1MB of ram in case it contains our boot params.
*/
//虚拟ram地址的第一个1M空间包含了参数列表,也需要映射
add r0, r4, #PAGE_OFFSET >> 18
orr r6, r7, #(PHYS_OFFSET & 0xff000000)
.if (PHYS_OFFSET & 0x00f00000)
orr r6, r6, #(PHYS_OFFSET & 0x00f00000)
.endif
str r6, [r0]
mov pc, lr//页表建立完成,返回
页表创建后,具体的映射空间如下图:
执行完上述页表创建,开始执行内核跳转:
ldr r13, __switch_data @ address to jump to after
@ mmu has been enabled
adr lr, __enable_mmu @ return (PIC) address
add pc, r10, #PROCINFO_INITFUNC
__switch_data 是一个数据结构,如下
.type __switch_data, %object
__switch_data:
.long __mmap_switched
.long __data_loc @ r4
.long __data_start @ 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
语句“add pc, r10, #PROCINFO_INITFUNC”通过查表调用proc-v7.s中__v7_setup函数,该函数末尾通过将lr寄存器赋给pc,导致对__enable_mmu的调用,完成使能mmu的操作,之后将r13寄存器值赋给pc,调用__switch_data数据结构中的第一个函数__mmap_switched,
.type __mmap_switched, %function
__mmap_switched:
adr r3, __switch_data + 4
ldmia r3!, {r4, r5, r6, r7}
cmp r4, r5 @ 拷贝数据段
1: cmpne r5, r6
ldrne fp, [r4], #4
strne fp, [r5], #4
bne 1b
mov fp, #0 @ 清除BSS段
1: cmp r6, r7
strcc fp, [r6],#4
bcc 1b
ldmia r3, {r4, r5, r6, r7, sp}//然后调整指针到processor_id 域
str r9, [r4] @ 保存CPU ID
str r1, [r5] @保存机器类型
str r2, [r6] @ 保存参数列表指针
bic r4, r0, #CR_A @ Clear 'A' bit
stmia r7, {r0, r4} @ 保存控制信息
b start_kernel
最终调用init/main.c文件中的start_kernel函数。
这个 start_kernel 正是 kernel/init/main.c 的内核起始函数====================================================================================
第三部分:C部分
Linux启动之start_kernel
当内核与体系架构相关的汇编代码执行完毕,即跳入start_kernel。这个函数在kernel/init/main.c中。由于这部分涉及linux众多数据结构的初始化,包括内核命令行解析,内存缓冲区建立初始化,页面分配和初始化,虚拟文件系统建立,根文件系统挂载,驱动文件挂载,二进制程序文件的执行等,限于篇幅和理解水平,只能流程上的大致梳理,以上提及方面后期再做详细分析。为保证准确性,参考了一部分书籍和网上技术文档,如有疑问请及时提出,共同学习探讨。
asmlinkage void __init start_kernel(void)
{
char * command_line;
extern struct kernel_param __start___param[], __stop___param[];
//这里引用两个符号,是内核编译脚本定位的内核参数起始地址
smp_setup_processor_id();//多CPU架构的初始化,目前我们的高通linux侧是单核的,此多核不做分析
unwind_init();//本架构中没有用
lockdep_init();//本架构为空
debug_objects_early_init();
cgroup_init_early();
local_irq_disable();
early_boot_irqs_off();
early_init_irq_lock_class();
lock_kernel();//本架构为空函数
tick_init();
//时钟中断初始化函数,调用 clockevents_register_notifier函数向 clockevents_chain时钟事件链注册时钟控制函数 tick_notifier。这是个回调函数,指明了当时钟事件发生变化时应该执行的哪些操作,比如时钟的挂起操作等
boot_cpu_init();//用于多核CPU的初始化
page_address_init();//用于高地址内存,我们都用32位CPU,此函数为空
printk(KERN_NOTICE);
printk(linux_banner);
setup_arch(&command_line);
//具体看一下这个架构初始化函数完成哪些功能
void __init setup_arch(char **cmdline_p)
{
struct tag *tags = (struct tag *)&init_tags;//定义了一个默认的内核参数列表
struct machine_desc *mdesc;
char *from = default_command_line;
setup_processor();//汇编的CPU初始化部分已讲过,不再讨论
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);
//由于MMU单元已打开,此处需要而boot_params是物理地址,需要转换成虚拟地址才能访问,因为此时CPU访问的都是虚拟地址
/*
* If we have the old style parameters, convert them to
* a tag list.
*/
//内核参数列表第一项必须是ATAG_CORE类型
if (tags->hdr.tag != ATAG_CORE)//如果不是,则需要转换成新的内核参数类型,新的内核参数类型用下面struct tag结构表示
convert_to_tag_list(tags);//此函数完成新旧参数结构转换
struct tag {
struct tag_header hdr;
union {
struct tag_core core;
struct tag_mem32 mem;
struct tag_videotext videotext;
struct tag_ramdisk ramdisk;
struct tag_initrd initrd;
struct tag_serialnr serialnr;
struct tag_revision revision;
struct tag_videolfb videolfb;
struct tag_cmdline cmdline;
} u;
};
//旧的内核参数列表用下面结构表示
struct param_struct {
union {
struct {
unsigned long page_size; /* 0 */
unsigned long nr_pages; /* 4 */
unsigned long ramdisk_size; /* 8 */
unsigned long flags; /* 12 */
。。。。。。。。。。。。//较长,省略
}
if (tags->hdr.tag != ATAG_CORE)//如果没有内核参数
tags = (struct tag *)&init_tags;//则选用默认的内核参数
if (mdesc->fixup)
mdesc->fixup(mdesc, tags, &from, &meminfo);//用内核参数列表填充meminfo
if (tags->hdr.tag == ATAG_CORE) {
if (meminfo.nr_banks != 0)
squash_mem_tags(tags);
save_atags(tags);
parse_tags(tags);//解析内核参数列表,然后调用内核参数列表的处理函数对这些参数进行处理。比如,如果列表为命令行,则最终会用parse_tag_cmdlin函数进行解析,这个函数用_tagtable编译连接到了内核里
__tagtable(ATAG_CMDLINE, parse_tag_cmdline);
}
//下面是记录内核代码的起始,结束虚拟地址
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;
//下面是对命令行的处理,刚才在参数列表处理parse_tag_cmdline函数已把命令行拷贝到了from空间
memcpy(boot_command_line, from, COMMAND_LINE_SIZE);
boot_command_line[COMMAND_LINE_SIZE-1] = '/0';
parse_cmdline(cmdline_p, from);//解析出命令行,命令行解析出以后,同样会调用相关处理函数进行处理。系统用__early_param宏在编译阶段把处理函数编译进内核。
paging_init(&meminfo, mdesc);
//这个函数完成页表初始化,具体的方法为建立线性地址划分后每个地址空间的标志;清除在boot阶段建立的内核映射空间,也即把页表项全部清零;调用bootmem_init,禁止无效的内存节点,由于我们的物理内存都是连续的空间,因此,内存节点为1个。接下来判断INITRD映像是否存在,若存在则检查其所在的地址是否在一个有效的地址内,然后返回此内存节点号。
先看两个数据结构。
struct meminfo表示内存的划分情况。Linux的内存划分为bank。每个bank用
struct membank表示,start表示起始地址,这里是物理地址,size表示大小,node表示此bank所在的节点号,对于只有一个节点的内存,所有bank节点都相等
struct membank {
unsigned long start;
unsigned long size;
int node;
};
struct meminfo {
int nr_banks;
struct membank bank[NR_BANKS];
};
//在page_init函数中比较重要的是bootmem_init函数,此函数在完成原来映射页表的清除后,最终调用bootmem_init_node如下:
bootmem_init_node(int node, int initrd_node, struct meminfo *mi)
{
unsigned long zone_size[MAX_NR_ZONES], zhole_size[MAX_NR_ZONES];
unsigned long start_pfn, end_pfn, boot_pfn;
unsigned int boot_pages;
pg_data_t *pgdat;//每个节点用pg_data_t描述,这个结构用在非一致性内存中,我们的内存只有一个,地址是连续的
int i;
start_pfn = -1UL;
end_pfn = 0;
for_each_nodebank(i, mi, node) {
struct membank *bank = &mi->bank[i];
unsigned long start, end;
start = bank->start >> PAGE_SHIFT;//计算出页表号,实际也表示第几个物理页号
end = (bank->start + bank->size) >> PAGE_SHIFT;
if (start_pfn > start)
start_pfn = start;
if (end_pfn < end)
end_pfn = end;
map_memory_bank(bank);//将每个节点的每个bank重新映射,比如重新映射内核空间
}
if (end_pfn == 0)
return end_pfn;
//一个字节代表8个页,因此找到一个
//可放置这些所有自己的页面即可。用一个bit位表示一个页是否已占用,那么一个字节为8个页,比如4096个页需要4096/8=512字节,容纳这个位图需要一个页
boot_pages = bootmem_bootmap_pages(end_pfn - start_pfn);
boot_pfn = find_bootmap_pfn(node, mi, boot_pages);//在node节点内存的bank中找到一个可以放置位图的页面的页面序列,然后返回这个页面序列的首个页面号
node_set_online(node);//设置本节点有效
pgdat = NODE_DATA(node);//获取节点描述符pgdat
init_bootmem_node(pgdat, boot_pfn, start_pfn, end_pfn);//设置本节点内所有映射页的位图,即每个字节全部置为0xff,表示已经映射使用。然后填充pgdat结构
for_each_nodebank(i, mi, node)
free_bootmem_node(pgdat, mi->bank[i].start, mi->bank[i].size);//设置每个映射的页面空闲,实际是对位图的操作,对每个bit清零
reserve_bootmem_node(pgdat, boot_pfn << PAGE_SHIFT,
boot_pages << PAGE_SHIFT, BOOTMEM_DEFAULT);
//标示位图所占的页面被占用
if (node == 0)
reserve_node_zero(pgdat);
#ifdef CONFIG_BLK_DEV_INITRD
/*
* If the initrd is in this node, reserve its memory.
*/
if (node == initrd_node) {
int res = reserve_bootmem_node(pgdat, phys_initrd_start,
phys_initrd_size, BOOTMEM_EXCLUSIVE);
//INITRD映像占用的空间需要标示占用,INITRD是虚拟根文件系统,此时还未加载,因此挂载之前这个物理空间不能再被分配使用
if (res == 0) {
initrd_start = __phys_to_virt(phys_initrd_start);
initrd_end = initrd_start + phys_initrd_size;
} else {
printk(KERN_ERR
"INITRD: 0x%08lx+0x%08lx overlaps in-use "
"memory region - disabling initrd/n",
phys_initrd_start, phys_initrd_size);
}
}
#endif
/*
* initialise the zones within this node.
*/
memset(zone_size, 0, sizeof(zone_size));
memset(zhole_size, 0, sizeof(zhole_size));
/*
* The size of this node has already been determined. If we need
* to do anything fancy with the allocation of this memory to the
* zones, now is the time to do it.
*/
zone_size[0] = end_pfn - start_pfn;
zhole_size[0] = zone_size[0];
for_each_nodebank(i, mi, node)
zhole_size[0] -= mi->bank[i].size >> PAGE_SHIFT;
//计算共有多少页空洞,注意,有些bank的起始结束地址并不是刚好4K对齐的,因此,可能存在某些空白页框。用节点总的物理页框减去每个bank页框,就得到页空洞
//这个函数里面主要完成zone区的初始化,linux内存管理将内存节点又分为ZONE区管理,比如ZONE_DMA和ZONE_NORMAL等,因此需要初始化。由于平台只针对一致性内存管理,即物理内存空间只包含DDR部分,此处很多函数是空的,再次略过
arch_adjust_zones(node, zone_size, zhole_size);
free_area_init_node(node, zone_size, start_pfn, zhole_size);
return end_pfn;
}
//在page_init的最后完成devicemaps_init初始化,比如中断向量的映射。映射的大致过程是,申请一个物理框,然后调用creat_map将此物理页框映射到0xffff0000.最后再调用struct machine_desc的map_io完成IO设备的映射
//在完成内存页映射后即进入request_standard_resources,这个函数比较简单,主要完成从iomem_resource空间申请所需的内存资源,比如内核代码和视频所需的资源等
request_standard_resources(&meminfo, mdesc);
#ifdef CONFIG_SMP
smp_init_cpus();
#endif
cpu_init();//此函数为空
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
early_trap_init();//重定位中断向量,将中断向量代码拷贝到中断向量页,并把信号处理代码指令拷贝到向量页中
}
mm_init_owner(&init_mm, &init_task);//空函数
setup_command_line(command_line);//保存命令行,以备后用,此保存空间需申请
//这个函数调用完了,就开始执行下面初始化函数
unwind_setup();//空函数
setup_per_cpu_areas();//设置每个CPU信息,单核CPU为空函数
setup_nr_cpu_ids();//空函数
smp_prepare_boot_cpu(); //设置启动的CPU为在线状态.在多CPU架构下
//第一个启动的cpu启动到一定阶段后,开始启动其它的cpu,它会为每个后来启动的cpu创建一个0号进程,而这些0号进程的堆栈的thread_info结构中的cpu成员变量则依次被分配出来(利用alloc_cpu_id()函数)并设置好,这样当这些cpu开始运行的时候就有了自己的逻辑cpu号。
sched_init();//初始化调度器,对调度机制进行初始化,对每个CPU的运行队列
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);
//执行命令行解析,若参数不存在,则调用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();//linux2.6的一种互斥访问机制
init_IRQ();//中断向量初始化
pidhash_init();//进程嘻哈表初始化
init_timers();//定时器初始化
hrtimers_init();//高精度时钟初始化
softirq_init();//软中断初始化
timekeeping_init();//系统时间初始化
time_init();
sched_clock_init();
profile_init();//空函数
if (!irqs_disabled())
printk("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
vfs_caches_init_early();//建立节点嘻哈表和数据缓冲嘻哈表
cpuset_init_early();//空函数
mem_init();//对全局的物理页变量初始化,对没有分配的页面初始化
enable_debug_pagealloc();
cpu_hotplug_init();//没有热插拔CPU,此函数为空
kmem_cache_init();//内核内存缓冲区初始化
debug_objects_mem_init();
idr_init_cache();//创建idr缓冲区
setup_per_cpu_pageset();//采用的是一致性内存,此函数为空
numa_policy_init();//采用的是一致性内存,此函数为空
if (late_time_init)
late_time_init();
calibrate_delay();//校准延时函数的精确度,实际上是校准loops_per_jiffy全局变量,即每个时钟滴答内CPU执行的指令数
pidmap_init();//进程号位图初始化,一般用一个page来指示所有的进程PID占用情况
pgtable_cache_init();//空函数
prio_tree_init();//初始化优先级数组
anon_vma_init();//空函数
#ifdef CONFIG_X86
if (efi_enabled)
efi_enter_virtual_mode();
#endif
thread_info_cache_init();//空函数
fork_init(num_physpages);//初始化kernel的fork()环境。Linux下应用程序执行是靠系统调用fork()完成,fork_init所完成的工作就是确定可以fork()的线程的数量,然后是初始化init_task进程
proc_caches_init();//为proc文件系统创建高速缓存
buffer_init();//空函数
unnamed_dev_init();//初始化一个虚拟文件系统使用的哑文件
key_init();//没有键盘则为空,如果有键盘,则为键盘分配一个高速缓存
security_init();//空函数
vfs_caches_init(num_physpages);//虚拟文件系统挂载,这个函数的详细说明如下
void __init vfs_caches_init(unsigned long mempages)//参数说明系统内存的物理页数
{
unsigned long reserve;
reserve = min((mempages - nr_free_pages()) * 3/2, mempages - 1);
mempages -= reserve;
//创建一个高速缓存
names_cachep = kmem_cache_create("names_cache", PATH_MAX, 0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL);
filp_cachep = kmem_cache_create("filp", sizeof(struct file), 0,
SLAB_HWCACHE_ALIGN|SLAB_PANIC, NULL);
dcache_init();//在高速缓存中分配一个目录项,并初始化
inode_init();//在高速缓存中分配一个inode节点,并初始化
files_init(mempages);//初始化文件描述符,初始化全局文件状态变量
mnt_init();
bdev_cache_init();//如果编译阶段设置了块设备,则注册一个块设备文件系统
chrdev_init();//初始化字符设备管理数组cdev_map
}
// mnt_init()是创建根文件系统的关键,解释如下
void __init mnt_init(void)
{
unsigned u;
int err;
init_rwsem(&namespace_sem);
//创建一个虚拟文件系统的vfsmount结构缓存。每个挂载的文件系统都有一个
struct vfsmoun结构
mnt_cache = kmem_cache_create("mnt_cache", sizeof(struct vfsmount),
0, SLAB_HWCACHE_ALIGN | SLAB_PANIC, NULL);
//创建文件系统挂载嘻哈表
mount_hashtable = (struct list_head *)__get_free_page(GFP_ATOMIC);
if (!mount_hashtable)
panic("Failed to allocate mount hash table/n");
printk("Mount-cache hash table entries: %lu/n", HASH_SIZE);
//初始化嘻哈表
for (u = 0; u < HASH_SIZE; u++)
INIT_LIST_HEAD(&mount_hashtable[u]);
err = sysfs_init();//创建一个sysfs虚拟文件系统,并挂载为根文件系统。如果系统不指定sysfs,则此函数为空
if (err)
printk(KERN_WARNING "%s: sysfs_init error: %d/n",
__func__, err);
fs_kobj = kobject_create_and_add("fs", NULL);//创建一个对象文件,加到文件系统中
if (!fs_kobj)
printk(KERN_WARNING "%s: kobj create error/n", __func__);
init_rootfs();//注册一个rootfs文件系统
init_mount_tree();//将上面创建的rootfs文件系统挂载为根文件系统。这只是个虚拟的文件系统,就好比只是创建了一个/目录。最后,这个函数会为系统最开始的进程(即 init_task 进程)准备他的进程数据块中的namespace域,主要目的是将 do_kern_mount()函数中建立的 mnt 和 dentry 信息记录在了 init_task进程的进程数据块中,这样任何以后从 init_task进程 fork出来的进程也都先天地继承了这一信息。
//下面是进行radix树初始化。这个是linux2.6引入的为各种页面操作的重要结构之一struct radix_tree_node。这种数据结构将指针与一个long型键值关联起来,提供高效快速查找。具体不再分析
radix_tree_init();
signals_init();//创建并初始化信号队列
/* rootfs populating might need page-writeback */
page_writeback_init();//CPU在内存中开辟高速缓存,CPU直接访问高速缓存提以高速度。当cpu更新了高速缓存的数据后,需要定期将高速缓存的数据写回到存储介质中,比如磁盘和flash等。这个函数初始化写回的周期
#ifdef CONFIG_PROC_FS
proc_root_init();//如果配置了proc文件系统,则需初始化并加载proc文件系统。在根目录的proc文件夹就是proc文件系统,这个文件系统是ram类型的,记录系统的临时数据,系统关机后不会写回到flash中
#endif
cgroup_init();//没有配置cgroup,此函数为空
cpuset_init();//单CPU,此函数为空
taskstats_init_early();//进程状态初始化,实际上就是分配了一个存储线程状态的高速缓存
delayacct_init();//空函数
check_bugs();//空函数
acpi_early_init();//空函数
rest_init();//start_kernel启动的最后一个函数,进入这个函数,完成剩余启动的初始化
}
// rest_init()大致解释如下:
static void noinline __init_refok rest_init(void)
__releases(kernel_lock)
{
int pid;
//创建内核线程。Kernel_thread运用系统调用do_fork()产生新的子线程,子线程就调用传入的调用函数执行之,此处函数就是kernel_init. kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
numa_default_policy();//空函数
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
unlock_kernel();
/*
* The boot idle thread must execute schedule()
* at least once to get things moving:
*/
init_idle_bootup_task(current);
preempt_enable_no_resched();
schedule();
preempt_disable();
/* Call into cpu_idle with preempt disabled */
cpu_idle();
}
// kernel_init通过调用do_basic_setup完成编译阶段注册的设备驱动程序初始化。
//这个函数又调用了一个很重要的初始化函数Do_initcalls()。它用来启动所有在__initcall_start和__initcall_end段的函数,而静态编译进内核的modules也会将其入口放置在这段区间里。和根文件系统相关的初始化函数都会由rootfs_initcall()所引用。rootfs_initcall(populate_rootfs);
也就是说会在系统初始化的时候会调用populate_rootfs进行初始化。代码如下:
static int __init populate_rootfs(void)
{
char *err = unpack_to_rootfs(__initramfs_start,
__initramfs_end - __initramfs_start, 0);
if (err)
panic(err);
if (initrd_start) {
#ifdef CONFIG_BLK_DEV_RAM
int fd;
printk(KERN_INFO "checking if image is initramfs...");
err = unpack_to_rootfs((char *)initrd_start,
initrd_end - initrd_start, 1);
if (!err) {
printk(" it is/n");
unpack_to_rootfs((char *)initrd_start,
initrd_end - initrd_start, 0);
free_initrd();
return 0;
}
printk("it isn't (%s); looks like an initrd/n", err);
fd = sys_open("/initrd.image", O_WRONLY|O_CREAT, 0700);
if (fd >= 0) {
sys_write(fd, (char *)initrd_start,
initrd_end - initrd_start);
sys_close(fd);
free_initrd();
}
#else
printk(KERN_INFO "Unpacking initramfs...");
err = unpack_to_rootfs((char *)initrd_start,
initrd_end - initrd_start, 0);
if (err)
panic(err);
printk(" done/n");
free_initrd();
#endif
}
return 0;
}
//unpack_to_rootfs就是解压包,所解得包就是usr/initramfs_data.cpio.gz下的文件系统。然后将其释放至上面创建的rootfs。注意这个文件系统已经在编译的时候用build_in.O的方式一种是跟kernel融为一体了所在的段就是__initramfs_start至__initramfs_end的区域。这种情况下,直接调用unpack_to_rootfs将其释放到根目录.如果不是属于这种形式的。也就是由内核参数指定的文件系统,即image-initrd文件系统。如果配制CONFIG_BLK_DEV_RAM才会支持image-initrd。否则全当成cpio-initrd的形式处理。
对于是cpio-initrd的情况。直接将其释放到根目录。对于是image-initrd的情况。在根目录下建立/initrd.image文件,然后将INITRD写到这个文件中,并释放INITRD占用的空间。。
接下来,就开始具体的挂载操作,在kernel_init函数中完成
static int __init kernel_init(void * unused)
{
lock_kernel();
/*
* init can run on any cpu.
*/
set_cpus_allowed_ptr(current, CPU_MASK_ALL_PTR);
/*
* Tell the world that we're going to be the grim
* reaper of innocent orphaned children.
*
* We don't want people to have to make incorrect
* assumptions about where in the task array this
* can be found.
*/
init_pid_ns.child_reaper = current;
cad_pid = task_pid(current);
smp_prepare_cpus(setup_max_cpus);
do_pre_smp_initcalls();
smp_init();
sched_init_smp();
cpuset_init_smp();
do_basic_setup();
if (!ramdisk_execute_command)
ramdisk_execute_command = "/init";
//如果存在指定的INITRD命令行参数,则执行命令行参数指定的init文件,如果不存在,则制定执行的命令为根目录下的init文件。如果用户指定的文件系统存在,则调用prepare_namespace();进行文件系统挂载的预操作
if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command = NULL;
//在prepare_namespace()中首先会轮训检测块设备,若检测到则创建一个设备节点,然后分配一个设备号。如果saved_root_name不为空,则说明内核有指定设备作为根文件系统,则通过mount_block_root挂载根文件系统,然后退出即可。
比如有时指定了内核参数root=/dev/ram,则直接从这个位置进行挂载。
如果没有指定的块设备作为根文件系统,而是指明了INITRD映像,则调用initrd_load函数挂载initram文件系统。这个函数首先创建/dev/ram设备节点,然后把映像拷贝到这个设备文件中,接着调用handle_initrd对INITRD进行处理。
在prepare_namespace()执行最后调用mount_root();将指定的文件系统挂接到/root下,然后切换当前目录到root下。再者,还需调用sys_mount(".", "/", NULL, MS_MOVE, NULL);将当前目录挂接为/根目录。
prepare_namespace();
}
init_post();
return 0;
}
static void __init handle_initrd(void)
{
int error;
int pid;
real_root_dev = new_encode_dev(ROOT_DEV);//真正的根文件节点
create_dev("/dev/root.old", Root_RAM0);//创建一个设备节点,设备号是Root_RAM0,因此这个节点对应是/dev/ram的INITRD
/* mount initrd on rootfs' /root */
//将此设备挂接到/root下
mount_block_root("/dev/root.old", root_mountflags & ~MS_RDONLY);
sys_mkdir("/old", 0700);
root_fd = sys_open("/", 0, 0);//记录根目录的文件描述符
old_fd = sys_open("/old", 0, 0);//记录old目录的文件描述符
/* move initrd over / and chdir/chroot in initrd root */
sys_chdir("/root");//切换至root目录,刚才已经挂载了/dev/root.old
sys_mount(".", "/", NULL, MS_MOVE, NULL);//将当前目录挂载为根文件系统,也就是/dev/root.old变成现在的根文件系统
sys_chroot(".");//切换到当前文件系统的根目录
current->flags |= PF_FREEZER_SKIP;
//创建一个进程,运行目前文件系统下的/linuxrc文件
pid = kernel_thread(do_linuxrc, "/linuxrc", SIGCHLD);
if (pid > 0)
while (pid != sys_wait4(-1, NULL, 0, NULL))
yield();
current->flags &= ~PF_FREEZER_SKIP;
/* move initrd to rootfs' /old */
sys_fchdir(old_fd);
sys_mount("/", ".", NULL, MS_MOVE, NULL);
//处理完上面的文件,则initrd处理完之后,重新chroot进入rootfs
/* switch root and cwd back to / of rootfs */
sys_fchdir(root_fd);
sys_chroot(".");
sys_close(old_fd);
sys_close(root_fd);
//如果real_root_dev在 linuxrc中重新设成Root_RAM0,则initrd就是最终的realfs了,改变当前目录到initrd中,不作后续处理直接返回。
if (new_decode_dev(real_root_dev) == Root_RAM0) {
sys_chdir("/old");
return;
}
//否则需要重新挂载上面linuxRC文件执行时指定的根文件系统
ROOT_DEV = new_decode_dev(real_root_dev);
mount_root();
printk(KERN_NOTICE "Trying to move old root to /initrd ... ");
error = sys_mount("/old", "/root/initrd", NULL, MS_MOVE, NULL);
if (!error)
printk("okay/n");
else {
int fd = sys_open("/dev/root.old", O_RDWR, 0);
if (error == -ENOENT)
printk("/initrd does not exist. Ignored./n");
else
printk("failed/n");
printk(KERN_NOTICE "Unmounting old root/n");
sys_umount("/old", MNT_DETACH);
printk(KERN_NOTICE "Trying to free ramdisk memory ... ");
if (fd < 0) {
error = fd;
} else {
error = sys_ioctl(fd, BLKFLSBUF, 0);
sys_close(fd);
}
printk(!error ? "okay/n" : "failed/n");
}
}
static int noinline init_post(void)
{
free_initmem();
unlock_kernel();
mark_rodata_ro();
system_state = SYSTEM_RUNNING;
numa_default_policy();
if (sys_open((const char __user *) "/dev/console", O_RDWR, 0) < 0)
printk(KERN_WARNING "Warning: unable to open an initial console./n");
(void) sys_dup(0);
(void) sys_dup(0);
current->signal->flags |= SIGNAL_UNKILLABLE;
//刚才上面已经初始化了ramdisk_execute_command,此处可直接运行之。
如果不存在则运行下面程序,如果都不存在,则退出。
下面的/sbin/init就是上述挂载的根文件系统下的文件。
if (ramdisk_execute_command) {
run_init_process(ramdisk_execute_command);
printk(KERN_WARNING "Failed to execute %s/n",
ramdisk_execute_command);
}
if (execute_command) {
run_init_process(execute_command);
printk(KERN_WARNING "Failed to execute %s. Attempting "
"defaults.../n", execute_command);
}
run_init_process("/sbin/init");
run_init_process("/etc/init");
run_init_process("/bin/init");
run_init_process("/bin/sh");
panic("No init found. Try passing init= option to kernel.");
}
//当没有找到init程序后,则退出,进行进程调度,进入cpu_idle()进程
====================================================================================
第四部分:C部分
Android启动之init.c
在Android系统启动时,内核引导参数上一般都会设置“init=/init”,这样的话,如果内核成功挂载了这个文件系统之后,首先运行的就是这个根目录下的init程序。这个程序所了什么呢?我们只有RFSC(Read the Fucking Source code)!!
init程序源码在Android官方源码的system/core/init中,main在init.c里。我们的分析就从main开始 init:(1)init是一个守护进程,为了防止init的子进程成为僵尸进程(zombie process),需要init在子进程在结束时获取子进程的结束码,通过结束码将程序表中的子进程移除,防止成为僵尸进程的子进程占用程序表的空间,当程序表的空间达到上限时,则系统就不能再启动新的进程了,那么就会引起很严重的系统问题。在linux当中,父程序是通过捕捉SIGCHLD信号来得知子进程结束的情况的;由于系统默认在子进程暂停时也会发送信号SIGCHLD,init需要忽略子进程在暂停时发出的SIGCHLD信号,因此将act.sa_flags 置为SA_NOCLDSTOP,该标志位的含义是就是要求系统在子进程暂停时不发送SIGCHLD信号。 static void sigchld_handler(ints) { write(signal_fd, &s, 1); } int main(int argc, char **argv) { act.sa_handler= sigchld_handler; act.sa_flags= SA_NOCLDSTOP; sigaction(SIGCHLD,&act, 0); structsigaction act; act.sa_handler= sigchld_handler; act.sa_flags= SA_NOCLDSTOP; ……………………………………….. } Linux进程通过互相发送接收消息来实现进程间的通信,这些消息被称为“信号”。每个进程在处理其他进程发送的信号时都需要注册程序,此程序被称为信号处理。当进程的运行状态改变或者终止时,就会产生某种信号,init进程是所有进程的父进程,当其子进程终止产生SIGCHLD信号时,init进程需要调用信号安装函数sigaction(),并通过参数传递至sigcation结构体中,已完成信号处理器的安装。 Init进程通过上述代码注册与子进程相关的SIGCHLD信号处理器,并把sigcation结构体的sa_flags设置为SA_NOCLDSTOP,该值表示仅当进程终止时才接收SIGCHLD信号。 sigchld_handler函数用于通知全局变量signal_fd,SIGCHLD信号已发生。对于产生的信号的实际处理,在init进程的事件处理循环中进行。
(2)对umask进行清零。 何为umask,请看http://www.szstudy.cn/showArticle/53978.shtml umask是什么?
当我们登录系统之后创建一个文件总是有一个默认权限的,那么这个权限是怎么来的呢?这就是umask干的事情。umask设置了用户创建文件的默认权限,它与chmod的效果刚好相反,umask设置的是权限“补码”,而chmod设置的是文件权限码。一般在/etc/profile、$ [HOME]/.bash_profile或$[HOME]/.profile中设置umask值。
如何计算umask值? umask命令允许你设定文件创建时的缺省模式,对应每一类用户(文件属主、同组用户、其他用户)存在一个相应的umask值中的数字。对于文件来说,这一数字的最大值分别是6。系统不允许你在创建一个文本文件时就赋予它执行权限,必须在创建后用chmod命令增加这一权限。目录则允许设置执行权限,这样针对目录来说,umask中各个数字最大可以到7。 该命令的一般形式为:umasknnn 其中nnn为umask置000 - 777。 如:umask值为022,则默认目录权限为755,默认文件权限为644。
(3)为rootfs建立必要的文件夹,并挂载适当的分区。 /dev (tmpfs) /dev/pts (devpts) /dev/socket /proc (proc) /sys (sysfs) 编译Android系统源码时,在生成的根文件系统中,不存在/dev,/proc/,/sys这类目录,他们是系统运行时的目录,有init进程在运行中生成,当系统终止时,他们就会消失。 Init进程执行后,生成/dev目录,包含系统使用的设备,而后调用open_devnull_stdio();函数,创建运行日志输出设备。open_devnull_stdio()函数会在/dev目录下生成__null__设备节点文件,并将标准输入,标准输出,标准错误,标准错误输出全部重定向__null__设备中。 void open_devnull_stdio(void) { intfd; static const char *name = "/dev/__null__"; if(mknod(name, S_IFCHR | 0600, (1 << 8) | 3) == 0) { fd = open(name, O_RDWR); unlink(name); if (fd >= 0) { dup2(fd, 0); dup2(fd, 1); dup2(fd, 2); if (fd > 2) { close(fd); } return; } } exit(1); }
(4)创建/dev/null和/dev/kmsg节点。 init进程通过log_init函数,生成"/dev/__kmsg__"设备节点文件。__kmsg__设备调用内核信息输出函数printk(),init进程即是通过该函数输出log信息。 void log_init(void) { staticconst char *name = "/dev/__kmsg__"; if(mknod(name, S_IFCHR | 0600, (1 << 8) | 11) == 0) { log_fd = open(name, O_WRONLY); fcntl(log_fd, F_SETFD, FD_CLOEXEC); unlink(name); } } Init进程通过__kmsg__设备定义用于输出信息的宏。关于宏输出信息,可以使用dmesg实用程序进行确认,dmesg用于显示内核信息。 #define ERROR(x...) log_write(3, "<3>init: " x) #define NOTICE(x...) log_write(5, "<5>init: " x) #define INFO(x...) log_write(6, "<6>init: " x)
(5)解析/init.rc,将所有服务和操作信息加入链表
parse_config_file("/init.rc"); parse_config_file()函数用来分析*.rc配置文件,用来指定init.rc文件的路径。执行parse_config_file函数,读取并分析init.rc文件后,生成服务列表与动作列表。动作列表与服务列表全部会以链表的形式注册到service_list和action_list中,service_list和action_list是init进程中声明的全局结构体。 (6) 初始化qemu设备,设置模拟器环境;从/proc/cmdline中提取信息内核启动参数,并保存到全局变量。
qemu_init(); QEMU模拟器允许Android应用开发者在缺少Android实际设备的情况下运行处于开发中的应用程序。QEMU是面向PC的开源模拟器,能够模拟具有特定处理器设备,此外还提供虚拟网络,视频设备等。Android模拟器是虚拟的硬件平台,运行在模拟器的软件可以执行ARM命令集,LCD,相机,SD卡控制器等硬件设备都可以运行在Goldfish这种虚拟平台上。 import_kernel_cmdline(0); staticvoid import_kernel_cmdline(int in_qemu) { char cmdline[1024]; char *ptr; int fd; fd = open("/proc/cmdline",O_RDONLY); if (fd >= 0) { int n = read(fd, cmdline, 1023); if (n < 0) n = 0; /* get rid of trailing newline, ithappens */ if (n > 0 && cmdline[n-1] =='\n') n--; cmdline[n] = 0; close(fd); } else { cmdline[0] = 0; } ptr = cmdline; while (ptr && *ptr) { char *x = strchr(ptr, ' '); if (x != 0) *x++ = 0; import_kernel_nv(ptr, in_qemu); ptr = x; }
(7)先从上一步获得的全局变量中获取信息硬件信息和版本号,如果没有则从/proc/cpuinfo中提取,并保存到全局变量。
(8)根据硬件信息选择一个/init.(硬件).rc,并解析,将服务和操作信息加入链表。 在G1的ramdisk根目录下有两个/init.(硬件).rc:init.goldfish.rc和init.trout.rc,init程序会根据上一步获得的硬件信息选择一个解析。
(9)执行链表中带有“early-init”触发的的命令。 action_for_each_trigger("early-init",action_add_queue_tail);触发在init脚本文件中名字为early-init的action,并且执行其commands,其实是:on early-init,在我们的init.rc中是没有的。action_for_each_trigger函数会将第一个参数中的命令保存到action_add_queue_tail,而后通过drain_action_queue()函数将运行队列中的命令逐一取出执行。
(10)遍历/sys文件夹, 将这些目录下的uevent文件找出,并使kernel重新生成那些在init的设备管理器开始前的设备添加事件。 初始化动态设备管理,使内核产生设备添加事件(为了自动产生设备节点), 设备文件有变化时反应给内核。
device_fd = device_init(); int device_init(void) { suseconds_t t0, t1; int fd; fd = open_uevent_socket(); if(fd < 0) return -1; fcntl(fd, F_SETFD, FD_CLOEXEC); fcntl(fd, F_SETFL, O_NONBLOCK); t0 = get_usecs(); coldboot(fd, "/sys/class"); coldboot(fd, "/sys/block"); coldboot(fd, "/sys/devices"); t1 = get_usecs(); log_event_print("coldboot %ld uS\n", ((long) (t1 - t0))); make_device("/dev/pvrsrvkm", 0, 240, 0); #if 0 make_device("/dev/bc_example", 0, 242, 0); #endif #if 1 make_device("/dev/fb0", 0, 29, 0); make_device("/dev/fb1", 0, 29, 1); make_device("/dev/fb2", 0, 29, 2); make_device("/dev/fb3", 0, 29, 3); make_device("/dev/fb4", 0, 29, 4); #endif return fd; }
(11)初始化属性系统,并导入初始化属性文件。
初始化属性服务器,Actually theproperty system is working as share memory.Logically it looks like a registry underwindows system。 首先创建一个名字为system_properties的匿名共享内存区域,对并本init进程做mmap读写映射,其余共享它 的进程只有读的权限。然后将这个prop_area结构体通过全局变量__system_property_area__传递给property services。 接着调用函数load_properties_from_file(PROP_PATH_RAMDISK_DEFAULT)从/default.prop文件中加载编译时生成的属性。这个有点像Windows 下的注册表的作用。 在Android系统中,所有的进程共享系统设置值,为此提供了一个名称为属性的保存空间。Init进程调用property_init()函数,在共享内存区域中,创建并初始化属性域。而后通过执行中的进程锁提供的API,访问属性中的设置值。但更改属性值只能在init进程中进行。当修改属性值时,要预先向init进程提交值变更申请,然后init进程处理该申请,并修改属性值。
(12)从属性系统中得到ro.debuggable,如果ro.debuggable为1,则初始化组合键(keychord )监听 这段代码是从属性里获取调试标志,如果是可以调试,就打开组合按键输入驱动程序,初始化keychord监听。
// only listen for keychords ifro.debuggable is true debuggable =property_get("ro.debuggable"); if (debuggable &&!strcmp(debuggable, "1")) { keychord_fd = open_keychord(); }
(13)打開console,如果cmdline中沒有指定console則打開默認的/dev/console。
if (console[0]) { snprintf(tmp, sizeof(tmp),"/dev/%s", console); console_name = strdup(tmp); } //打开console,如果cmdline 中没有指定console 则打开默认的/dev/console fd = open(console_name, O_RDWR); if (fd >= 0) have_console = 1; close(fd);
(14)读取/initlogo.rle,是一张565 rle 压缩的位图,如果成功则在/dev/fb0显示Logo,如果失败则将/dev/tty0设为TEXT模式并打开/dev/tty0,输出文本的ANDROID字样。
load_565rle_image(INIT_IMAGE_FILE)函数将加载由参数传递过来的图像文件,而后将该文件显示在LCD屏幕上。如果想更改logo,只需修改INIT_IMAGE_FILE即可。由于函数只支持rle565格式图像的显示,再更改图像时,注意所选图像文件的格式。
(15) 这段代码是用来判断是否使用模拟器运行,如果是,就加载内核命令行参数。
if (qemu[0]) import_kernel_cmdline(1);
(16)这段代码是根据内核命令行参数来设置工厂模式测试,比如在工厂生产手机过程里需要自动化演示功能,就可以根据这个标志来进行特别处理。
if(!strcmp(bootmode,"factory")) property_set("ro.factorytest","1"); else if(!strcmp(bootmode,"factory2")) property_set("ro.factorytest","2"); else property_set("ro.factorytest","0"); //这段代码是设置手机序列号到属性里保存,以便上层应用程序可以识别这台手机。 property_set("ro.serialno",serialno[0] ? serialno : ""); //这段代码是保存启动模式到属性里。 property_set("ro.bootmode",bootmode[0] ? bootmode : "unknown"); //这段代码是保存手机基带频率到属性里。 property_set("ro.baseband",baseband[0] ? baseband : "unknown"); //这段代码是保存手机硬件载波的方式到属性里。 property_set("ro.carrier",carrier[0] ? carrier : "unknown"); //保存引导程序的版本号到属性里,以便系统知道引导程序有什么特性。 property_set("ro.bootloader",bootloader[0] ? bootloader : "unknown"); //这里是保存硬件信息到属性里,其实就是获取CPU 的信息。 property_set("ro.hardware",hardware); //这里是保存硬件修订的版本号到属性里,这样可以方便应用程序区分不同的硬件版本。 snprintf(tmp,PROP_VALUE_MAX, "%d", revision); property_set("ro.revision",tmp); /* (17)執行所有触发标识为init的action。
/* execute all the boot actions to get usstarted */ //执行所有触发标志为init的action action_for_each_trigger("init",action_add_queue_tail); drain_action_queue();
(18)開始property服務
/* read any property files on system or dataand * fire up the property service. This musthappen * after the ro.foo properties are set aboveso * that /data/local.prop cannot interferewith them. */ /* 开始property 服务,读取一些property 文件,这一动作必须在前面那些ro.foo设置后做, 以便/data/local.prop 不能干预到他们 - /system/build.prop - /system/default.prop - /data/local.prop - 在读取认识的property 后读取presistentpropertie,在/data/property 中 这段代码是加载system 和data 目录下的属性,并启动属性监听服务。
(19)為sigchld handler創建信號機制。
property_set_fd = start_property_service();
(20)创建一个全双工的通讯机制的两个SOCKET
/* 这段代码是创建一个全双工的通讯机制的两个SOCKET,信号可以在signal_fd 和signal_recv_fd 双向通讯,从而建立起沟通的管道。其实这个信号管理,就 是用来让init 进程与它的子进程进行沟通的,子进程从signal_fd 写入信息,init 进程从signal_recv_fd 收到信息,然后再做处理。 */ /* create a signalling mechanism for thesigchld handler */ if (socketpair(AF_UNIX, SOCK_STREAM, 0, s)== 0) { signal_fd = s[0]; signal_recv_fd = s[1]; fcntl(s[0], F_SETFD, FD_CLOEXEC); fcntl(s[0], F_SETFL, O_NONBLOCK); fcntl(s[1], F_SETFD, FD_CLOEXEC); fcntl(s[1], F_SETFL, O_NONBLOCK); } /* make sure we actually have all thepieces we need */ /*
(21)这段代码是判断关键的几个组件是否成功初始化,主要就是设备文件系统是否成功初始化,属性服务是否成功初始化,信号通讯机制是否成功初始化。
if((device_fd < 0) || (property_set_fd< 0) || (signal_recv_fd< 0)) { ERROR("initstartup failure\n"); return1; }
(22)執行所有触发标识为early-boot的
action action_for_each_trigger("early-boot",action_add_queue_tail); //执行所有触发标志为boot的action action_for_each_trigger("boot",action_add_queue_tail); drain_action_queue();
(23)基于當前property狀態,執行所有触发标识为property的action
//这段代码是根据当前属性,运行属性命令。 queue_all_property_triggers(); drain_action_queue(); /* enable propertytriggers */ // 标明属性触发器已经初始化完成。 property_triggers_enabled= 1; /* 这段代码是保存三个重要的服务socket,以便后面轮询使用。
*/ ufds[0].fd =device_fd; ufds[0].events =POLLIN; ufds[1].fd =property_set_fd; ufds[1].events =POLLIN; ufds[2].fd =signal_recv_fd; ufds[2].events =POLLIN; fd_count = 3; //这段代码是判断是否处理组合键轮询。 ufds[3].events = POLLIN; fd_count++; } else { ufds[3].events = 0; ufds[3].revents = 0; } //如果支持BOOTCHART,则初始化BOOTCHART #if BOOTCHART /* 这段代码是初始化linux 程序启动速度的性能分析工具,这个工具有一个好处,就是图形化显示每个进程启动顺序和占用时间,如果想优化系统的启动速度,记得启用这个工具。
*/ bootchart_count = bootchart_init(); if (bootchart_count < 0) { ERROR("bootcharting initfailure\n"); } else if (bootchart_count > 0) { NOTICE("bootcharting started(period=%d ms)\n", bootchart_count*BOOTCHART_POLLING_MS); } else { NOTICE("bootcharting ignored\n"); } #endif /* 进入主进程循环:
- 重置轮询事件的接受状态,revents 为0 - 查询action 队列,并执行。 - 重启需要重启的服务 - 轮询注册的事件 - 如果signal_recv_fd 的revents 为POLLIN,则得到一个信号,获取并处理 - 如果device_fd 的revents 为POLLIN,调用handle_device_fd - 如果property_fd 的revents 为POLLIN,调用handle_property_set_fd - 如果keychord_fd 的revents 为POLLIN,调用handle_keychord 这段代码是进入死循环处理,以便这个init 进程变成一个服务。 */ for(;;) { int nr, i, timeout = -1; // 清空每个socket 的事件计数。 for (i = 0; i < fd_count; i++) ufds[i].revents = 0; // 这段代码是执行队列里的命令。 drain_action_queue(); // 这句代码是用来判断那些服务需要重新启动。 restart_processes(); // 这段代码是用来判断哪些进程启动超时。 if (process_needs_restart) { timeout = (process_needs_restart -gettime()) * 1000; if (timeout < 0) timeout = 0; } #if BOOTCHART //这段代码是用来计算运行性能。 if (bootchart_count > 0) { if (timeout < 0 || timeout >BOOTCHART_POLLING_MS) timeout = BOOTCHART_POLLING_MS; if (bootchart_step() < 0 ||--bootchart_count == 0) { bootchart_finish(); bootchart_count = 0; } } #endif // 这段代码用来轮询几个socket 是否有事件处理。 nr = poll(ufds, fd_count, timeout); if (nr <= 0) continue; /* 这段代码是用来处理子进程的通讯,并且能删除任何已经退出或者杀死进程,这样做可以保持系统更加健壮性,增强容错能力。
*/ if (ufds[2].revents == POLLIN) { /* we got a SIGCHLD - reap and restart asneeded */ read(signal_recv_fd, tmp, sizeof(tmp)); while (!wait_for_one_process(0)) ; continue; } // 这段代码是处理设备事件。 if (ufds[0].revents == POLLIN) handle_device_fd(device_fd); // 这段代码是处理属性服务事件。 if (ufds[1].revents == POLLIN) handle_property_set_fd(property_set_fd); // 这段代码是处理调试模式下的组合按键。 if (ufds[3].revents == POLLIN) handle_keychord(keychord_fd); } return 0; }
概括起来大体上也可做以下分析:
init:
(1)安装SIGCHLD信号。(如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。因此需要对SIGCHLD信号做出处理,回收僵尸进程的资源,避免造成不必要的资源浪费。
(2)对umask进行清零。
(3)为rootfs建立必要的文件夹,并挂载适当的分区。
(5)解析/init.rc,将所有服务和操作信息加入链表。
(7)先从上一步获得的全局变量中获取信息硬件信息和版本号,如果没有则从/proc/cpuinfo中提取,并保存到全局变量。
(8)根据硬件信息选择一个/init.(硬件).rc,并解析,将服务和操作信息加入链表。
(9)执行链表中带有“early-init”触发的的命令。
(10)遍历/sys文件夹,是内核产生设备添加事件(为了自动产生设备节点)。
(11)初始化属性系统,并导入初始化属性文件。
(12)从属性系统中得到ro.debuggable,若为1,則初始化keychord監聽。
(13)打開console,如果cmdline中沒有指定console則打開默認的/dev/console。
(14)讀取/initlogo.rle(一張565 rle壓縮的位圖),如果成功則在/dev/graphics/fb0顯示Logo,如果失敗則將/dev/tty0設為TEXT模式并打開/dev/tty0,輸出文本“ANDROID”字樣。
(15)判斷cmdline 中的參數,并设置属性系统中的参数:
(16)執行所有触发标识为init的action。
(17)開始property服務,讀取一些property文件,這一動作必須在前面那些ro.foo設置后做,以便/data/local.prop不能干預到他們。
(18)為sigchld handler創建信號機制。
(19)確認所有初始化工作完成:
(20) 執行所有触发标识为early-boot的action
(21) 執行所有触发标识为boot的action
(22)基于當前property狀態,執行所有触发标识为property的action
(23)注冊輪詢事件:
(24)如果支持BOOTCHART,則初始化BOOTCHART
(25)進入主進程死循環:
第五部分:C部分+Java部分
Android启动之开启系统服务
经过init线程启动之后,可分成两个方向:1、正常的system方向;2、recovery系统方向。
以下具体分析:
1、正常的system方向
第1步:
1)
2)
a)
b)
c)
第2步: 系统服务systemserver
1)
2)
第3步:桌面launcher
1)
2)
第4步:
1)
frameworks/policies/base/phone/com/android/internal/policy/impl/*lock*
2)
第5步:
1)
frameworks/base/services/java/com/android/server/am/ActivityManagerService.java
2)
第6步:总结
综上所述,系统层次关于启动最核心的部分是zygote(即app_process)和systemserver,zygote它负责最基本的虚拟机的建立,以支持各个应用程序的启动,而systemserver用于管理android后台服务,启动步骤及顺序。
2、recovery系统方向
执行recovery二进制可执行程序文件,进入recovery main函数:
代码路径在 android 源码的根路径: bootable\recovery 其入口文件就是 recovery.c 中 main函数
下面就开始逐步了解其Recovery的设计思想:
static const char *COMMAND_FILE = "/cache/recovery/command";
static const char *INTENT_FILE = "/cache/recovery/intent";
static const char *LOG_FILE = "/cache/recovery/log";
注解里面描述的相当清楚:
* The recovery tool communicates with the main system through /cache files.
* /cache/recovery/command - INPUT - command line for tool, one arg per line
* /cache/recovery/log - OUTPUT - combined log file from recovery run(s)
* /cache/recovery/intent - OUTPUT - intent that was passed in
static const char *LAST_LOG_FILE = "/cache/recovery/last_log";
static const char *LAST_INSTALL_FILE = "/cache/recovery/last_install";
static const char *CACHE_ROOT = "/cache";
static const char *SDCARD_ROOT = "/sdcard";
下面的描述针对写入的 command 有大致的介绍:
* The arguments which may be supplied in the recovery.command file:
* --send_intent=anystring - write the text out to recovery.intent
* --update_package=path - verify install an OTA package file
* --wipe_data - erase user data (and cache), then reboot
* --wipe_cache - wipe cache (but not user data), then reboot
* --set_encrypted_filesystem=on|off - enables / diasables encrypted fs
两种升级模式步骤说明:
* After completing, we remove /cache/recovery/command and reboot.
* Arguments may also be supplied in the bootloader control block (BCB).
* These important scenarios must be safely restartable at any point:
*
* FACTORY RESET
* 1. user selects "factory reset"
* 2. main system writes "--wipe_data" to /cache/recovery/command
* 3. main system reboots into recovery
* 4. get_args() writes BCB with "boot-recovery" and "--wipe_data"
* -- after this, rebooting will restart the erase --
* 5. erase_volume() reformats /data
* 6. erase_volume() reformats /cache
* 7. finish_recovery() erases BCB
* -- after this, rebooting will restart the main system --
* 8. main() calls reboot() to boot main system
*
* OTA INSTALL
* 1. main system downloads OTA package to /cache/some-filename.zip
* 2. main system writes "--update_package=/cache/some-filename.zip"
* 3. main system reboots into recovery
* 4. get_args() writes BCB with "boot-recovery" and "--update_package=..."
* -- after this, rebooting will attempt to reinstall the update --
* 5. install_package() attempts to install the update
* NOTE: the package install must itself be restartable from any point
* 6. finish_recovery() erases BCB
* -- after this, rebooting will (try to) restart the main system --
* 7. ** if install failed **
* 7a. prompt_and_wait() shows an error icon and waits for the user
* 7b; the user reboots (pulling the battery, etc) into the main system
* 8. main() calls maybe_install_firmware_update()
* ** if the update contained radio/hboot firmware **:
* 8a. m_i_f_u() writes BCB with "boot-recovery" and "--wipe_cache"
* -- after this, rebooting will reformat cache & restart main system --
* 8b. m_i_f_u() writes firmware image into raw cache partition
* 8c. m_i_f_u() writes BCB with "update-radio/hboot" and "--wipe_cache"
* -- after this, rebooting will attempt to reinstall firmware --
* 8d. bootloader tries to flash firmware
* 8e. bootloader writes BCB with "boot-recovery" (keeping "--wipe_cache")
* -- after this, rebooting will reformat cache & restart main system --
* 8f. erase_volume() reformats /cache
* 8g. finish_recovery() erases BCB
* -- after this, rebooting will (try to) restart the main system --
* 9. main() calls reboot() to boot main system
从上面的几段注解中,基本上就明白的 Recovery 是如何工作的啦。下面就从具体代码开始一步步分析。
1、recovery main 函数
- int
- main(int argc, char **argv) {
- time_t start = time(NULL);
- // If these fail, there's not really anywhere to complain...
- freopen(TEMPORARY_LOG_FILE, "a", stdout); setbuf(stdout, NULL);
- freopen(TEMPORARY_LOG_FILE, "a", stderr); setbuf(stderr, NULL);
- printf("Starting recovery on %s", ctime(&start));
将标准输出和标准错误输出重定位到 "/tmp/recovery.log", 如果是 eng 模式,就可以通过 adb pull /tmp/recovery.log, 看到当前的 log 信息,这为我们提供了有效的调试手段。
ui_init();
一个简单的基于framebuffer的ui系统,叫miniui 主要建立了图像部分(gglInit、gr_init_font、framebuffer)及进度条和事件处理(input_callback)
load_volume_table();
根据 /etc/recovery.fstab 建立分区表
// command line args come from, in decreasing precedence:
// - the actual command line
// - the bootloader control block (one per line, after "recovery")
// - the contents of COMMAND_FILE (one per line)
get_args(&argc, &argv);
从misc 分区以及 CACHE:recovery/command 文件中读入参数,写入到argc, argv (get_bootloader_message) 并有可能写回 misc 分区(set_bootloader_message)
做完以上事情后就开始解析具体参数:
while ((arg = getopt_long(argc, argv, "", OPTIONS, NULL)) != -1) {
switch (arg) {
case 'p': previous_runs = atoi(optarg); break;
case 's': send_intent = optarg; break;
case 'u': update_package = optarg; break;
case 'w': wipe_data = wipe_cache = 1; break;
case 'c': wipe_cache = 1; break;
case 't': ui_show_text(1); break;
case '?':
LOGE("Invalid command argument\n");
continue;
}
}
printf("Command:");
for (arg = 0; arg < argc; arg++) {
printf(" \"%s\"", argv[arg]);
}
printf("\n");
以上仅仅是打印表明进入到哪一步,方便调试情况的掌握
下面的代码就是具体干的事情了:
if (update_package != NULL) {
status = install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE);
if (status == INSTALL_SUCCESS && wipe_cache) {
if (erase_volume("/cache")) {
LOGE("Cache wipe (requested by package) failed.");
}
}
if (status != INSTALL_SUCCESS) ui_print("Installation aborted.\n");
} else if (wipe_data) {
if (device_wipe_data()) status = INSTALL_ERROR;
if (erase_volume("/data")) status = INSTALL_ERROR;
if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
if (status != INSTALL_SUCCESS) ui_print("Data wipe failed.\n");
clear_sdcard_update_bootloader_message();
} else if (wipe_cache) {
if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
if (status != INSTALL_SUCCESS) ui_print("Cache wipe failed.\n");
clear_sdcard_update_bootloader_message();
} else {
status = update_by_key(); // No command specified
}
根据用户提供参数,调用各项功能,比如,安装一个升级包,擦除cache分区, 擦除user data分区等等,后在会将继续详细分解。
if (status != INSTALL_SUCCESS) prompt_and_wait();
如果前面做的操作成功则进入重启流程,否则由用户操作,可选操作为: reboot, 安装update.zip,除cache分区, 擦除user data分区
// Otherwise, get ready to boot the main system...
finish_recovery(send_intent);
先看函数注解:
// clear the recovery command and prepare to boot a (hopefully working) system,
// copy our log file to cache as well (for the system to read), and
// record any intent we were asked to communicate back to the system.
// this function is idempotent: call it as many times as you like.
其实主要的就是如下函数操作:
// Remove the command file, so recovery won't repeat indefinitely.
if (ensure_path_mounted(COMMAND_FILE) != 0 ||
(unlink(COMMAND_FILE) && errno != ENOENT)) {
LOGW("Can't unlink %s\n", COMMAND_FILE);
}
将指定分区mounted 成功并 unlink 删除一个文件的目录项并减少它的链接数
ensure_path_unmounted(CACHE_ROOT);
将指定分区 unmounted
sync(); // For good measure.
对于上面的代码总结:
它的功能如下:
1、将前面定义的intent字符串写入(如果有的话):CACHE:recovery/command
2、将 /tmp/recovery.log 复制到 "CACHE:recovery/log";
3、清空 misc 分区,这样重启就不会进入recovery模式
4、删除command 文件:CACHE:recovery/command;
最后重启机器
ui_print("Rebooting...\n");
android_reboot(ANDROID_RB_RESTART, 0, 0);
2、factory reset 核心代码实现
按照前面所列的8条步骤,其中1-6及7-8都与 main 通用流程一样,不再复述。
* 5. erase_volume() reformats /data
* 6. erase_volume() reformats /cache
这两个操作是如何做到的呢?
if (erase_volume("/data")) status = INSTALL_ERROR;
if (erase_volume("/cache")) status = INSTALL_ERROR;
最后就是
clear_sdcard_update_bootloader_message();
看看 erase_volume() 函数:
- static int
- erase_volume(const char *volume) {
- ui_set_background(BACKGROUND_ICON_INSTALLING);
- ui_show_indeterminate_progress();
- ui_print("Formatting %s...\n", volume);
- ensure_path_unmounted(volume);
- if (strcmp(volume, "/cache") == 0) {
- // Any part of the log we'd copied to cache is now gone.
- // Reset the pointer so we copy from the beginning of the temp
- // log.
- tmplog_offset = 0;
- }
- return format_volume(volume);
- }
上面红字标明的是重要函数调用
int ensure_path_unmounted(const char* path) {
Volume* v = volume_for_path(path);
result = scan_mounted_volumes();
return unmount_mounted_volume(mv);
}
就是将指定的path中径mount point进行卸载掉,而 format_volume的主要功能就是:
MtdWriteContext *write = mtd_write_partition(partition);
mtd_erase_blocks(write, -1);
mtd_write_close(write);
不要细说了吧,就是将整个分区数据全清掉。
最后一个函数:
void
clear_sdcard_update_bootloader_message() {
struct bootloader_message boot;
memset(&boot, 0, sizeof(boot));
set_bootloader_message(&boot);
}
就是将misc分区数据重置清0
这样子就完成的恢复出厂设置的情况了。将 data/cache分区erase擦掉就好了。
3、OTA 安装 核心代码实现
主要函数就是如何安装 Package :
* 5. install_package() attempts to install the update
* NOTE: the package install must itself be restartable from any point
int
install_package(const char* path, int* wipe_cache, const char* install_file)
-->
static int
really_install_package(const char *path, int* wipe_cache){
clear_sdcard_update_bootloader_message();
ui_set_background(BACKGROUND_ICON_INSTALLING);
ui_print("Finding update package...\n");
ui_show_indeterminate_progress();
LOGI("Update location: %s\n", path);
更新 ui 显示
for(;((i < 5)&&(ensure_path_mounted(path) != 0));i++){
LOGE("Can't mount %s\n",path);
sleep(1);
}
if((i >= 5)&&(ensure_path_mounted(path) != 0)){
return INSTALL_CORRUPT;
}
确保升级包所在分区已经mount,通常为 cache 分区或者 SD 分区
RSAPublicKey* loadedKeys = load_keys(PUBLIC_KEYS_FILE, &numKeys);
// Look for an RSA signature embedded in the .ZIP file comment given
// the path to the zip. Verify it matches one of the given public
// keys.
//
// Return VERIFY_SUCCESS, VERIFY_FAILURE (if any error is encountered
// or no key matches the signature).
err = verify_file(path, loadedKeys, numKeys);
从/res/keys中装载公钥,并进行确认文件的合法性
/* Try to open the package.
*/
ZipArchive zip;
err = mzOpenZipArchive(path, &zip);
打开升级包,将相关信息存到ZipArchive数据机构中,便于后面处理。
/* Verify and install the contents of the package. */ ui_print("Installing update...\n"); return try_update_binary(path, &zip, wipe_cache);
进行最后的安装包文件
}
// If the package contains an update binary, extract it and run it.
static int
try_update_binary(const char *path, ZipArchive *zip, int* wipe_cache) {
const ZipEntry* binary_entry =
mzFindZipEntry(zip, ASSUMED_UPDATE_BINARY_NAME);
char* binary = "/tmp/update_binary";
unlink(binary);
int fd = creat(binary, 0755);
bool ok = mzExtractZipEntryToFile(zip, binary_entry, fd);
close(fd);
mzCloseZipArchive(zip);
将升级包内文件META-INF/com/google/android/update-binary 复制为/tmp/update_binary
// When executing the update binary contained in the package, the
// arguments passed are:
//
// - the version number for this interface
//
// - an fd to which the program can write in order to update the
// progress bar. The program can write single-line commands:
int pipefd[2];
pipe(pipefd);
char** args = malloc(sizeof(char*) * 5);
args[0] = binary;
args[1] = EXPAND(RECOVERY_API_VERSION); // defined in Android.mk
args[2] = malloc(10);
sprintf(args[2], "%d", pipefd[1]);
args[3] = (char*)path;
args[4] = buf_uuid;
args[5] = NULL;
组装新的进程参数
pid_t pid = fork();
if (pid == 0) { // child process
close(pipefd[0]);
execv(binary, args);
}
// parent process
close(pipefd[1]);
ui_show_progress
ui_set_progress
ui_print
总结一下代码主要行为功能:
1、将会创建新的进程,执行:/tmp/update_binary
2、同时,会给该进程传入一些参数,其中最重要的就是一个管道fd,供新进程与原进程通信。
3、新进程诞生后,原进程就变成了一个服务进程,它提供若干UI更新服务:
a) progress
b) set_progress
c) ui_print等。
这样,新进程就可以通过老进程的UI系统完成显示任务。而其他功能就靠它自己了。