4、Linux内核启动:内核解压、初始化、进程1的诞生与核心子系统初始化
好,咱们接着往下走。上一章我们聊完了Bootloader,它把内核镜像从存储介质里捞出来,放到了内存里。现在,内核镜像就躺在那里,像个沉睡的巨人。接下来,就是把它唤醒的过程。
这一章,我们聚焦在Linux内核启动的核心环节。说白了,就是从一段压缩的二进制数据,变成一个能跑进程、能管内存、能调度任务的完整操作系统。我个人觉得,这是整个Android启动流程里最硬核、也最迷人的一段。
4.1 内核解压:从压缩包到可执行体
你想想看,现在的内核镜像动辄几兆甚至十几兆。Bootloader要是直接加载这么大的东西,那得等到猴年马月去。所以,内核镜像通常是压缩过的。最常见的是gzip压缩,也有用LZMA、LZO的。
Bootloader加载到内存里的,其实是这个压缩包的开头部分——一个叫做decompress_kernel()的小程序。它自己就是未压缩的,负责把后面的内核主体解压出来。
// arch/arm/boot/compressed/head.S (简化示意)
start:
// 设置栈指针
ldr sp, =stack_top
// 调用C语言解压函数
bl decompress_kernel
// 解压完成,跳转到解压后的内核入口
b __turn_mmu_on
嗯,这里要注意。解压的过程不是简单的“解压完就跑”。它要处理一个关键问题:解压后的内核可能会覆盖掉解压程序自己。所以,解压程序在运行时会小心翼翼地计算地址,确保自己不会被“自己”砸死。我在项目中遇到过几次解压失败的情况,最后发现是内存布局冲突,解压后的内核把解压程序的栈给踩了。那排查起来,真是让人头大。
解压完成后,控制权就交给了真正的内核入口——start_kernel()。这是C语言世界的起点。
4.2 内核初始化:start_kernel() 的宏大叙事
start_kernel()这个函数,你可以把它理解为操作系统的“构造函数”。它被调用时,系统还处于一个非常原始的状态:只有一个CPU在跑,内存管理还没建立,连中断都还没开。它要做的,就是把所有核心子系统一个一个地“点亮”。
我习惯把它的执行流程分成几个关键阶段:
- 早期架构初始化:设置页表、初始化CPU的各个模式、打开MMU(内存管理单元)。这一步做完,虚拟地址空间就建立起来了。
- 中断与异常处理:初始化中断控制器,设置中断向量表。从此,CPU可以响应外部事件了。
- 内存管理初始化:建立伙伴系统、slab分配器。内核自己分配内存的能力,就是从这里开始的。
- 进程管理初始化:创建第一个内核线程——
init进程(也就是进程1)。 - 设备与驱动初始化:枚举总线上的设备,加载驱动。
- 文件系统挂载:挂载根文件系统,让系统能访问文件。
你看,这个顺序是有讲究的。你不能先挂文件系统再初始化内存,因为文件系统本身就需要内存来缓存数据。每一步都依赖前一步的成果。
4.3 进程1的诞生:从0到1的跨越
在start_kernel()的最后阶段,它会调用kernel_init()。这个函数会做一件大事:创建进程1。
进程0是内核自己,它其实不是一个真正的进程,而是一个“空闲任务”。当CPU没事干的时候,就跑去执行进程0。而进程1,才是第一个真正的用户态进程。
创建进程1的过程,是通过kernel_thread()来实现的。它会复制当前的内核线程上下文,然后让新线程去执行init程序。
// init/main.c (简化)
static int __ref kernel_init(void *unused)
{
// ... 各种初始化 ...
// 尝试执行用户空间的init程序
if (ramdisk_execute_command) {
ret = run_init_process(ramdisk_execute_command);
if (!ret)
return 0;
}
// 默认路径
if (!try_to_run_init_process("/sbin/init") ||
!try_to_run_init_process("/etc/init") ||
!try_to_run_init_process("/bin/init") ||
!try_to_run_init_process("/bin/sh"))
return 0;
panic("No init found. Try passing init= option to kernel.");
}
这段代码的逻辑很直白:挨个尝试执行常见的init程序路径。只要有一个成功了,系统就活了。如果全失败了,内核就崩溃给你看。
4.4 核心子系统初始化:骨架与血肉
在进程1诞生之前,内核已经完成了大部分核心子系统的初始化。我们来快速过一遍几个最重要的:
| 子系统 | 初始化函数 | 核心作用 |
|---|---|---|
| 内存管理 | mm_init() | 建立页表、伙伴系统、slab分配器 |
| 进程调度 | sched_init() | 初始化调度器、就绪队列 |
| 中断管理 | init_IRQ() | 设置中断向量、初始化中断控制器 |
| 时间管理 | time_init() | 初始化系统时钟、定时器 |
| 块设备层 | blk_dev_init() | 初始化I/O调度器、请求队列 |
| 网络协议栈 | sock_init() | 初始化socket子系统、协议族注册 |
每个子系统的初始化,都像在搭积木。内存管理是地基,进程调度是骨架,中断和时间管理是心跳。没有这些,系统就是个死物。
initcall_debug。它会打印出每个初始化函数的调用时间和返回值。我在调试驱动加载顺序问题时,这个参数帮了大忙。
4.5 从内核态到用户态:最后的交接
当进程1成功执行了/sbin/init(在Android里是/init),内核就完成了它的历史使命。它从“统治者”变成了“服务者”。
内核会为进程1准备好一个完整的用户空间环境:
- 文件描述符0、1、2(标准输入、输出、错误)都指向控制台
- 信号处理函数设置为默认
- 进程的代码段、数据段、堆栈都已经映射好
然后,内核通过一条return指令,从内核态切换到用户态。CPU的特权级别从最高级(内核态)降到了最低级(用户态)。从此,进程1就在用户空间里自由驰骋了。
你想想看,从一段压缩的二进制数据,到此刻一个活生生的用户进程在跑,中间经历了多少步骤?解压、建页表、开MMU、初始化调度器、创建设备模型……每一步都环环相扣,缺一不可。我个人觉得,理解了这个过程,你就真正理解了“操作系统是怎么启动的”这个问题的核心。
下一章,我们会跟着进程1的脚步,看看Android的init进程是如何接管系统,启动各种服务的。那又是另一段精彩的故事了。