第三章 Linux内核启动:内核解压、设备树加载、init进程创建
好,我们继续往下走。上一章我们聊完了Bootloader的工作,它把内核镜像和dtb文件都加载到了内存里。现在,轮到Linux内核登场了。这一章,我会带你看看内核是怎么把自己解压出来的,怎么读取设备树,以及怎么创建出第一个用户空间进程——init。
说实话,这部分内容我当年啃了很久。因为内核启动的代码,大部分是汇编写的,而且涉及很多硬件细节。但别怕,我会把关键路径给你理清楚。
3.1 内核解压:从压缩镜像到可执行内核
你想想看,Bootloader加载到内存的zImage或Image.gz,其实是个压缩包。CPU没法直接执行压缩的代码。所以,第一步就是解压。
内核镜像的开头,有一段自解压代码。这段代码是用汇编写的,位置在 arch/arm/boot/compressed/head.S(ARM架构下)。如果是ARM64,路径类似,但细节有差异。
解压的过程,我简单概括一下:
- 设置解压地址:内核会检查当前运行地址和解压目标地址是否重叠。如果重叠,它会把自己先挪个位置,避免解压时覆盖自己。
- 解压算法:常见的有gzip、lz4、lzo。我记得在某个低端设备上,因为解压速度太慢,我们换过lz4,启动时间直接降了200ms。
- 跳转到解压后的入口:解压完成后,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)转换成内核可以操作的节点树结构。
个人经验:我建议你在调试启动问题时,打开内核的 earlyprintk 和 debug 选项。这样,设备树解析过程中的错误会直接打印出来。否则,设备树写错了,内核可能直接挂掉,连日志都看不到。
设备树里,最关键的几个节点:
| 节点路径 | 作用 |
|---|---|
/chosen |
传递启动参数(bootargs)、initrd地址等 |
/memory |
描述内存的起始地址和大小 |
/cpus |
描述CPU核心信息,比如架构、频率 |
/soc |
描述片上系统(SoC)内部的外设,比如UART、I2C、SPI |
说白了,设备树就是硬件的“身份证”。内核通过它,才知道该加载哪些驱动,不该加载哪些驱动。
3.3 init进程创建:从内核态到用户态
设备树解析完了,各种子系统也初始化好了(比如内存管理、调度器、中断系统)。这时候,内核要做一件非常重要的事——创建第一个用户空间进程。
这个进程,就是init进程。它的PID是1。
创建过程,我拆成几步来讲:
- 内核调用
kernel_init:这是一个内核线程,它负责执行用户空间的初始化。 - 查找init程序:内核会按顺序尝试以下几个路径:
/sbin/init/etc/init/bin/init/bin/sh(如果以上都找不到,最后尝试shell)
- 执行
run_init_process:找到init程序后,内核调用execve来执行它。这时候,内核线程就变成了用户空间的init进程。 - 挂载根文件系统:在执行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系统的核心服务的。