Linux内核Kernel启动过程

在上一篇计算机启动过程文章中介绍了计算机启动的基本流程,本篇文章主要介绍Linux内核Kernel的启动过程。

一、内核启动的基本流程

sequenceDiagram participant Bootloader participant Kernel participant InitProcess Bootloader->>Kernel: 加载内核映像 Kernel->>Kernel: 内核解压 Kernel->>Kernel: 内核启动 Kernel->>Kernel: 调用start_kernel函数 Kernel->>InitProcess: 启动初始进程

1. 启动加载程序 (Bootloader)

启动加载程序(如GRUB、LILO、syslinux等)负责将内核映像从存储设备加载到内存中,并准备好内核启动所需的环境。

  • 加载内核映像:启动加载程序将压缩的内核映像(如vmlinuz)从硬盘加载到内存中。内核映像通常是一个gzip或其他格式压缩的二进制文件。
  • 加载initrd/initramfs:如果使用initrd(初始RAM盘)或initramfs(初始RAM文件系统),启动加载程序也会将这些文件加载到内存中,以便内核在启动时使用。

2. 内核解压阶段

在内核映像的开头,有一个小的解压缩程序,它负责解压内核的主体部分。

  • 解压内核:内核映像被加载到内存后,解压缩程序会运行并将压缩的内核映像解压到适当的内存位置。
  • 跳转到解压后的内核:一旦解压完成,控制权会被移交给解压后的内核代码的入口点。

3. 内核启动(Kernel Startup)

解压后的内核代码会从一个固定的入口点开始执行,这个入口点是平台和架构相关的。对于x86架构,通常是startup_32或startup_64函数。

  • 架构特定的初始化:根据具体的硬件架构,内核会执行一些必要的初始化步骤,比如设置CPU的运行模式,初始化分页机制,建立基本的内存映射等。
  • 初始化内核堆栈:内核设置好自己的堆栈,以便后续的函数调用和操作。
  • 调用start_kernel函数:完成基础的硬件初始化后,内核会调用start_kernel函数,这是内核初始化的核心部分。

4. start_kernel函数

start_kernel函数位于init/main.c文件中,负责完成大部分内核的初始化工作。

  • 初始化控制台:设置内核的打印机制,以便后续的输出可以显示出来。
  • 初始化内存管理子系统:建立初始的内存管理结构,准备好内存分配机制。
  • 检测和初始化硬件设备:内核会检测并初始化系统中的各种硬件设备和驱动程序。
  • 启动中断处理机制:设置和启动中断处理机制,使得内核可以响应硬件中断。
  • 初始化内核调度器:初始化内核调度器,以便管理进程调度。
  • 加载初始进程:内核创建并启动第一个用户空间进程,通常是/sbin/init。

5. 启动初始进程

init进程是用户空间的第一个进程,负责进一步的系统初始化工作,包括启动系统服务和守护进程。

  • init进程的初始化:init进程执行系统初始化脚本,设置各种系统参数和启动服务。
  • 启动用户空间服务:最终,init进程启动配置的所有用户空间服务和守护进程,从而完成系统的启动过程。

二、内核文件加载及解压缩

1.为什么是压缩文件

Linux内核映像通常是一个压缩文件,主要有以下原因:

  • 减少存储空间: 压缩内核映像可以显著减少其在存储设备上的占用空间。这对嵌入式系统、存储资源有限的设备以及需要快速分发和更新内核的环境尤其重要
  • 加快加载速度: 压缩文件占用的空间更小,这意味着启动加载程序从磁盘读取文件到内存中的时间会更短。虽然解压缩内核映像需要一些时间,但现代处理器的解压缩速度非常快,通常解压缩的时间比从存储设备读取更多数据的时间要少。这会整体上加快启动过程。
  • 提高传输效率: 在网络上传输内核映像时,压缩文件可以显著减少带宽使用量。这对于需要远程更新内核的系统(OTA)非常有利。
  • 便于管理和分发: 压缩内核映像更便于在各种介质上分发,比如光盘、U盘等。一个较小的文件更容易管理、备份和分发。
  • 标准化处理: 使用压缩内核映像是一种标准做法,启动加载程序(如GRUB)已经能够很好地支持这种格式,能自动识别并处理压缩的内核映像。这使得系统启动过程更简单可靠。

2.文件类型vmlinuxz和bzImage

在连接压缩映像文件之前,我们先来了解一下未经压缩的编译文件vmlinux

2.1 什么是vmlinux?

vmlinux内核编译过程中生成的一个包含所有内核代码和数据的二进制文件。它是未经压缩和未经过处理的内核映像,通常位于内核源码目录的根目录下,特性如下:

  • 未压缩:vmlinux 是内核的未压缩映像。它包含所有内核代码、内核模块以及相关的数据结构。
  • ELF 格式:vmlinux 通常是一个 ELF(Executable and Linkable Format)文件,这是一个标准的可执行文件格式,用于存储可执行文件、目标代码和共享库等。
  • 符号信息:vmlinux 文件中包含调试符号和符号表信息,这些信息对内核调试和分析非常重要。
  • 没有文件后缀:虽然 vmlinux 通常没有文件后缀,但它是一个标准的 ELF 文件,可以通过文件头信息识别其格式。

2.2 vmlinux的生成过程

