您的位置:首页 > 运维架构 > Linux

linux内核启动+Android系统启动过程详解

2014-08-20 14:37 253 查看
第一部分:汇编部分

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
= 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进行清零。

何为umask,请看http://www.szstudy.cn/showArticle/53978.shtml

(3)为rootfs建立必要的文件夹,并挂载适当的分区。

/dev (tmpfs)

/dev/pts(devpts)

/dev/socket

/proc (proc)

/sys (sysfs)

(4)创建/dev/null和/dev/kmsg节点。

(5)解析/init.rc,将所有服务和操作信息加入链表。

(6)从/proc/cmdline中提取信息内核启动参数,并保存到全局变量。

(7)先从上一步获得的全局变量中获取信息硬件信息和版本号,如果没有则从/proc/cpuinfo中提取,并保存到全局变量。

(8)根据硬件信息选择一个/init.(硬件).rc,并解析,将服务和操作信息加入链表。

在G1的ramdisk根目录下有两个/init.(硬件).rc:init.goldfish.rc和init.trout.rc,init程序会根据上一步获得的硬件信息选择一个解析。

(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 中的參數,并设置属性系统中的参数:

1、 如果bootmode為

-factory,設置ro.factorytest值為1

-factory2,設置ro.factorytest值為2

-其他的設ro.factorytest值為0

2、如果有serialno参数,則設置ro.serialno,否則為""

3、如果有bootmod参数,則設置ro.bootmod,否則為"unknown"

4、如果有baseband参数,則設置ro.baseband,否則為"unknown"

5、如果有carrier参数,則設置ro.carrier,否則為"unknown"

6、如果有bootloader参数,則設置ro.bootloader,否則為"unknown"

7、通过全局变量(前面从/proc/cpuinfo中提取的)設置ro.hardware和ro.version。

(16)執行所有触发标识为init的action。

(17)開始property服務,讀取一些property文件,這一動作必須在前面那些ro.foo設置后做,以便/data/local.prop不能干預到他們。

- /system/build.prop

- /system/default.prop

- /data/local.prop

- 在讀取默認的property后讀取presistentpropertie,在/data/property中

(18)為sigchld handler創建信號機制。

(19)確認所有初始化工作完成:

device_fd(device init 完成)

property_set_fd(property server start 完成)

signal_recv_fd (信號機制建立)

(20) 執行所有触发标识为early-boot的action

(21) 執行所有触发标识为boot的action

(22)基于當前property狀態,執行所有触发标识为property的action

(23)注冊輪詢事件:

-device_fd

-property_set_fd

-signal_recv_fd

-如果有keychord,則注冊keychord_fd

(24)如果支持BOOTCHART,則初始化BOOTCHART

(25)進入主進程死循環:

- 重置輪詢事件的接受狀態,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
====================================================================================

第五部分:C部分+Java部分

Android启动之开启系统服务

经过init线程启动之后,可分成两个方向:1、正常的system方向;2、recovery系统方向。

以下具体分析:

1、正常的system方向

第1步: 重要的后台程序zygote

1) 源码:frameworks/base/cmds/app_main.cpp等

2) 说明:zygote是一个在init.rc中被指定启动的服务,该服务对应的命令是/system/bin/app_process

a) 建立JavaRuntime,建立虚拟机

b) 建立Socket接收ActivityManangerService的请求,用于Fork应用程序

c) 启动SystemServer

第2步: 系统服务systemserver

1) 源码:frameworks/base/services/java/com/android/server/SystemServer.java

2) 说明:被zygote启动,通过SystemManager管理android的服务(这里的服务指frameworks/base/services下的服务,如卫星定位服务,剪切板服务等)

第3步:桌面launcher

1) 源码:ActivityManagerService.java为入口,packages/apps/launcher*实现

2) 说明:系统启动成功后SystemServer使用xxx.systemReady()通知各个服务,系统已经就绪,桌面程序Home就是在ActivityManagerService.systemReady()通知的过程中建立的,最终调用 ()启launcher

第4步: 解锁

1) 源码:

frameworks/policies/base/phone/com/android/internal/policy/impl/*lock*

2) 说明:系统启动成功后SystemServer调用wm.systemReady()通知WindowManagerService,进而调用PhoneWindowManager,最终通过LockPatternKeyguardView显示解锁界面,跟踪代码可以看到解锁界面并不是一个Activity,这是只是向特定层上绘图,其代码了存放在特殊的位置

第5步: 开机自启动的第三方应用程序

1) 源码:

frameworks/base/services/java/com/android/server/am/ActivityManagerService.java

2) 说明:系统启动成功后SystemServer调用ActivityManagerNative.getDefault().systemReady()通知ActivityManager启动成功,ActivityManager会通过置变量mBooting,通知它的另一线程,该线程会发送广播android.intent.action.BOOT_COMPLETED以告知已注册的第三方程序在开机时自动启动。

第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 函数

[cpp]
view plaincopy

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() 函数:

[cpp]
view plaincopy

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系统完成显示任务。而其他功能就靠它自己了。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: