第三章 Linux内核启动:内核解压、设备树加载、init进程创建

好,我们继续往下走。上一章我们聊完了Bootloader的工作,它把内核镜像和dtb文件都加载到了内存里。现在,轮到Linux内核登场了。这一章,我会带你看看内核是怎么把自己解压出来的,怎么读取设备树,以及怎么创建出第一个用户空间进程——init。

说实话,这部分内容我当年啃了很久。因为内核启动的代码,大部分是汇编写的,而且涉及很多硬件细节。但别怕,我会把关键路径给你理清楚。

3.1 内核解压:从压缩镜像到可执行内核

你想想看,Bootloader加载到内存的zImage或Image.gz,其实是个压缩包。CPU没法直接执行压缩的代码。所以,第一步就是解压。

内核镜像的开头,有一段自解压代码。这段代码是用汇编写的,位置在 arch/arm/boot/compressed/head.S(ARM架构下)。如果是ARM64,路径类似,但细节有差异。

解压的过程,我简单概括一下:

  1. 设置解压地址:内核会检查当前运行地址和解压目标地址是否重叠。如果重叠,它会把自己先挪个位置,避免解压时覆盖自己。
  2. 解压算法:常见的有gzip、lz4、lzo。我记得在某个低端设备上,因为解压速度太慢,我们换过lz4,启动时间直接降了200ms。
  3. 跳转到解压后的入口:解压完成后,CPU跳转到 start_kernel 之前的入口函数,通常是 __mmap_switched 或类似的符号。

核心要点:解压阶段,MMU(内存管理单元)通常是关闭的。所有地址都是物理地址。所以,你看到的任何地址操作,都是直接操作物理内存。

我曾经遇到过一个坑:某个项目的Flash读取速度很慢,导致内核解压时频繁等待I/O。后来我们在Bootloader里提前把内核拷贝到DDR中,解压速度才正常。嗯,这里要注意,解压阶段的I/O性能,直接影响启动时间。

3.2 设备树加载:硬件描述的传递

解压完成后,内核开始执行真正的初始化代码。这时候,它需要知道当前硬件长什么样——CPU型号、内存大小、外设地址等等。这些信息,就藏在设备树(Device Tree)里。

设备树的加载,其实在Bootloader阶段就开始了。Bootloader会把dtb文件的物理地址传给内核。内核启动时,会从这个地址读取设备树。

具体流程是这样的:

  • 内核入口函数(比如 stext_text)会接收三个参数:r0、r1、r2。在ARM架构中,r2就是设备树的物理地址。
  • 早期解析:在 start_kernel 调用之前,汇编代码会做一些最基本的设备树解析,比如获取内存大小,以便设置页表。
  • 完整解析:进入 start_kernel 后,内核调用 setup_arch,其中会调用 unflatten_device_tree,把扁平设备树(FDT)转换成内核可以操作的节点树结构。

个人经验:我建议你在调试启动问题时,打开内核的 earlyprintkdebug 选项。这样,设备树解析过程中的错误会直接打印出来。否则,设备树写错了,内核可能直接挂掉,连日志都看不到。

设备树里,最关键的几个节点:

节点路径 作用
/chosen 传递启动参数(bootargs)、initrd地址等
/memory 描述内存的起始地址和大小
/cpus 描述CPU核心信息,比如架构、频率
/soc 描述片上系统(SoC)内部的外设,比如UART、I2C、SPI

说白了,设备树就是硬件的“身份证”。内核通过它,才知道该加载哪些驱动,不该加载哪些驱动。

3.3 init进程创建:从内核态到用户态

设备树解析完了,各种子系统也初始化好了(比如内存管理、调度器、中断系统)。这时候,内核要做一件非常重要的事——创建第一个用户空间进程。

这个进程,就是init进程。它的PID是1。

创建过程,我拆成几步来讲:

  1. 内核调用 kernel_init:这是一个内核线程,它负责执行用户空间的初始化。
  2. 查找init程序:内核会按顺序尝试以下几个路径:
    • /sbin/init
    • /etc/init
    • /bin/init
    • /bin/sh(如果以上都找不到,最后尝试shell)
  3. 执行 run_init_process:找到init程序后,内核调用 execve 来执行它。这时候,内核线程就变成了用户空间的init进程。
  4. 挂载根文件系统:在执行init之前,内核必须挂载根文件系统。根文件系统的类型和设备,由启动参数 root= 指定,或者从设备树的 /chosen 节点读取。

注意:如果根文件系统挂载失败,内核会panic。我曾经在一个项目里,因为内核编译时没把ext4驱动编进去,结果根文件系统是ext4格式,启动直接挂掉。排查了半天,最后发现是内核配置的问题。

init进程启动后,它会读取配置文件(比如 /etc/inittab/init.rc),然后启动各种系统服务。在Android系统中,init进程会解析 init.rc 文件,启动zygote、servicemanager等核心进程。

嗯,这里有个细节:init进程是内核态切换到用户态的分水岭。在此之前,系统运行在内核态,只有内核线程。在此之后,系统进入用户态,init进程成为所有用户进程的祖先。

3.4 启动过程中的关键日志

我个人习惯,在调试启动问题时,会重点关注以下几行内核日志:

  • Uncompressing Linux... done, booting the kernel. —— 解压完成
  • Machine model: ... —— 设备树中的机器型号
  • Kernel command line: ... —— 内核启动参数
  • VFS: Mounted root ... filesystem on device ... —— 根文件系统挂载成功
  • Run /init as init process —— init进程开始执行

如果启动卡住了,看看最后一条日志出现在哪里。比如,如果卡在 Run /init 之前,那多半是根文件系统的问题。如果卡在之后,那就是init进程本身或它的配置文件有问题。

好了,这一章的内容就到这里。我们从内核解压,聊到了设备树加载,最后看到了init进程的诞生。下一章,我们会深入init进程,看看它是怎么解析 init.rc,以及怎么启动Android系统的核心服务的。