LV050-系统启动流程简介
一、uboot 启动流程分析
1. PC 机、Linux、Andorid 启动过程
(1)PC 机启动过程: BIOS 和位于硬盘 MBR 中的 LILO 和 GRUB 等共同构成了 PC 的引导加载程序,通常 BIOS 完成检测系统硬件(如初始化 DDR 内存、 硬盘)以及分配资源等工作,随后把位于硬盘 MBP 里的 BootLoader 加载到 RAM 中,从此 BIOS 便会将控制权交由这段 BootLoader(LILO 和 GRUB 等)接管,BootLoader 主要负责将内核的 OS 镜像从硬盘加载到 RAM 当中运行,之后便跳转到 内核的入口地址去运行,OS 在启动之后 BIOS 就没有什么作用了。
(2)嵌入式 Linux 启动过程: 在嵌入式 Linux 当中,uboot 以及 OS 都部署在 flash 或 eMMC 当中,一般在嵌入式系统中很少有和 PC 机类似的 BIOS 程序,所以系统 的启动加载任务完全由 BootLoader 来负责,该 BootLoader 所做的工作和 PC 机的类似,都要初始化硬件(如 DDR 与 flash 或 eMMC)、 建立内存空间映射图等,接着将 OS 从 flash 或 eMMC 中读取到 DDR 中,最后启动 OS,OS 在启动之后 u-boot 也有什么作用了。
(2)Andorid 系统启动过程: 由于安卓系统是基于 linux 系统发展而来的,所以他们的启动过程基本一致,只是在内核启动以后加载根文件系统之后有差别。 Andorid 系统启动可以概括为两个阶段,第一阶段是 uboot 到 OS,第二阶段是 OS 启动之后到根文件系统加载再到命令的执行,andorid 系统的启动与嵌入式 Linux 的主要差别为第二阶段的不同。
嵌入式 Linux 系统和 PC 机的启动过程几乎一样,只不过是 uboot 替代了 BIOS,flash 或 eMMC 替代了硬盘而已,嵌入式 Linux 系统与 andorid 更加类似,只是 andorid 在加载根文件系统之后有差异,三者都是类似的。
2. u-boot 启动流程简要分析
2.1 iROM
imx6ull 的启动方式与大多数 ARM 芯片启动类似,还未启动 linux 启动前,可以将 imx6ull 理解为与 STM32 类似的单片机,因为没有开启 mmu,它访问的所有 地址都是实际的物理地址,imx6ull 启动是,首先会去 0x0000_0000 地址处开始取值并执行。那么问题来了,0x0000_0000 地址到底在哪里?他在芯片内部 iROM 中。
我们可以看《i.MX 6ULL Applications Processor Reference Manual 》的 8.4.1 节:

iROM 是芯片内部自带的 ROM,里面存放的也是一段程序。也就是说,imx6ull 芯片上电后第一个执行的就是其内部 iROM 的一段程序,这段程序是芯片厂商写好并固化在 imx6ull 芯片 iROM 中的。 与生俱来,那 iROM 里面的程序有什么用,为什么要执行这段程序而不是直接执行我们下载到外部 flash 或者 emmc 中的程序呢?这段程序是 NXP 官方写的,它的源代码是不对外开放的。
2.2 启动方式
这段 iROM 代码是为了方便用户做的,我们都知道我们的开发板或者其他大多数开发板板都有一个拨码开关,我们可以拨动拨码开关来设置芯片的启动方式(nand 启动、emmc 启动或者 sd 卡启动等), 为什么可以设置其启动方式?我们写的程序它都还没加载,是谁赋予了它这个能力?这就是 iROM 的作用,他会根据系统复位后当前的 boot 启动模式配置引脚的电平状态来决定从哪里加载 u-boot,如下图所示:

这里面的引脚都控制什么?我们可以看这里:裸机开发部分的imx6ull启动方式相关笔记。
2.3 启动流程
iROM 启动流程如下图所示,大致是分成两种启动模式,第一种是下载模式,第二种是内部引导模式,复位后检查 boot 引脚电平状态,判断是使用那种引导模式,若是内部引导模式,则加载并验证引导镜像,然后执行引导程序;否则进入串口(USB 或 UART)下载模式,下载完之后便执行引导程序。

再来看下其启动流程图:

