Linux内核的引导
引导Linux 系统的过程包括很多阶段。一般的SoC 内嵌入了bootrom,上电时bootrom 运行。对于CPU0 而言,bootrom 会去引导bootloader,而其他CPU 则判断自己是不是CPU0,进入WFI 的状态等待CPU0 来唤醒它。CPU0 引导bootloader,bootloader 引导Linux 内核,在内核启动阶段,CPU0 会发中断唤醒CPU1,之后CPU0 和CPU1 都投入运行。CPU0 导致用户空间的init 程序被调用,init 程序再派生其他进程,派生出来的进程再派生其他进程。CPU0 和CPU1 共担这些负载,进行负载均衡。
bootrom 是各个SoC 厂家根据自身情况编写的,目前的SoC 一般都具有从SD、eMMC、NAND、USB 等介质启动的能力,这证明这些bootrom 内部的代码具备读SD、NAND 等能力。
嵌入式Linux 领域最著名的bootloader 是U-Boot, 其代码仓库位于http://git.denx.de/u-boot.git/。早前,bootloader 需要将启动信息以ATAG 的形式封装,并且把ATAG 的地址填充在r2 寄存器中,机型号填充在r1 寄存器中,详见内核文档Documentation/arm/booting。在ARM Linux 支持设备树(Device Tree)后,bootloader 则需要把dtb 的地址放入r2 寄存器中。
当然,ARM Linux 也支持直接把dtb 和zImage 绑定在一起的模式(内核ARM_APPENDED_DTB 选项“ Use appended device tree blob to zImage”),这样r2 寄存器就不再需要填充dtb 地址了。
类似zImage 的内核镜像实际上是由没有压缩的解压算法和被压缩的内核组成,所以在bootloader 跳入zImage 以后,它自身的解压缩逻辑就把内核的镜像解压缩出来了。关于内核启动,与我们关系比较大的部分是每个平台的设备回调函数和设备属性信息,它们通常包装在DT_MACHINE_START 和MACHINE_END 之间, 包含reserve()、map_io()、init_machine()、init_late()、smp 等回调函数或者属性。这些回调函数会在内核启动过程中被调用。
用户空间的init 程序常用的有busybox init、SysVinit、systemd 等,它们的职责类似,把整个系统启动,最后形成一个进程树,比如Ubuntu 上运行的pstree: