HaveFunWithEmbeddedSystem/Chapter8_SOC_与_Linux/8.14_Linux_系统的启动流程.md

17 KiB
Raw Permalink Blame History

8.14 Linux 系统的启动流程

从上电到系统启动

一般而言,系统从上电到 Linux 系统启动完毕需要经历以下主要阶段:

硬件系统复位 -> 硬件引导 -> 软件引导阶段 -> 内核阶段 -> initramfs -> rootfs -> initrc -> systemd service

通常硬件系统具有一组上电自动复位电路这将引起整个硬件系统的复位包括将某些寄存器赋予初始值以及将程序指针IP/PC指针指向复位位置。如果该位置具有有效指令则程序将继续执行从而进入到引导阶段。

随着系统复杂度的增加以及接口的持续标准化,一些硬件厂商倾向于在系统 ROM 中嵌入一段引导代码,用于完成一部分初始化工作,这段代码运行在其他软件引导程序之前,可认为是硬件引导程序,比如说 BIOS 系统。

以 ARM 平台为例,硬件引导阶段通常不具备外部 DRR 的驱动能力,只使用内部的 s-RAM 进行工作。硬件引导在完成一系列准备工作后,将加载第一阶段软件引导程序到 s-RAM 中的指定位置,并跳转到该程序位置继续执行,此时软件引导代码接管系统权限。

软件引导可以由一个程序完成,也可以分为多个引导程序,一般而言,第一级软件引导程序执行完毕后系统就具备的访问 DDR 的能力,具备简单的外部存储器的驱动能力,但不具备文件系统功能,因此无法从文件系统中加载程序,只能装载外存中 Boot 区的内容。外存 Boot 区存有二级引导程序,当二级引导程序运行完毕后,系统就具备了文件系统的访问能力。引导程序执行完毕后,将再次修改程序指针,使之指向内核程序段首地址,运行内核程序的第一条指令。

从整个上电和引导流程来看,引导程序的关键就是逐步的为下一级程序创造合适的运行环境,然后通过程序跳转等方式修改程序指针到下一级程序的首地址,直至系统具备全部的硬件访问能力,并能够从外存的文件系统中加载程序和文件,完成内核的引导和文件系统的装载工作。另外,对于二级软件引导系统而言,很可能需要提供一些额外的用户功能,比如命令行交互,串口、以太网和外部存储器的的驱动等,甚至是加载文件系统,当然也要准备好对应的中断资源。

引导系统体现的是从实际问题出发,实事求是的解决主要矛盾的核心思想。比如,由于硬件能力有限,最初的 x86 系统,其刚启动时,硬件只会加载磁盘第一个扇区并引导其运行,这一扇区中的程序又要能够继续访问外部存储器,加载后续程序,准备其运行环境,并执行之。这就要求这段程序必须十分简洁,有针对性的去解决实际问题,并且整个引导系统都要进行合理的设计。

软件引导阶段

如前文所述,软件引导可以分为多个阶段。对于基于 x86/64 的 Linux 系统,通常由 BIOS 完成硬件引导,然后交由 GRUB 进行后续引导,最后将控制权交给内核。

而对于基于 ARM 的 Linux 系统,通常先由硬件厂商定制的引导程序引导,然后交由 UBoot 等进行第二阶段引导,装载内核并启动系统。

第一阶段引导

对于一些 ARM 系统,其引导程序以二进制形式直接烧录于外部的 SPI Flash。加载这种引导程序不需要复杂的外存控制器驱动也不需要文件系统来支撑对于资源有限的硬件引导程序来说这种设计比较实际。

第一阶段引导程序的后续主要工作就是装载第二阶段引导系统,第二阶段引导系统可能位于外部 Flash、eMMC 甚至是 SD 卡中,为此,一阶段引导程序就必须具备相应设备的驱动能力,有的第一阶段引导程序只支持从其中一种存储器中装载程序,此时需要注意烧录镜像是否正确,或者在支持多存储器的情况下,注意拨码开关的设置。

一旦能成功访问到第二阶段引导程序,其将被装载到一个特定的内存地址中,第一阶段引导程序完成全部工作后,就跳转到这个地址,第二阶段引导程序开始执行。

值得注意的是,第一阶段引导程序也有可能划分为多个子阶段或子程序,但从功能上来看,它们都在从事第一阶段的引导工作。

第二阶段引导和 UBoot

