1 前言
上节我们学习了驱动开发基础,这节我们继续学习,这节我们主要来了解安卓系统是怎么启动的,以及内核的初始化工作。
2 简介
Android采用分层的架构设计,从高到低分别是应用层,Java API 框架层,系统运行层(包括Android Runtime和原生态的C/C++库)、硬件抽象层、Linux内核层。而我们这篇文章将从上电讲到Kernel的启动,大致流程如下。
3 启动流程
3.1 上电
首先,得要给板子上电才会有后面的事情,一般的个人电脑上电后会加载BIOS进行初始化然后跳入到系统,具体过程就是将非易失性存储器中的系统内核拷贝到RAM执行init代码从而启动内核。安卓作为ARM架构的一个操作系统和个人电脑有所不同,但总体原理一致。
在安卓中,上电后在PC(程序计数器)中取得的第一个地址指向的是Boot Rom(比如Flash)中的第一段程序Bootloader(Boot Loader有很多种,这里不展开讲),然后由Bootloader将内核镜像拷贝至运存,然后启动内核的第一行代码。
3.2 Boot Loader
Boot Loader是嵌入式系统的引导加载程序,它是系统上电后运行的第一段程序,其作用类似于 PC 机上的 BIOS。 不同的处理器上电或复位后执行的第一条指令地址并不相同,对于 ARM 处理器来说,该地址为 0x00000000。对于一般的嵌入式系统,通常把 Flash 等非易失性存储器映射到这个地址处,而 Boot loader就位于该存储器的最前端,所以系统上电或复位后执行的第一段程序便是Bootloader.
Boot Loader启动后接下来要做的事主要分下面几个步骤:
- 初始化 RAM
因为 Linux 内核一般都会在 RAM 中运行,所以在调用 Linux 内核之前 boot loader 必须设置和初始化 RAM,为调用 Linux内核做好准备。初始化 RAM 的任务包括设置CPU 的控制寄存器参数,以便能正常使用 RAM以及检测RAM大小等。 - 初始化串口
串口在 Linux 的启动过程中有着非常重要的作用,它是 Linux内核和用户交互的方式之一。Linux 在启动过程中可以将信息通过串口输出,这样便可清楚的了解 Linux 的启动过程。虽然它并不是 Bootloader 必须要完成的工作,但是通过串口输出信息是调试Bootloader 和Linux内核的强有力的工具,所以一般的 Bootloader 都会在执行过程中初始化一个串口做为调试端口。 - 检测处理器类型
Bootloader在调用 Linux内核前必须检测系统的处理器类型,并将其保存到某个常量中提供给 Linux 内核。Linux 内核在启动过程中会根据该处理器类型调用相应的初始化程序 - 设置linux启动参数
- 调用linux内核映像
Bootloader完成的最后一项工作便是调用 Linux内核。如果 Linux 内核存放在 Flash 中,并且可直接在上面运行(这里的 Flash 指 Nor Flash),那么可直接跳转到内核中去执行。但由于在 Flash 中执行代码会有种种限制,而且速度也远不及 RAM 快,所以一般的嵌入式系统都是将 Linux内核拷贝到 RAM 中,然后跳转到 RAM 中去执行。
以上步骤是bootloader的一般动作(上述BootLoader功能的内容转自此链接),实际上由于芯片厂商的不同,Bootloader的具体工作也是不同的,有些会在启动Kernel前执行安全固件的程序进行安全检查,然后启动little kernel,最后再由little kernel启动Kernel.
3.3 Kernel的初始化
内核的启动主要分为以下几个步骤:
- 检查硬件和CPU类型
- 初始化堆栈、MMU(Memory Management Unit的缩写,中文名是内存管理单元,它是一种负责处理中央处理器(CPU)的内存访问请求的计算机硬件)等程序.
- 初始化各种模块
- 挂接根文件系统
- 执行init程序,启动关键服务Zygote
注:在Android系统中,zygote所有应用进程的父进程,app的进程都是由这个zygote分裂出来的。zygote则是由Linux系统用户空间的第一个进程init进程,通过fork的方式创建的。
以上过程的代码可以在kernel的init文件夹中main.c文件中进行了解,以下贴出不完全的start_kernel函数。注kernel版本:4.9
asmlinkage __visible void __init start_kernel(void)
{
char *command_line; // 命令行,用来存放 bootloader 传递过来的参数
char *after_dashes;
/* 函数参数:init_task即手工创建的PCB,0号进程就是最终的idle进程,它是个结构体:struct task_struct init_task = INIT_TASK(init_task);
INIT_TASK 是一个宏用来设置拼接第一个PCB,Base=0, limit=0x1fffff (=2MB)
函数功能:获取栈边界地址,然后把 STACK_END_MAGIC这个宏设置为栈溢出的标志。
*/
set_task_stack_end_magic(&init_task);
/*针对SMP处理器,用于获取当前CPU的硬件ID ,如果不是多核,函数为空*/
smp_setup_processor_id();
/*初始化哈希桶 (hash buckets) 并将 static object 和 pool object 放 入 poll 列表,这样堆栈就可以完全操作了 【这个函数的主要作用就是对调试对象进行早期的初始化,就是HASH锁和静态对象池进行初始化,执行完后,object tracker已经开始完全运作了*/
debug_objects_early_init();
/*
* 初始化堆栈保护的加纳利值,防止栈溢出攻击的堆栈保护关键字
*/
boot_init_stack_canary();
/*在系统启动时初始化 cgroups ,同时初始化需要 early_init 的子系统
【这个函数作用是控制组 (control groups) 早期的初始化,
控制组就是 定义一组进程具有相同资源的占有程度,
比如,可以指定一组进程使用 CPU 为 30% ,磁盘 IO 为 40% ,网络带宽为 50%,
目的就是为了把所有进程分配不同的资源*/
cgroup_init_early();
/*关闭当前CPU的所有中断响应,操作CPSR寄存器。对应后面的系统中断关闭标志,当early_init完毕后,会恢复中断设置标志为false 。*/
local_irq_disable();
early_boot_irqs_disabled = true;
/*
* Interrupts are still disabled. Do necessary setups, then
* enable them
*/
boot_cpu_init();
page_address_init();
pr_notice("%s", linux_banner);
/*内核架构相关初始化函数,是非常重要的一个初始化步骤。
其中包含了处理器相关参数的初始化、内核启动参数 (tagged list)的获取
和前期处理、内存子系统的早期初始化(bootmem 分配器)*/
setup_arch(&command_line);
...
page_alloc_init();
...
/*初始化中断向量*/
trap_init();
/*内存管理模块初始化*/
mm_init();
/*调度模块初始化*/
sched_init();
...
/*剩下的初始化工作*/
rest_init();
}
对于start_kernel最后一行,对于我们后续研究比较重要
现在我们跟随源码在main.c文件中依次进入以下函数:
rest_init->
kernel_thread(kernel_init, NULL, CLONE_FS)->
kernel_init->
kernel_init_freeable()->
do_basic_setup():
/* 到这一步,系统的内核初始化就已经差不多完成了,CPU 的子系统已经启动并运行,
且内存和处理器管理系统已经在工作了。但还有些设置未完成,这些
设置对于驱动工程师比较重要,特别是driver_init和do_initcalls函数
*/
static void __init do_basic_setup(void)
{
// 针对SMP系统,初始化内核control group的cpuset子系统。如果非SMP,此函数为空。
cpuset_init_smp();
shmem_init();
// 初始化驱动模型中的各子系统,可见的现象是在/sys中出现的目录和文件
driver_init();
// 在proc文件系统中创建irq目录,并在其中初始化系统中所有中断对应的目录。
init_irq_proc();
// 调用链接到内核中的所有构造函数,也就是链接进.ctors段中的所有函数。
do_ctors();
// 启用用户态的帮助器
usermodehelper_enable();
// 调用所有编译内核的驱动模块中的初始化函数。
do_initcalls();
}
至此我们本节的内容基本结束,内核也完成了初始化,至于更多的内容需要对源码进行更深的研究,后期有机会会出几篇特定的源码解析。
4 总结
本节,主要研究了安卓系统内核从上电到启动之后初始化的全过程,并对一些函数进行了解析,但还不够完善,一些比较重要的函数源码还没有进行研究,下节将对driver_init进行深入研究。
本系列链接传送:
【Android底层学习总结】1. 驱动开发基础
【Android底层学习总结】3. 内核中driver_init函数源码解析