完成 8.14 Linux 系统的启动流程.
Signed-off-by: chen.yang <chen.yang@yuzhen-iot.com>
This commit is contained in:
parent
39fb85fe7a
commit
8d32078452
@ -44,15 +44,19 @@
|
||||
4. 装载 initramfs
|
||||
5. 调用系统内核
|
||||
|
||||
最为常见的二级引导程序有:GRUB、UBoot 和 Systemd-Boot 等,其中 UBoot 是在嵌入式系统中使用最广泛的二级引导程序,其特点是功能强大,移植容易。它能够引导 zImage、uImage 等类型的内核镜像、装载 ramfs,还支持一种 FIT 引导方式。
|
||||
最为常见的二级引导程序有:GRUB、UBoot 和 Systemd-Boot 等,其中 UBoot 是在嵌入式系统中使用最广泛的二级引导程序,其特点是功能强大,移植容易。它能够引导 zImage、uImage 等类型的内核镜像、装载 ramfs 等。
|
||||
|
||||
zImage 是 Linux 定义的内核镜像格式,其特点是头部第 37~40 字节处为由 LINUX_ZIMAGE_MAGIC 宏定义的魔数:0x016F2818。zImage 的实质是从 vmlinuz/vmlinux(elf 格式) 通过 objcopy 得来的二进制程序文件,之后该程序文件又经过了压缩和增加头部信息等处理,进一步的减少了文件体积。而为了能正确解压该镜像文件,其中又嵌入了一段解压代码。
|
||||
|
||||
UBoot 等二级引导程序将 zImage 加载到系统的内存中,确切的说是[内核内存管理](./8.11_内核内存管理.md)中提到的直接映射区。之后引导程序跳转到内核首地址去执行,这个地址被称为内核的入口点。此后系统权限让度给 Linux 系统内核,解压算法首先被执行,将真正的内核镜像释放到正确的内存位置上。
|
||||
UBoot 等二级引导程序将 zImage 加载到系统的内存中,确切的说是[内核内存管理](./8.11_内核内存管理.md)中提到的直接映射区。之后引导程序跳转到内核首地址去执行,这个地址被称为内核的入口点(EP)。需要注意的是,内核入口点并不一定位于内核镜像文件的零偏移处。
|
||||
|
||||
为了加载 zImage,就需要提供一组参数,如内核加载地址,内核入口地址等。为了方便 UBoot 加载内核镜像,UBoot 增加了一种新的内核镜像格式——uImage。uImage 在 zImage 基础上增加了一个 64 字节的头,打包了魔数、镜像长度、内核加载地址、内核入口地址等加载内核时所需的参数。
|
||||
一旦统权限由 UBoot 让度给 Linux 系统内核,解压算法首先被执行,将真正的内核镜像释放到正确的内存位置上。
|
||||
|
||||
UBoot 提供了 mkimage 工具用于制作 uImage 文件,该命令基本格式如下:
|
||||
在加载内核时,UBoot 还需要预留 0x8000(32k) 的空间用于存储内核参数,例如对 console、init、isolcpus 的设定等都会存储在这个区域中。
|
||||
|
||||
为了加载 zImage,就需要提供一组内核加载信息,如:内核加载地址,内核入口地址等。为方便加载内核镜像,UBoot 增加了一种新的内核镜像格式——Legacy uImage。Legacy uImage 在 zImage 基础上增加了一个 64 字节的头,打包了魔数、镜像长度、内核加载地址、内核入口地址等加载内核时所需的参数。
|
||||
|
||||
UBoot 提供了 mkimage 工具用于制作 Legacy uImage 文件,该命令基本格式如下:
|
||||
|
||||
```bash
|
||||
# -A ==> set architecture to 'arch'(“alpha”,”arm”,”x86″,”ia64″,”m6k8″,”microblaze”,”mips”,”mips64″,”nios”,”nios2″,”ppc”,”s390″,”sh”,”sparc”,”sparc64″,“blackfin”,”avr32″)
|
||||
@ -67,60 +71,183 @@ UBoot 提供了 mkimage 工具用于制作 uImage 文件,该命令基本格式
|
||||
mkimage -n 'linux-2.6.32' -A arm -O linux -T kernel -C none -a 0x30008000 -e 0x30008040 -d zImage uImage
|
||||
```
|
||||
|
||||
## 内核阶段
|
||||
除了 Legacy uImage,UBoot 还扩展了一种 FIT uImage 格式。FIT 运用了与 Device Tree 类似的语法和打包格式。dtc 和 mkimage 工具在 Image Source File(.its) 文件的指导下,将镜像文件打包成一体。FIT uImage 不仅可以包含内核,还可以包含设备树文件等,从而实现 Unify Kernel 的愿景。
|
||||
|
||||
第二引导阶段代码,如 UBoot 等,将 Linux 内核镜像拷贝到 DDR 中,然后将控制权交给 Linux 内核。当内核运行起来后,就会初始化全部总线控制器、以及外部设备等。当完成全部初始化工作后,系统就具备了最大的执行能力。
|
||||
Image Source File 的语法和 Device Tree Source File 完全一样,只不过自定义了一些特有的节点,包括:
|
||||
|
||||
## 挂载文件系统
|
||||
* Images 节点:指定所要包含的二进制文件,可以指定多种类型的多个文件。
|
||||
* Configurations 节点:可以将不同类型的二进制文件,根据不同的场景,组合起来,形成一个个的配置项,u-boot在boot的时候,以配置项为单位加载、执行,这样就可以根据不同的场景,方便的选择不同的配置。
|
||||
|
||||
Once the booting or initialization of the drivers is done, there is a search of the rootfs device. Rootfs device location can also be configured or modified from the command line parameters of Linux. Command-line parameters for Linux are the environment variables in u-boot environment, hence to update the rootsfs device location is just a modification of the environment variable in u-boot. There is other information as well available in u-boot environment.
|
||||
示例如下:
|
||||
|
||||
## init 程序
|
||||
```its
|
||||
/*
|
||||
* U-Boot uImage source file with multiple kernels, ramdisks and FDT blobs
|
||||
*/
|
||||
|
||||
Rootfs device is searched and mounted and then the init process is searched within the rootfs device. After the init image is located control is passed on to the init after invoking the init process. This is the first userland process which starts execution. Once init gets the control, it initializes the userspace services by running the init scripts.
|
||||
/dts-v1/;
|
||||
|
||||
## systemd service
|
||||
/ {
|
||||
description = "Various kernels, ramdisks and FDT blobs";
|
||||
#address-cells = <1>;
|
||||
|
||||
All the daemons are started and system level services are started either executing the init services present in /etc/ or if the system is systemctl based system then all the services are started as per the guidelines mentioned for systemctl system. After all the services are started then shell program is invoked which creates a login session prompt for the user.
|
||||
images {
|
||||
kernel@1 {
|
||||
description = "vanilla-2.6.23";
|
||||
data = /incbin/("./vmlinux.bin.gz");
|
||||
type = "kernel";
|
||||
arch = "ppc";
|
||||
os = "linux";
|
||||
compression = "gzip";
|
||||
load = <00000000>;
|
||||
entry = <00000000>;
|
||||
hash@1 {
|
||||
algo = "md5";
|
||||
};
|
||||
hash@2 {
|
||||
algo = "sha1";
|
||||
};
|
||||
};
|
||||
|
||||
## 引导阶段
|
||||
kernel@2 {
|
||||
description = "2.6.23-denx";
|
||||
data = /incbin/("./2.6.23-denx.bin.gz");
|
||||
type = "kernel";
|
||||
arch = "ppc";
|
||||
os = "linux";
|
||||
compression = "gzip";
|
||||
load = <00000000>;
|
||||
entry = <00000000>;
|
||||
hash@1 {
|
||||
algo = "sha1";
|
||||
};
|
||||
};
|
||||
|
||||
引导系统将内核装载到指定位置并跳转到入口点去执行。在[内核内存管理](./8.11_内核内存管理.md)提到,内核对低端内存进行了直接映射,而引导系统加载内核的地址范围,也正是在此段区域。
|
||||
kernel@3 {
|
||||
description = "2.4.25-denx";
|
||||
data = /incbin/("./2.4.25-denx.bin.gz");
|
||||
type = "kernel";
|
||||
arch = "ppc";
|
||||
os = "linux";
|
||||
compression = "gzip";
|
||||
load = <00000000>;
|
||||
entry = <00000000>;
|
||||
hash@1 {
|
||||
algo = "md5";
|
||||
};
|
||||
};
|
||||
|
||||
引导系统将 zImage 加载到对应地址上后,就会执行 zImage 中的自解压程序,将内核释放。
|
||||
ramdisk@1 {
|
||||
description = "eldk-4.2-ramdisk";
|
||||
data = /incbin/("./eldk-4.2-ramdisk");
|
||||
type = "ramdisk";
|
||||
arch = "ppc";
|
||||
os = "linux";
|
||||
compression = "gzip";
|
||||
load = <00000000>;
|
||||
entry = <00000000>;
|
||||
hash@1 {
|
||||
algo = "sha1";
|
||||
};
|
||||
};
|
||||
|
||||
预留 0x8000(32k) 的空间给内核参数。
|
||||
ramdisk@2 {
|
||||
description = "eldk-3.1-ramdisk";
|
||||
data = /incbin/("./eldk-3.1-ramdisk");
|
||||
type = "ramdisk";
|
||||
arch = "ppc";
|
||||
os = "linux";
|
||||
compression = "gzip";
|
||||
load = <00000000>;
|
||||
entry = <00000000>;
|
||||
hash@1 {
|
||||
algo = "crc32";
|
||||
};
|
||||
};
|
||||
|
||||
TODO: 只要不冲突,UBoot 可以将 zImage 加载到任何位置,zImage 自己会将自己释放到正确位置上。
|
||||
fdt@1 {
|
||||
description = "tqm5200-fdt";
|
||||
data = /incbin/("./tqm5200.dtb");
|
||||
type = "flat_dt";
|
||||
arch = "ppc";
|
||||
compression = "none";
|
||||
hash@1 {
|
||||
algo = "crc32";
|
||||
};
|
||||
};
|
||||
|
||||
### UBoot 引导
|
||||
fdt@2 {
|
||||
description = "tqm5200s-fdt";
|
||||
data = /incbin/("./tqm5200s.dtb");
|
||||
type = "flat_dt";
|
||||
arch = "ppc";
|
||||
compression = "none";
|
||||
load = <00700000>;
|
||||
hash@1 {
|
||||
algo = "sha1";
|
||||
};
|
||||
};
|
||||
|
||||
TODO:
|
||||
};
|
||||
|
||||
LINUX_ZIMAGE_MAGIC
|
||||
configurations {
|
||||
default = "config@1";
|
||||
|
||||
1. 这个是定义的一个魔数,这个数等于0x016F2818,表示这个镜像是一个zImage。也就是说zImage格式的镜像中,在头部的一个固定位置存放了这个数作为格式标记。如果拿到了一个Image,去那个位置读取4个字节判断它是否等于LINUX_ZIMAGE_MAGIC,则可以知道这个镜像是不是一个zImage。
|
||||
2. 命令bootm 30008000,所以do_bootm的argc=2、argv[0]=boom、argv[1]=0x30008000,但是实际bootm命令还可以不带参数执行。如果不带参数直接bootm,则会从CFG_LOAD_ADDR地址去执行(定义位于x210_sd.h中)。
|
||||
3. zImage头部开始的第37~40字节处存放着zImage标志魔数,从这个位置取出然后对比LINUX_ZIMAGE_MAGIC。
|
||||
config@1 {
|
||||
description = "tqm5200 vanilla-2.6.23 configuration";
|
||||
kernel = "kernel@1";
|
||||
ramdisk = "ramdisk@1";
|
||||
fdt = "fdt@1";
|
||||
};
|
||||
|
||||
uImage启动
|
||||
config@2 {
|
||||
description = "tqm5200s denx-2.6.23 configuration";
|
||||
kernel = "kernel@2";
|
||||
ramdisk = "ramdisk@1";
|
||||
fdt = "fdt@2";
|
||||
};
|
||||
|
||||
1. LEGACY(遗留的),在do_bootm函数中,这种方式指的就是uImage的方式。
|
||||
2. uImage方式是uboot本身发明的支持linux启动的镜像格式,但是后来这种方式被一种新的方式替代,这个新的方式就是设备树方式(在do_bootm中叫FIT)。
|
||||
3. uImage的启动校验主要在boot_get_kernel函数中,主要任务就是校验uImage的头信息,并且得到真正的kernel的起始位置去启动。
|
||||
config@3 {
|
||||
description = "tqm5200s denx-2.4.25 configuration";
|
||||
kernel = "kernel@3";
|
||||
ramdisk = "ramdisk@2";
|
||||
};
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
镜像的entrypoint
|
||||
它包含了 3 种配置,每种配置使用了不同的 kernel、ramdisk 和 fdt,默认配置项由“default”指定,当然也可以在运行时指定。
|
||||
|
||||
1. ep就是entrypoint的缩写,就是程序入口。一个镜像文件的起始执行部分不是在镜像的开头(镜像开头有n个字节的头信息),真正的镜像文件执行时,第一句代码在镜像的中部某个字节处,相当于头是有一定的偏移量的。这个偏移量记录在头信息中。
|
||||
2. 一般执行一个镜像文件的过程是:第一步,先读取头信息,然后在头信息的特定地址找MAGIC_NUM,由此来确定镜像种类;第二步,对镜像进行校验;第三步,再次读取头信息,由头信息的特定地址知道这个镜像的各种信息(镜像长度、镜像种类、入口地址);第四步,去entrypoint处执行镜像。
|
||||
3. theKernel = (void (*)(int, int, uint))ep; —— 将ep赋值给theKernel,则这个函数指针就指向了内存中加载的OS镜像的真正入口地址(就是操作系统的第一句执行的代码)。
|
||||
使用 mkimage 制作 FIT uImage 的命令如下:
|
||||
|
||||
传参并启动操作
|
||||
```bash
|
||||
# 制作镜像
|
||||
mkimage -f <Image Source File>.its <FIT uImage File>.itb
|
||||
# 查看信息
|
||||
mkimage -l <FIT uImage File>.itb
|
||||
```
|
||||
|
||||
FIT uImage 还能够支持签名,以实现 SecureBoot 功能。
|
||||
|
||||
在调试时,经常使用到 UBoot 命令行,在该命令行下,使用 boot 系列命令来引导内核,区别如下:
|
||||
|
||||
* bootm:启动 ARM uImage。
|
||||
* booti:启动 ARM64 uImage。
|
||||
* bootz:启动 zImage。
|
||||
* bootefi:启动 EFI 分区中的内核镜像。
|
||||
* bootp:从网络加载并启动内核镜像。
|
||||
* nboot:启动 Nand Flash 中的内核镜像。
|
||||
|
||||
UBoot 启动内核镜像时,将使用一些参数对内核进行设定,这些参数以 UBoot 环境变量的形式来使用。这些环境变量通常被保存在外部存储器中,以实现数据的持久化。
|
||||
|
||||
## 内核阶段
|
||||
|
||||
一旦第二阶段引导程序将控制权交给内核后,内核就会初始化总线控制器、外设等。当完成全部初始化工作后,系统就具备了完全的执行能力。
|
||||
|
||||
内核阶段代码是从 arch/\<ARCH>/kernel/head.S 开始的,其大体流程如下:
|
||||
|
||||
```cpp
|
||||
arch/arm/kernel/head.S // 内核的启动汇编
|
||||
arch/\<ARCH>/kernel/head.S // 内核的启动汇编
|
||||
|
|
||||
+---__create_page_tables // 创建内核运行时所需的 rodata、data、bss 段
|
||||
|
|
||||
@ -137,7 +264,9 @@ arch/arm/kernel/head.S // 内核的启动汇编
|
||||
+---setup_command_line(command_line);
|
||||
```
|
||||
|
||||
## 内核模块的加载顺序
|
||||
head.S 将创建一个内核进程,称为 0 号进程,0 号进程是系统中全部其他进程的父进程。
|
||||
|
||||
内核完成自身初始化后,将会按一定顺序加载内核模块。这个顺序首先由 init 宏决定,其次由内核 Makefile 文件决定:
|
||||
|
||||
1. 由 init 宏决定了优先级:
|
||||
1. pure_initcall()
|
||||
@ -155,12 +284,40 @@ arch/arm/kernel/head.S // 内核的启动汇编
|
||||
|
||||
内核启动后就要挂载 rootfs,但是 rootfs 在外部存储器中,因此需要加载外存的驱动程序,而外存的驱动很可能也在 rootfs 中,必须先加载 rootfs 才能加载驱动。此时就陷入先有鸡还是先有蛋的问题中。Linux 系统解决此问题的办法就是使用 initramfs。
|
||||
|
||||
initramfs 是一个内存文件系统,可以一些必要的驱动程序或工具打包进该文件系统。这个文件系统由 Bootloader 加载进内存,如 GRUB 或 UBoot,内核直接使用即可。
|
||||
initramfs 是一个内存文件系统,可以将一些必要的驱动程序或工具打包进该文件系统。这个文件系统由二级引导程序加载进内存,如 UBoot,内核直接使用即可。mkimage 能够将 initramfs 的镜像文件一起打包到 uImage 中,initramfs 镜像文件也称作 ramdisk 文件。
|
||||
|
||||
initramfs 不是必须的,如果全部必须的驱动已经 build in 到内核,则可以不使用 initramfs。
|
||||
|
||||
## rootfs
|
||||
## 挂载文件系统
|
||||
|
||||
TODO: 挂载外存驱动和 init 进程谁先谁后?
|
||||
当内核和驱动全部加载并运行后,内核将挂载根文件系统。根文件系统可由内核的 root 参数进行指定,也能够通过 rootfstype、rootflags 等对根文件系统参数进行设定。
|
||||
|
||||
有了外存驱动以后,内核可以加载对应的外存驱动,执行第一个进程,即 init 程序,并挂载文件系统。但是都需要挂载哪些磁盘分区,哪个磁盘分区才是根文件系统呢?这是由 /etc/fstab 文件决定的。
|
||||
根文件系统也可以通过 /etc/fstab 文件来指定,该文件同时可以指定其他要挂载的文件系统,比如交换分区等:
|
||||
|
||||
```bash
|
||||
# /etc/fstab: static file system information.
|
||||
#
|
||||
# Use 'blkid' to print the universally unique identifier for a device; this may
|
||||
# be used with UUID= as a more robust way to name devices that works even if
|
||||
# disks are added and removed. See fstab(5).
|
||||
#
|
||||
# <file system> <mount point> <type> <options> <dump> <pass>
|
||||
UUID=XXXX-XXXX /boot/efi vfat umask=0077 0 2
|
||||
UUID=XXXX-XXXX-XXXX-XXXX-XXXX / ext4 defaults,noatime 0 1
|
||||
/swapfile swap swap defaults,noatime 0 0
|
||||
tmpfs
|
||||
```
|
||||
|
||||
以上 fstab 文件使用了磁盘分区的 UUID,当然也可以直接指定分区设备,如:/dev/sda1 等,但使用 UUID 的好处是,即便磁盘加载的顺序变了,挂载程序也能将该分区挂载到正确的挂载点上。
|
||||
|
||||
## init 程序
|
||||
|
||||
挂载完根文件系统后,内核将搜索并执行根文件系统中的 init 程序,init 是由 0 号进程创建的,称为 1 号进程,同时也是用户态的第一个进程,是用户态其他进程的父进程。
|
||||
|
||||
关于 1 号进程,比较有趣的一点是,1 号进程诞生时还处于内核态,然后逐步的切换到用户态去执行。init 程序进入到用户态后就会调用 init 脚本去启动一些用户态服务程序。
|
||||
|
||||
## Systemd Service
|
||||
|
||||
init 程序的设计非常简洁,因此一些复杂的系统服务无法通过 init 程序来实现。比如服务的启动、监控、日志记录等。在 init 脚本中,通常会启动其他系统服务管理器,Systemd 便是最常用的一种。
|
||||
|
||||
Systemd 服务由 /etc/systemd/system 下的文件进行配置,同时能够决定哪些服务将在启动时加载。当全部服务都被启动后,系统 shell 就会被建立起来,并最终为用户提供一个登陆会话。
|
||||
|
Loading…
x
Reference in New Issue
Block a user