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

时间:2022-08-19 04:32:39

第一部分:汇编部分

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指针

r1r2中分别存放着由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-ICEANGLE需要在板子上有驻留程序,然后通过串口就可以调试了。用过的AXDtrace调试环境的话,对此应该比较熟悉。 

not_angel:  //若不是通过angel调试进入内核

              mrs  r2, cpsr          @ turn off interrupts to

              orr   r2, r2, #0xc0         @ prevent angel from running

              msr  cpsr_c, r2   //这里将cpsrIF位分别置“1,关闭IRQFIQ

#else

              teqp pc, #0x0c000003          @ turn off interrupts

常用 TEQP PC,#(新模式编号)来改变模式

#endif

  另外链接器会把一些处理器相关的代码链接到这个位置,也就是arch/arm/boot/compressed/head-xxx.S文件中的代码。在高通平台下,这个文件是head-msm.S连接脚是compress/vmlinux.lds,其中部分内容大致如下,在连接时,连接器根据每个文件中的段名将相同的段合在一起,比如将head.Shead-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 r0LC0的值实际上按下面公式计算:

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 bufferI/DcacheTLB.接着打开i-cache,设置为Round-robin replacement。调用__common_mmu_cache_on,打开mmud-cache.把页表基地址和域访问控制写入协处理器寄存器c2c3. __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空间里打开cacheablebufferable

              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空间,并把对应的描述符的CB位均置”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,所以这里先对页表进行初始化,然后打开mmucache

上面这段就是对一级描述符表(页表)的初始化,首先比较这个描述符所描述的地址是否在那个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//lc0SP进行了定义  .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

 

缓冲区空间的起始地址和结束地址分别存放在r1r2中。然后判断最终内核地址,也就是解压后内核的起始地址,是否大于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处理中断的,打开MMUcach操作是不同的,因此,在内核开始执行前需要定位CPU架构,比如高通利用的ARM11Ti用的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的映射空间,这里是按照11一致映射,即代码的基地址(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();//用于高地址内存,我们都用32CPU,此函数为空

       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_DMAZONE_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_descmap_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);//初始化kernelfork()环境。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_initrdINITRD进行处理。

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/ramINITRD

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

  何为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
  1. int  
  2. main(int argc, char **argv) {  
  3.     time_t start = time(NULL);  
  4.   
  5.     // If these fail, there's not really anywhere to complain...  
  6.     freopen(TEMPORARY_LOG_FILE, "a", stdout); setbuf(stdout, NULL);  
  7.     freopen(TEMPORARY_LOG_FILE, "a", stderr); setbuf(stderr, NULL);  
  8.     printf("Starting recovery on %s", ctime(&start));  

将标准输出和标准错误输出重定位到 "/tmp/recovery.log", 如果是 eng 模式,就可以通过 adb pull /tmp/recovery.log,  看到当前的 log 信息,这为我们提供了有效的调试手段。

ui_init();

一个简单的基于framebufferui系统,叫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
  1. static int  
  2. erase_volume(const char *volume) {  
  3.     ui_set_background(BACKGROUND_ICON_INSTALLING);  
  4.     ui_show_indeterminate_progress();  
  5.     ui_print("Formatting %s...\n", volume);  
  6.   
  7.     ensure_path_unmounted(volume);  
  8.   
  9.     if (strcmp(volume, "/cache") == 0) {  
  10.         // Any part of the log we'd copied to cache is now gone.  
  11.         // Reset the pointer so we copy from the beginning of the temp  
  12.         // log.  
  13.         tmplog_offset = 0;  
  14.     }  
  15.   
  16.     return format_volume(volume);  
  17. }  

上面红字标明的是重要函数调用

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系统完成显示任务。而其他功能就靠它自己了。