可以分为四个步骤:
- 芯片上电默认到 0x0000_0000 地址(iROM)处取指,iROM 内部固化的程序(就是前面学习的 boot ROM 程序)负责完成系统基本功能的初始化,如时钟和堆栈,然后 iROM 从一个特定的启动设备中引导镜像(bootloader)到内部 128KB 的 RAM 中,iROM 通过判断拨码开关状态来决定启动设备(Flash、eMMC、SD 等),并根据安全引导键值执行完整性检查引导镜像。
- 将启动设备中的前 4KB 数据拷贝到 iRAM 中运行,这段数据包含了 Program image(飞思卡尔定义的一个镜像数据结构,就是前面学习映像文件的 IVT 等相关信息),它包含了启动数据的地址以及长度,告诉 boot ROM 将启动数据拷贝到哪里以及拷贝多大,然后初始化外部 DRAM 控制器。
- 在初始化完 DRAM 控制器之后,bootloader 主体部分被拷贝至外部 DRAM 中(就是 DDR),bootloader 随后便将 linux 操作系统镜像从启动设备加载到 DRAM 中,并对 OS 完整性检查。
- 引导完成之后,bootloader 跳到操作系统中去运行,启动 Linux 内核。
二、Linux 内核又做了哪些事情
1. linux 启动后做了什么?
linux 启动以后,接着就是挂载文件系统。那么有个问题,linux 和文件系统是什么关系?linux 启动以后不挂载文件系统可以么?
Linux 和 STM32 上谈到的传统的 ucos、FreeRTOS 不一样,Linux 运行以后必须挂载文件系统,注意!是在 linux 运行后才挂载的。那安卓系统又是什么系统? 安卓系统也是基于 Linux 系统的,他和 Qt、ubuntu 系统一样都基于 linux 系统,区分他们的就是文件系统不一样,这好几套系统底层全是 Linux。
Linux 内核启动以后就开始挂载文件系统,然后找到找到文件系统中的初始化脚本,开始启动一个又一个服务(或应用程序)。挂载文件系统的方式有两种,一种是 通过 nfs 的方式挂载网络文件系统,另一种方式是 从块设备挂载文件系统,以那种方式挂载,取决于对 bootargs 中 root 参数的设置,设置 rootfstype 参数以表示挂载的文件系统的 类型。如“bootargs = console = ttymxc0,115200 root =/dev/mmcblk0p2 rw init =/linuxrc rootfstype = ext2”表示使用串口 0 作为控制终端,挂载 mmc 中第 0 块的第二个扇区作为文件系统,文件系统类型为 ext2。 init 参数是什么?这是我们接下来要学习的。
2. 传统的启动方式
在以前,当 Linux 启动后,首先需要禁止中断并进入 SVC 模式,然后配置好各种环境,之后创建第一个进程, 也就是 init 进程,该进程完成了根文件系统的挂载,init 是 Linux 系统操作中不可缺少的程序之一, 所谓的 init 进程,它是一个由内核启动的用户级进程。内核会在过去曾使用过 init 的几个地方查找它, 它的正确位置(对 Linux 系统来说)是/sbin/init。如果内核找不到 init,它就会试着运行/bin/sh, 如果没有找到或者运行失败,那么系统的启动也会失败。内核自行启动(已经被载入内存,开始运行, 并已初始化所有的设备驱动程序和数据结构等)之后,就通过启动一个用户级程序 init 的方式,完成引导进程。 所以 init 始终是第一个进程(其进程编号始终为 1)。

