目录
Android支持多种启动模式,主要有正常模式(normal mode)、安全模式(safe mode)、恢复模式(recorvery mode)、工厂模式(factory mode)、快速启动模式(fastboot mode)等。本文讲述正常模式下Android的启动过程。本文代码基于Android kernel 4.4。
1、正常模式的启动流程
(1)、Boot ROM阶段:系统加电,固化在处理器ROM中的启动引导代码(BIOS)开始执行,进行一系列加电自检动作,之后会去寻找BootLoader的代码并将其加载进内存。(由芯片厂商设计实现,代码是固定在某个位置的)
(2)、BootLoader阶段:BootLoader代码又称为“引导装入程序”。其中MBR()中主引导程序扫描分区表,寻找活动分区,将活动分区引导记录中的次引导程序加载进内存并执行。次引导记录负责加载Linux内核映像,之后将控制权交给内核。
(3)、kernel阶段:内核加载进内存后,将首先进入内核引导阶段。进行初始化各种硬件环境、加载驱动程序。最后通过调用start_kernel进入内核启动阶段。在内核启动阶段,进行一系列应用级初始化后,最终启动用户空间的init程序。
(4)、init进程:init程序首先创建基本的文件系统目录并挂载相关的文件系统。然后通过对init.rc配置文件解析将相关进程启动起来,其中最重要的两个守护进程就是zygote进程和servicemanager进程。之后在一个无限循环中监听和处理事件,包括重启异常退出的service、循环处理来自属性服务(property service)、signal和keyboard的事件。
(5)、zygote进程:zygote虚拟机启动子进程system_server,在其中开启Android核心系统服务并将核心系统服务添加到ServiceManager中,然后系统进入systemReady状态。
(6)、ActivityManagerService进程:在systemReady状态下,ActivityManagerService与zygote中的socket进行通信,通过zygote启动home应用进入系统桌面。
2、内核启动过程
2.1 内核引导阶段
head.S是Linux内核启动的汇编程序入口,但head.S中并没有直接调用start_kernel()。可以先找到调用start_kernel()的函数,然后通过回溯法找到其调用的源头。在head-common.S中可以找到start_kerne()的调用位置,代码如下:
//函数_mmap_switched
__mmap_switched:
......
//这里真正调用了start kernel
b start kernel //第104行
//b指令是跳转指令。遇到b指令,处理器跳转到给定@的目标地址,从目标地址继续执行
从head-common.S代码中可以看到,系统在__mmap_switched中通过b汇编指令调用了 start_kernel。那么__mmap_switched又是在哪里被调用的呢?回到head.s这个汇编入口,代码如下:
//MMU可用后,将跳转到这个地址
ldr r13, =__map_switched //第135行
adr 1r, BSYM(1f) @ 返回地址(PIC)
mov r8, r4
ARM( add pc r10, #PROCINFO INITFUNC)
THUMB( add r12, r10, #PROCINFO INITFUNC)
THUMB( mov pc, r12)
1:b __enable_mmu @启动MMU
ENDPROC(stext)
这里将__mmap_switched的地址保存到r13。那pc指针又是什么时候指向r13的呢!在源码中搜索,发现是在__turn_mmu_on结束之时将pc指针指向r13的。代码如下:
.align 5
__turn_mmu_on:
...//省略部分代码
mov r3, r13 //跳转到r13,即__mmap_switched //第478行
mov pc, r3 //pc指针,指向当前要运行的指令
__enable_mmu_end:
ENDPROC(_turn_mmu_on)
到这里,可以继续跟踪__turn_mmu_on是在哪里调用的。这很容易找到,系统通过b汇编指令跳转到__turn_mmu_on。代码如下:
__enable_mmu:
//省略部分代码
mov r5,#(domain val(DOMAIN USER, DOMAIN MANAGER)\
domain val(DOMAIN KERNEL, DOMAIN MANAGER)I\
domain val(DOMAIN TABLE, DOMAIN MANAGER)IY
domain val(DOMAIN IO,DOMAIN CLIENT))
// load domain access register
mcr p5,0, r5, c3,c0,0
// load page table pointer
mcr p15, 0, r4, c2, c0, 0
//系统在这里跳转到_turn_mmu_on
b __turn_mmu_on //第441行
ENDPROC(__enable_mmu)
到这里,可以看出start_kernel的调用流程:系统在开启MMU的时候 【 MMU(Memory Management Unit )是内存管理单元的简称】,在函数__enable_mmu中调用__turn_mmu_on ,然后__turn_mmu_on通过“mov r3, r13”和“mov pc, r3”两条指令调用__mmap_switched 。而函数__mmap_switched是head-common.S中的函数,在该函数中调用start _kernel。
start_kernel函数是Linux内核通用的启动函数,也是汇编代码执行完毕后的第一个C语言函数,它的实现代码位于init/main.c中。start_kernel的代码比较复杂,其中做了很多软硬件初始化的工作,比如调用了setup_arch()、timer_init()、init_IRQ、console_init()。
2.2 内核启动阶段
内核初始化阶段结束时通过调用start_kernel函数进入内核启动阶段。在内核启动阶段,首先进行一些应用级的初始化工作,最后调用rest_init函数启动init进程。init进程是 Android用户空间的1号进程,担负Android运行环境启动的重要职责。本节首先分析内核启动阶段都做了哪些工作。这部分代码主要位于kerel/init/main.c,如下所示:
asmlinkage void __init start_kernel(void)
{
......//省略部分内容
local irq_disable(); //关闭当前CPU中断
early_boot_irgs_disabled=true;
page_address_init(); //页地址初始化
printk(KERN_NOTICE "s",linux_banner); //输出内核版本信息
setup_arch(&command line); //体系结构相关的初始化
//command_1ine由bootloader传入
......//省略部分内容
sched_init(); //初始化进调度器
preempt_disable(); //禁止内核抢占
early_irg_init( //初始化中断处理函数
init_IRQ();
init_timers(); //初始化定时器
hrtimers_init(); //高精度时钟的初始化
softirq_init(): //初始化软中断
time_init(); //初始化系统时间
profile_init(); //初始化内核性能调试工具profile
early_boot_irgs_disabled=false;
local_irg_enable(): //打开IRO中断
console_init(): //初始化控制台显示printk打印的内容
//在此之前调用的printk只是缓存信息
fork_init(totalram pages); //计算允许创建进程的数量
......//省略部分内容
signals_init(); //初始化信号量
ftrace_init();
rest_init(); //创建init进程
}
start_kernel的源代码可以分成以下两部分:
(1)C部分的初始化过程。
(2)调用rest_init创建init进程。
如果对C部分的初始化感兴趣,可以参考专门介绍Linux内核的书。本文将重点放在init进程创建过程。在start_kernel函数的最后调用了rest_init函数进行后续的初始化,定位到rest_init方法的实现部分kerel/init/main.c,代码如下:
static noinline void __init_refok rest_init(void)
{
int pid;
rcu_scheduler_starting();
/*通过kernel_thread启动内核线程,执行kernel init*函数,
此函数首先创建了init进程,以使其成为1号进程*/
kernel_thread(kernel_init, NULL,CLONE_FS | CLONE_SIGHAND);
numa_default_policy();
/*启动内核线程kthreadd,始终运行在内核空间, 负责所有内核线程的调度和管理*/
pid = kernel_thread(kthreadd, NULL,CLONE_FS | CLONE_FILES);
....//省略部分代码
/*调用schedule,释放当前线程占用的CPU,以激活其他线程*/
schedule();
preempt_disable();
/*调用cpu_idle,使当前线程变成idle线程(空闲进程),减少系统资源占用*/
cpu_idle();
}
rest_init中的主要任务是通过kernel_thread启动了两个内核线程。kernel_thread的实现代码位于kernel/common/arch/arm/kernel/process.c,函数原型如下:
pid_t kernel_thread(int (*fn)(void *),void *arg,unsigned 1ong flags)
kernel_thread用来启动一个内核线程,第一个参数fn是要执行的函数的指针;第二个参数arg是传递给该函数的参数;第三个参数flags为do_fork创建线程时的标志。对于rest_init方法中第一个kernel_thread调用,实际上调用了kernel_init函数。进入kernal_init 函数体一看究竟(kerel/init/main.c),代码如下:
static int __init kernel_init(void * unused)
{
//等待kthreadd进程完成所有设置
wait_for_completion(&kthreadd_done);
/*Now the scheduler is fully set up and can do blocking allocations */
gfp_allowed_mask = __GFP_BITS_MASK;
//init进程可以给任何节点分配页
set_mems_allowed(node_states[N_HIGH_MEMORY]);
//init进程可以运行在任何一个CPU上
set_cpus_allowed_ptr(current, cpu_all_mask);
/* 多CPU激活开始 */
cad_pid = task_pid(current);
smp_prepare_cpus(setup_max_cpus);
do_pre_smp_initcalls();
lockup_detector_init();
//激活SMP系统中其他CPU
smp_init();
sched_init_smp();
/*此时与体系结构相关的部分已经完成了初始化,接下来do_basic_setup函数初始化设备,完成外设及其驱动程序的加载和初始化*/
do_basic_setup();
/* Open the /dev/console on the rootfs, this should never fail */
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);
//定义init进程
if (!ramdisk_execute_command)
ramdisk_execute_command = "/init";
if (sys_access((const char __user *) ramdisk_execute_command, 0) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
}
/*最后调用init_post,启动进程负责用户空间的初始化*/
init_post();
return 0;
}
kernel_init方法中,前面部分做了一些初始化工作,然后定义了init进程的位置,接着调用了init_post函数。接下来定位到init_post函数体(kerel/init/main.c),代码如下:
static noinline int init_post(void)
{
/* 释放内存之前完成所有异步 __init 代码 */
async_synchronize_full();
free_initmem();
mark_rodata_ro();
system_state = SYSTEM_RUNNING;
numa_default_policy();
current->signal->flags |= SIGNAL_UNKILLABLE;
if (ramdisk_execute_command) {
//run_init_process执行后将不再返回
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. "
"See Linux Documentation/init.txt for guidance.");
}
execute_command是bootloader传递给内核的参数,一般是/init(即根目录下的init程序),也即是调用文件系统里的init进程。如果找不到就会继续寻找“/sbin/init”、“/etc/initt”、“/bin/init”、“bin/sh”,找到后便执行run_init_process,且不再返回。run_init_process的函数体非常简单。仅仅是对kernel_execve函数的封装,代码如下:
static void run_init_process(const char *init_filename)
{
argv_init[0] = init_filename;
kernel_execve(init_filename, argv_init, envp_init);
}
kernel_execute是Linux内核中创建用户进程的方法接口,实现位于arch/arm/kernel/sys_arm.c。至此,已经对Android kernel如何引导以及用户空间1号进程(init进程)如何启动做了详细分析。下一节详细分析Android init进程的执行过程。