文章大纲
- 引言
- 一、上电开机,启动BootLoader
- 二、加载Linux 内核并初始化
- 1、内核(kernel)自解压
- 2、内核初始化和建立页表
- 3、执行start_kernel 函数创建init进程
- 4、挂载文件系统
引言
Android系统的启动指Android设备断电关机后,再长按电源通电Android设备开机的过程,涉及到软硬件的上电和启动,而Android是基于Linux内核的操作系统,所以会涉及到一些Linux内核的知识,接下来系列文章将逐一介绍和总结这一流程,系列文章链接如下:
- Android 进阶——系统启动之BootLoader 简介及内核启动(一)
- Android 进阶——系统启动之Linux init进程的创建和启动(二)
- Android 进阶——系统启动之Android init进程的创建和启动(三)
- Android 进阶——系统启动之Android init.rc脚本解析(四)
- Android 进阶——系统启动之Android init进程解析init.rc脚本(五)
源码基于android 9.0 ,由于知识水平有限,可能有些理解和标识在学术上有所偏差和不够严谨,仅供参考。
一、上电开机,启动BootLoader
从硬件层面上说,开机就是给系统上电,此时硬件电路会产生一个确定的复位时序,确保CPU是最后一个被复位的器件(因为假如CPU被复位后并开始运行时,但其他硬件(I/O设备、内存等)内部的寄存器状态可能还没有准备好,就有可能导致硬件初始化错误)。当所有部件完成复位后,CPU开始执行第一条指令,该指令所在的内存地址是由CPU厂商指定且固定的(不同的CPU可能会从不同的地址获取指令,但该地址必须是固定的),该地址对应的程序就是名为Bootloader
。Bootloader
是嵌入式系统在上电后在操作系统内核运行之前执行的第一段代码用于
- 初始化硬件设备
- 建立内存空间映射图,从而将系统的软硬件环境带到一个合适状态,
- 加载操作系统内核
因此整个嵌入式系统内核的加载和启动就完全由BootLoader来完成。
在多数基于ARM的实际硬件系统,会从并口NAND Flash 芯片中的 0x00000000地址处装载程序,对于一些小型嵌入式系统而言,该地址中的程序就是最终要执行的用户程,而对于Android而言,该地址中的程序还不是Android程序,而是一个名为U-Boot(或fastboot)用于初始化硬件设备的程序(U-Boot的源码是通过GCC和Makefile组织编译的)
U-Boot(或fastboot)可以看成是Android 设备的Bootloader,当U-Boot(或fastboot)被装载后便开始运行,它一般会先检测用户是否按下某些特别按键,这些特别按键是uboot在编译时预先被约定好的,用于进入调试模式。如果用户没有按这些特别的按键,则uboot会从NAND Flash中装载Linux内核,装载的地址是在编译uboot时预先约定好的。
BootLoader 的启动一般是直接由汇编发起,再调用到对应的C代码。
二、加载Linux 内核并初始化
Linux系统内核启动主要分为三个阶段:
- 自解压过程
- 设置ARM处理器的工作模式、设置一级页表等初始化工作。
- 执行相关C代码完成Android的初始化的全部工作。
1、内核(kernel)自解压
当嵌入式设备从uboot跳转到kernel的时候,首先运行的是kernel的自解压程序,Linux内核在编译后是以压缩文件形式存在(路径是kernel/out/arch/arm/boot/zImage),根out/arch/arm/boot/compressed/vmlinux.lds
文件组织存放,如果要修改代码段和数据段位置也需要修改该链接脚本。内核压缩和解压缩代码都在目录kernel/arch/boot/compressed,编译完成后将产生head.o、misc.o、piggy.gzip.o、vmlinux、decompress.o这几个可重定位文件。
- head.o——内核的头部文件,负责初始设置
- misc.o——将主要负责内核的解压工作,它在head.o之后
- piggy.gzip.o——一个中间文件,本质是一个压缩的内核(kernel/vmlinux),只不过没有和初始化文件及解压缩文件链接而已
- vmlinux——没有(zImage是压缩过的内核)压缩过的内核,就是由piggy.gzip.o、head.o、misc.o组成的
- decompress.o是未支持更多的压缩格式而新引入的。
BootLoader完成系统的引导以后并将Linux内核加载之后,调用do_bootm_linux()函数跳转到kernel的真实位置(主要是直接通过汇编和C实现)。若kernel没被压缩,就可以启动了;但kernel被压缩过,就要先解压,然后再启动。
2、内核初始化和建立页表
当内核解压完成后通过汇编代码检查处理器类型和机器码类型调用相应的初始化函数,再建立页表,最后跳转到start_kernel()函数开始内核的初始化工作。
检查处理器是汇编子函数
__lookup_processor_type
中完成的(在文件head-commom.S实现)。__lookup_processor_type
调用结束返回原程序时,会将返回结果保存到寄存器中。其中r5寄存器返回一个描述处理器的结构体地址,并对r5进行判断,如果r5的值为0说明不支持这种处理器,将进入_error_p。r8保存了页表的标志位,r9保存了处理器的ID号,r10保存了与处理相关的struct proc_info_list检查机器码类型是汇编函数
__lookup_machine_type
中完成,该函数返回时会将返回结构保存在r5、r6和r7三个寄存器中,其中r5寄存器返回一个用来描述机器的机构体地址,并对r5进行判断,如果r5为0,则说明不支持这种机器,将进入__error_a。r6保存了I/O的页表偏移地址。当检测处理器类型和机器码类型结束后,将调用__create_page_tables子函数来建立页表,它所要做的工作就是将RAM地址开始的1M空间物理地址映射到0xC0000000开始的虚拟地址处。
3、执行start_kernel 函数创建init进程
Linux内核启动的是通过汇编调用start_kernel函数(位于init/main.c)开始的,start_kernel是所有Linux平台进入系统内核初始化后的入口函数,大致的功能逻辑包含:
- 调用setup_arch()函数进行与体系结构相关的第一个初始化工作;对于不同的体系结构来说该函数有不同的定义。对于ARM平台而言,该函数定义在arch/arm/kernel/setup.c。它首先通过检测出来的处理器类型进行处理其内核的初始化,然后通过bootmem_init()函数根据系统定义的meminfo结构进行内存结构的初始化,最后调用 paging_init()开启MMU,创建内核页表,映射所有的物理内存和IO空间。
- 创建异常向量表和初始化中断处理函数
- 初始化系统核心进程调度器和时钟中断处理机制
- 初始化串口控制台
- 创建初始化系统cache,为各种内存调用机制提供缓存,包括动态内存分配,虚拟文件系统(VirtuaFile System)及页缓存。
- 初始化内存管理,检测内存大小及被内核占用的内存情况。
- 初始化系统的进程间通信机制(IPC);当以上所有的初始化工作结束后,**start_kernel()函数会调用rest_init()函数来进行最后的初始化,包括内核解析执行init.rc脚本创建系统的第一个进程——init进程(进程id为1 )**来结束内核的启动。
简而言之,主要完成剩余与硬件平台的相关初始化工作。
4、挂载文件系统
在进行一些系列的与内核相关的初始后,调用init进程并等待用户进程的执行之前,Linux内核启动的最后一个阶段就是挂载根文件系统,以便安装适当的内核模块来驱动某些硬件设备或启动某些功能和
启动存储于文件系统中的init服务,以便让init服务接手后续的启动工作。至少要挂载以下目录:
- /etc/——存储重要的配置文件
- /bin/——存储常用且开机时必须用到的执行文件。
- /sbin/——存储着开机过程中所需的系统执行文件。
- /lib/——存储/bin/及/sbin/的执行文件所需要的链接库,以及Linux的内核模块
- /dev/——存储设备文件
Linxu内核启动的最后一个动作,就是从根文件系统上找出并执行init服务(不同的目录都有对应的init服务)。
- 第一步检查 /sbin/是否有init服务
- 第二步检查/etc/是否有init服务
- 第三步检查/bin/是否有init服务
- 如果都找不到你最后执行/bin/sh
找到init服务后,Linux会让init服务负责后续初始化系统使用环境的工作,init启动后,就代表系统已经顺利地启动了Linux内核。启动init服务时,init服务会读取/etc/inittab文件,根据/etc/inittab中的设置数据进行初始化系统环境工作。/ect/inittabl定义init服务在Linux启动过程中必须执行以下几个脚本:
- /etc/rc.d/rc.sysinit 主要功能是设置系统的基本环境,当init服务执行rc.sysinit时,要依次完成下面一系列工作:
- 启动udev
- 设置内核参数:执行sysctl -p,以便从/etc/sysctl.conf设置内核参数
- 设置系统时间:将硬件时间设置为系统时间
- 启动交换内存空间:执行swpaon -a -e,以便根据、etc/fstab的设置启动所有的交互内存空间。
- 检查并挂载所有文件系统:检查所有需要挂载的文件系统,以确保这些文件系统的完整性。检查完毕后可读可写的方式挂载文件系统。(之所以采用只读的方式挂载根文件系统,是因为此时Linux内核仍在启动阶段,还不是很稳定,若采用可读可写的方式挂载跟文件系统,万一Linux不小心宕机了可能破坏根文件系统上的数据,导致Linux下次开机时得花上很长时间来检查并修复根文件系统)
- 初始化硬件设备:Linux除了在启动内核时以静态驱动部分的硬件外,在执行rc.sysinit时,也会试着驱动剩余的硬件设备。
- 初始化串行端口设备:Init服务会管理所有的串行端口设备,比如调制解调器、不断电系统、串行端口控制台等。Init服务则通过rc.sysinit来初始化Linux串行端口设备。当rc.sysinit发现Linux才能在这/etc/rc.serial时,才会执行/etc/rc.serial,借以初始化所有的串行端口设备。因此,你可以在/etc/rc.serial中定义如何初始化Linux所有的串行端口设备。
- 清除过程的锁定文件与IPC文件
- 建立用户接口
- 建立虚拟控制台
- /etc/rc.d/rc
- /etc/rc.d/rc.local
在建立虚拟控制台, init会在若干个虚拟控制台中执行/bin/login,以便用户可以从虚拟控制台登录Linux。Linux默认在前6个虚拟控制台,也就tty1~tty6,执行/bin/login登录程序。当所有的初始化工作结束后,cpu-idle()函数会被调用来使用系统处于闲置(idle)状态并等待用户程序的执行。至此,整个Linux内核启动完毕。
Android设备启动要依次经过BootLoader引导、Linux Kernel加载和初始化和初始化Android系统服务(都会有相应的启动动画对应),未完待续…