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在跑,内存管理还没建立,连中断都还没开。它要做的,就是把所有核心子系统一个一个地“点亮”。

我习惯把它的执行流程分成几个关键阶段:

  1. 早期架构初始化:设置页表、初始化CPU的各个模式、打开MMU(内存管理单元)。这一步做完,虚拟地址空间就建立起来了。
  2. 中断与异常处理:初始化中断控制器,设置中断向量表。从此,CPU可以响应外部事件了。
  3. 内存管理初始化:建立伙伴系统、slab分配器。内核自己分配内存的能力,就是从这里开始的。
  4. 进程管理初始化:创建第一个内核线程——init进程(也就是进程1)。
  5. 设备与驱动初始化:枚举总线上的设备,加载驱动。
  6. 文件系统挂载:挂载根文件系统,让系统能访问文件。

你看,这个顺序是有讲究的。你不能先挂文件系统再初始化内存,因为文件系统本身就需要内存来缓存数据。每一步都依赖前一步的成果。

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程序路径。只要有一个成功了,系统就活了。如果全失败了,内核就崩溃给你看。

我曾经踩过的坑: 有一次我定制了一个嵌入式系统,为了省空间,把根文件系统里的 /sbin/init 给删了,以为用 /bin/sh 就行。结果内核启动到最后一步,直接 panic 了。后来才意识到,内核是按顺序尝试的,它先找 /sbin/init,找不到才找 /bin/sh。但 /sbin/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进程是如何接管系统,启动各种服务的。那又是另一段精彩的故事了。