编译Linux内核时,vmlinux是在链接阶段生成的。以下是一个简化的生成过程:

  • 编译各个源文件:内核的各个源文件(.c.S 文件)首先被编译为目标文件(.o 文件)。
  • 链接目标文件:所有目标文件通过链接器(如ld)链接在一起,生成一个完整的内核映像,这个映像就是 vmlinux。

链接命令举例:ld -o vmlinux [object files] [linker scripts]

2.3 vmlinuxz和bzImage的生成过程

在获得编译文件vmlinux后,通常使用压缩工具做进一步处理。

  • 压缩内核映像:将 vmlinux 压缩生成 vmlinuz。通常使用 gzip 或其他压缩工具。

压缩命令: gzip -c vmlinux > vmlinuz

  • 生成引导加载程序格式的内核映像:一些系统需要特定格式的内核映像,例如 bzImage(适用于 x86 架构)。

生成命令:make bzImage

2.4 其他压缩格式

vmlinuzbzImagezImageuImage 都是不同的 Linux 内核映像文件格式,它们各自有不同的用途和特性。

  • vmlinuz:通用的压缩内核映像名称,主要用于各种 Linux 发行版。通常使用 gzip 压缩。
  • bzImage:大内核映像,解决了早期 zImage 的内存限制问题。用于 x86 架构,支持较大的内核映像。
  • zImage:较老的内核映像格式,适用于小内核映像,受限于低内存地址空间。
  • uImage:U-Boot 使用的内核映像格式,广泛用于嵌入式系统。包含 U-Boot 头部信息,支持多种压缩算法。

2.5 Android系统文件

在 Android 系统中,内核的压缩文件格式通常是zImageImage.gz,具体取决于所使用的启动加载程序和设备的要求。

  • zImage:在一些早期的 Android 设备上,内核映像可能采用 zImage 格式。这种格式的内核映像通常会被启动加载程序直接加载并解压,然后启动内核。
  • Image.gz:Image.gz 是指经过 gzip 压缩的内核映像。这种格式的内核映像通常是 Linux 内核编译过程中生成的 vmlinuz 文件,只是在 Android 系统中可能被重新命名为 Image.gz。启动加载程序会加载这个压缩的内核映像,并在加载到内存后解压缩,然后启动内核。

3.内核加载过程

sequenceDiagram participant BootLoader participant KernelEntry participant DecompressionCode participant DecompressionFunction participant KernelStartup BootLoader->>KernelEntry: 跳转到内核入口点 KernelEntry->>DecompressionCode: 跳转到解压缩代码 DecompressionCode->>DecompressionFunction: 调用解压缩函数 DecompressionFunction->>DecompressionFunction: 解压内核映像 DecompressionFunction->>DecompressionCode: 返回解压结果 DecompressionCode->>KernelStartup: 跳转到内核启动

3.1 内核映像加载到内存中

启动加载程序(Bootloader)负责将压缩的内核映像加载到内存中,并准备好启动内核的环境。

  • 加载内核和initrd:GRUB 会根据配置文件(通常是 grub.cfg)加载压缩的内核映像和可选的 initrd/initramfs 文件。
  • 设置内核参数:GRUB 会设置内核启动参数,这些参数可以通过命令行传递给内核。
  • 跳转到内核入口点:GRUB 将控制权转移到内核映像的入口点。对于 x86 架构,这个入口点通常在内核映像的开头。

3.1.1 启动BootLoader

以BIOS为例

  • CPU在重置后执行的第一条指令的内存地址0xfffffff0,它包含一个 jump 指令,这个指令通常指向BIOS入口点。
  • BIOS会进行一系列硬件初始化和自检,然后根据设置(例如启动顺序)选择一个启动设备(如硬盘、光盘、USB 等)
  • 将控制权转移到启动设备的启动扇区代码。

3.1.2 加载内核文件

  • 启动设备的启动扇区代码被执行,通常这段代码非常小,只占用一个扇区(512字节)。
  • 启动扇区代码负责完成一些基本的初始化操作,然后跳转到更复杂的引导加载程序,如 GRUB 的核心映像(core image)。
  • 核心映像开始执行,它负责进一步的初始化操作,如加载GRUB的模块和配置文件(grub.cfg)。
  • 根据grub.cfg文件中的配置,GRUB加载压缩的内核映像(vmlinuz)和可选的initrd/initramfs文件。
  • 内核映像加载完成后,GRUB 将控制权转移给内核的入口点代码,完成控制权从 BIOS 到内核的转移。

3.2 内核解压

以下以x86系统为例

3.2.1 关键文件和代码路径

  • arch/x86/boot/header.S:启动代码的汇编部分,定义了内核入口点。
  • arch/x86/boot/compressed/head_64.S 和 arch/x86/boot/compressed/misc.c:解压缩代码。
  • arch/x86/kernel/head_64.S 和 arch/x86/kernel/head.c:解压后的内核启动代码。

3.2.2 主要步骤

3.2.2.1 启动加载程序跳转到内核入口点:
  • BootLoader根据grub.cfg文件中的配置加载内核映像(vmlinuz)到内存,并跳转到内核映像的入口点,即内核代码的起始地址。
3.2.2.2 解压缩程序的初始化:
  • 内核入口点代码(在 header.S 中)会设置初始的 CPU 状态和内存环境,然后跳转到解压缩代码的入口。
  • 32位方法startup_32,64位方法startup_64。(长模式的32到64转换这里不做讨论,