第二阶段引导程序的主要工作是准备内核运行环境包括设置启动引导参数等决定使用的设备树文件内核或驱动的参数等例如Linux 内核文档中对于 ARM 平台引导阶段所需要做的事情给出了以下清单:

  1. 设置并初始化 RAM
  2. 探知硬件平台类型
  3. 设置设备树
  4. 装载 initramfs
  5. 调用系统内核

最为常见的二级引导程序有GRUB、UBoot 和 Systemd-Boot 等,其中 UBoot 是在嵌入式系统中使用最广泛的二级引导程序,其特点是功能强大,移植容易。它能够引导 zImage、uImage 等类型的内核镜像、装载 ramfs 等。

zImage 是 Linux 定义的内核镜像格式,其特点是头部第 37~40 字节处为由 LINUX_ZIMAGE_MAGIC 宏定义的魔数0x016F2818。zImage 的实质是从 vmlinuz/vmlinuxelf 格式) 通过 objcopy 得来的二进制程序文件,之后该程序文件又经过了压缩和增加头部信息等处理,进一步的减少了文件体积。而为了能正确解压该镜像文件,其中又嵌入了一段解压代码。

UBoot 等二级引导程序将 zImage 加载到系统的内存中,确切的说是内核内存管理中提到的直接映射区。之后引导程序跳转到内核首地址去执行这个地址被称为内核的入口点EP。需要注意的是内核入口点并不一定位于内核镜像文件的零偏移处。

一旦统权限由 UBoot 让度给 Linux 系统内核,解压算法首先被执行,将真正的内核镜像释放到正确的内存位置上。

在加载内核时UBoot 还需要预留 0x800032k 的空间用于存储内核参数,例如对 console、init、isolcpus 的设定等都会存储在这个区域中。

为了加载 zImage就需要提供一组内核加载信息内核加载地址内核入口地址等。为方便加载内核镜像UBoot 增加了一种新的内核镜像格式——Legacy uImage。Legacy uImage 在 zImage 基础上增加了一个 64 字节的头,打包了魔数、镜像长度、内核加载地址、内核入口地址等加载内核时所需的参数。

UBoot 提供了 mkimage 工具用于制作 Legacy uImage 文件,该命令基本格式如下:

# -A ==> set architecture to 'arch'(“alpha”,”arm”,”x86″,”ia64″,”m6k8″,”microblaze”,”mips”,”mips64″,”nios”,”nios2″,”ppc”,”s390″,”sh”,”sparc”,”sparc64″,“blackfin”,”avr32″)
# -O ==> set operating system to 'os'(“4_4bsd”,”artos”,”esix”,”freebsd”,”irix”,”linux”,”lynxos”,”ncr”,”netbsd”,”openbsd”,”psos”,”qnx”,”rtems”,”sco”,”sloaris”,“u-boot”,vxworks”)
# -T ==> set image type to 'type'(“filesystem”,”firmware”,”firmware”,”kernel”,”multi”,”ramdisk”,”script”,”standalone”,”flat_dt”)
# -C ==> set compression type 'comp'(“none”,”bzip2″,”gzip”)
# -a ==> set load address to 'addr' (hex)
# -e ==> set entry point to 'ep' (hex)(一般是-a参数指定的值加上0x40。因为前面有个mkimage添加的0x40个字节的头)
# -n ==> set image name to 'name'
# -d ==> use image data from 'datafile'
# -x ==> set XIP (execute in place)
mkimage -n 'linux-2.6.32' -A arm -O linux -T kernel -C none -a 0x30008000 -e 0x30008040 -d zImage uImage

除了 Legacy uImageUBoot 还扩展了一种 FIT uImage 格式。FIT 运用了与 Device Tree 类似的语法和打包格式。dtc 和 mkimage 工具在 Image Source File.its 文件的指导下将镜像文件打包成一体。FIT uImage 不仅可以包含内核,还可以包含设备树文件等,从而实现 Unify Kernel 的愿景。

Image Source File 的语法和 Device Tree Source File 完全一样,只不过自定义了一些特有的节点,包括:

  • Images 节点:指定所要包含的二进制文件,可以指定多种类型的多个文件。
  • Configurations 节点可以将不同类型的二进制文件根据不同的场景组合起来形成一个个的配置项u-boot在boot的时候以配置项为单位加载、执行这样就可以根据不同的场景方便的选择不同的配置。

示例如下:

/*
 * U-Boot uImage source file with multiple kernels, ramdisks and FDT blobs
 */

/dts-v1/;

/ {
    description = "Various kernels, ramdisks and FDT blobs";
    #address-cells = <1>;

    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";
            };
        };

        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";
            };
        };

        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";
            };
        };

        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";
            };
        };

        fdt@1 {
            description = "tqm5200-fdt";
            data = /incbin/("./tqm5200.dtb");
            type = "flat_dt";
            arch = "ppc";
            compression = "none";
            hash@1 {
                algo = "crc32";
            };
        };

        fdt@2 {
            description = "tqm5200s-fdt";
            data = /incbin/("./tqm5200s.dtb");
            type = "flat_dt";
            arch = "ppc";
            compression = "none";
            load = <00700000>;
            hash@1 {
                algo = "sha1";
            };
        };

    };

    configurations {
        default = "config@1";

        config@1 {
            description = "tqm5200 vanilla-2.6.23 configuration";
            kernel = "kernel@1";
            ramdisk = "ramdisk@1";
            fdt = "fdt@1";
        };

        config@2 {
            description = "tqm5200s denx-2.6.23 configuration";
            kernel = "kernel@2";
            ramdisk = "ramdisk@1";
            fdt = "fdt@2";
        };

        config@3 {
            description = "tqm5200s denx-2.4.25 configuration";
            kernel = "kernel@3";
            ramdisk = "ramdisk@2";
        };
    };
};

它包含了 3 种配置,每种配置使用了不同的 kernel、ramdisk 和 fdt默认配置项由“default”指定当然也可以在运行时指定。

使用 mkimage 制作 FIT uImage 的命令如下:

# 制作镜像
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 开始的,其大体流程如下:

arch/\<ARCH>/kernel/head.S  // 内核的启动汇编
|
+---__create_page_tables    // 创建内核运行时所需的 rodata、data、bss 段
|
+---secondary_start_kernel  // 跳转到 C 语言的入口函数
    |
    ----init/main.c > void __init start_kernel(void)   // C 语言的程序入口
        |
        +---arch/<arch>/kernel/setup.c -> setup_arch(&command_line);    // 体系结构初始化
        |   |
        |   +---setup_machine(machine_arch_type); //配置当前的机器类型
        |       |
        |       +----xxxxx_init_machine();
        |
        +---setup_command_line(command_line);

head.S 将创建一个内核进程,称为 0 号进程0 号进程是系统中全部其他进程的父进程。

内核完成自身初始化后,将会按一定顺序加载内核模块。这个顺序首先由 init 宏决定,其次由内核 Makefile 文件决定:

  1. 由 init 宏决定了优先级:
    1. pure_initcall()
    2. core_initcall()/core_initcall_sync()
    3. postcore_initcall()/postcore_initcall_sync()
    4. arch_initcall()/arch_initcall_sync()
    5. subsys_initcall()/subsys_initcall_sync()
    6. fs_initcall()/fs_initcall_sync()
    7. rootfs_initcall()
    8. device_initcall()/moudle_init()/device_initcall_sync()
    9. late_initcall()/late_initcall_sync()
  2. 同一优先级的模块由 Makefile 决定,排在 Makefile 前面的模块先加载。

initramfs

内核启动后就要挂载 rootfs但是 rootfs 在外部存储器中,因此需要加载外存的驱动程序,而外存的驱动很可能也在 rootfs 中,必须先加载 rootfs 才能加载驱动。此时就陷入先有鸡还是先有蛋的问题中。Linux 系统解决此问题的办法就是使用 initramfs。

initramfs 是一个内存文件系统,可以将一些必要的驱动程序或工具打包进该文件系统。这个文件系统由二级引导程序加载进内存,如 UBoot内核直接使用即可。mkimage 能够将 initramfs 的镜像文件一起打包到 uImage 中initramfs 镜像文件也称作 ramdisk 文件。

initramfs 不是必须的,如果全部必须的驱动已经 build in 到内核,则可以不使用 initramfs。

挂载文件系统

当内核和驱动全部加载并运行后,内核将挂载根文件系统。根文件系统可由内核的 root 参数进行指定,也能够通过 rootfstype、rootflags 等对根文件系统参数进行设定。

根文件系统也可以通过 /etc/fstab 文件来指定,该文件同时可以指定其他要挂载的文件系统,比如交换分区等:

# /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 就会被建立起来,并最终为用户提供一个登陆会话。