/etc/init 的主要的功能就是准备软件运行的环境,包括系统的主机名称、网络配置、语系处理、文件系统格式及其他服务的启动等。 而启动 init 进程的配置文件是/etc/inittab,/etc/inittab 文件是 init 在启动时读取的配置文件,也就是指挥官的决策书, 这个决策书规定了当前战争的局势,比如和平局势、冷战局势、战争局势。以及该局势下的核心策略。 init 就是这样,通过 inittab 这个文件控制了计算机的启动级别,及该级别下启动的进程。 我们一般默认的启动级别是 5,其启动级别可配置为 0~6,当设为 0 时,代表系统停机,你可以尝试输入“init 0”指令查看系统是否停机,当设置为 6 时,系统会重启。 init 启动脚本位置位于/etc/init.d 目录符号链接到不同的 RunLevel 目录 (比如/etc/rc3.d、/etc/rc5.d 等)。
传统的 init 启动也带来了一些问题
- 启动时间太长。init 进程是串行启动的,也就是说只有当前一个进程启动之后才能接着启动下一个进程。
- 启动脚本复杂。init 进程只是执行启动脚本,并不搭理其他事情,脚本需要自己处理各种情况,如此会使得脚本变得很长。
3. Systemd 启动方式
说明:我移植的 4.19 版本的内核好像还是原来的方式,这里还是以教程中的图为准吧。
现在,采用的是 systemd 方式启动,systemd 就是为了解决传统 init 启动问题而诞生的。其设计目标是,为系统的启动和管理提供一套完整的解决方案。
systemd 即为 system daemon, 是 linux 下的一种 init 软件, 由 Lennart Poettering 带头开发, 并在 LGPL 2.1 及其后续版本许可证下开源发布, 开发目标是提供更优秀的框架以表示系统服务间的依赖关系, 并依此实现系统初始化时服务的并行启动,同时达到降低 Shell 的系统开销的效果,最终代替常用的 System V 与 BSD 风格 init 程序。 当内核自解压完成,则加载 systemd 进程,并转移控制权到 systemd。
输入“ps -A”查看所有进程,可以看到 Systemd 的进程 PID 为 1,说明它是系统启动后运行的第一个进程,其他程序的启动由它负责, 功能还包括日志进程、控制基础系统配置,维护登陆用户列表以及系统账户、运行时目录和设置,可以运行容器和虚拟机, 可以简单的管理网络配置、网络时间同步、日志转发和名称解析等。
在传统的 init 进程的配置文件是/etc/inittab,各种服务的配置文件存放在/etc/sysconfig 目录。现在的配置文件主要存放在/lib/systemd 目录, 在/etc/systemd 目录里面的修改可以覆盖原始设置。且传统的 init 启动模式中,会有 runlevel(运行等级)的概念,在 Systemd 启动方式中,它被称为一种 Target, 与之不同的是,runlevel 是互斥的,也就是说不能同时有多个 runlevel 启动,而 target 允许多个启动。
| runlevel | Target | 注释 |
|---|---|---|
| 0 | poweroff.target | 关闭系统 |
| 1 | rescue.target | 维护模式(单用户模式) |
| 2,3,4 | multi-user.target | 多用户,无图形界面。用户可以通过终端或网络登录。 |
| 5 | graphical.target | 多用户,图形界面。继承级别 3 的服务,并启动图形界面服务。 |
| 6 | reboot.target | 重启系统 |
我们输入以下命令:
systemd-analyze plot > boot.svg
# 或
systemctl status -l可以查看系统启动时都做了哪些工作,输入此命令后, 系统把整个引导过程写入一个 SVG 格式文件里。整个引导过程非常长不方便阅读, 所以通过这个命令我们可以把输出写入一个文件,之后再查看和分析。

可以看到系统以 systemd 为首,创建了很多服务。比如创建打印 logo 服务、dbus 服务、avahi 服务、系统登陆服务等。 与 init 相比,Systemd 采用的时并行启动,它为每个需要启动的守护进程建立一个套接字(如上图所示.socket),这样使得 不同进程之间实现信息交互。且 Systemd 会创建新的进程并且为其分配一个控制组,而且处于不同控制组的进程之间可以实现互相通信。 Systemd 初始化系统引导,完成相关的初始化工作,它执行 default.target 以获得设定的启动 target(输入“systemctl get-default”命令可知默认的 target 为 graphical.target),接着 Systemd 执行相应的启动单元文件, 依据单元文件中定义的依赖关系,传递控制权,依次执行其他的 target 单元文件。
我们输入
cat /lib/systemd/system/graphical.target命令,查看默认的 target→ graphical.target 单元文件的依赖关系。

由上可知,他将启动 multi-user.target、rescue.service、rescue.target、display-manager.service,而 multi-user.target 又有相关的依赖,继续查看 multi-user.target 单元文件的依赖关系。

根据依赖关系总结得出它会依次执行 multi-user.target→ basic.target→ sysinit.target→ local-fs.target→ local-fs-pre.target→… 同时启动的每个 target 包含位于/etc/systemd/system/目录下的 Unit。
systemd 先到这里,后面我们将详细学习 systemd。
三、启动过程总结
