第三章 Linux内核启动:内核初始化流程

好,咱们继续往下走。上一章我们聊了引导加载程序怎么把内核请进内存,现在内核终于要开始自己干活了。说实话,内核初始化这部分,我当年啃源码的时候啃得头皮发麻——代码量太大了。但后来我发现,抓住几个核心子系统的启动顺序,整个脉络就清晰了。

3.1 从start_kernel说起

内核启动的入口函数是start_kernel,在init/main.c里。这函数就像交响乐的指挥,把所有子系统按顺序叫醒。我个人习惯把它的工作分成三个阶段:

  1. 早期初始化:架构相关、内存管理基础、中断
  2. 核心子系统初始化:进程调度、内存管理、设备驱动
  3. 用户空间启动:挂载根文件系统、启动init进程

来看看代码长什么样:

// init/main.c
asmlinkage __visible void __init start_kernel(void)
{
    char *command_line;
    char *after_dashes;

    // 早期初始化
    setup_arch(&command_line);      // 架构相关
    mm_init_cpumask(&init_mm);     // 内存管理基础
    setup_command_line(command_line);

    // 核心子系统
    trap_init();                    // 中断向量表
    mm_init();                      // 内存管理核心
    sched_init();                   // 进程调度器
    init_IRQ();                     // 中断控制器
    softirq_init();                 // 软中断
    time_init();                    // 时钟系统

    // 设备驱动
    console_init();                 // 控制台
    vfs_caches_init();              // 虚拟文件系统
    rest_init();                    // 启动用户空间
}

嗯,这里要注意,rest_init之后会创建两个内核线程:kernel_initkthreadd。前者负责启动用户空间的init进程,后者是内核线程的守护进程。

3.2 进程调度器的初始化

调度器是操作系统的灵魂。你想想看,没有调度器,CPU就不知道先跑哪个任务。Android系统里,调度器的好坏直接影响App的流畅度。

sched_init()函数主要干了三件事:

  • 初始化每个CPU的运行队列(runqueue)
  • 设置调度类(CFS、实时调度等)
  • 初始化当前进程的调度信息

我记得有一次在项目中遇到一个诡异的问题:某个App偶尔会卡顿几百毫秒。查了半天,发现是调度器的负载均衡算法在迁移进程时,没有考虑到某些CPU的缓存亲和性。说白了,就是进程被迁移到了另一个CPU,缓存全失效了,性能瞬间掉下来。

核心要点:调度器初始化时,会为每个CPU创建一个空闲进程(idle task)。这个进程的优先级最低,只有当CPU没有其他任务可运行时才会执行它。在Android设备上,idle进程通常会让CPU进入低功耗模式。

3.3 内存管理子系统

内存管理这块,我建议你重点关注mm_init()。它负责建立内核的内存映射,初始化伙伴系统和slab分配器。

流程大致是这样的:

  1. 建立页表:把物理内存映射到内核虚拟地址空间
  2. 初始化伙伴系统:管理物理页面的分配和回收
  3. 初始化slab分配器:管理小内存对象的分配
  4. 设置内存水线:控制内存回收的阈值

来看一段关键代码:

// mm/mm_init.c
void __init mm_init(void)
{
    // 初始化伙伴系统
    mem_init();
    
    // 初始化slab分配器
    kmem_cache_init();
    
    // 设置内存水线
    setup_per_zone_wmarks();
    
    // 初始化页回收机制
    page_alloc_init();
}

我曾经踩过一个坑:在某个低端Android设备上,系统启动后没多久就OOM了。排查后发现是slab分配器的缓存对象太多,占用了大量内存。后来调整了slab_max_size参数才解决。嗯,这种问题在内存小的设备上特别常见。

避坑指南:我曾经在调试内存泄漏时发现,很多开发者不知道/proc/meminfo里的Slab字段代表什么。其实它包含了slab分配器占用的内存,如果这个值持续增长,说明有内核对象泄漏了。

3.4 设备驱动初始化

设备驱动初始化是内核启动中最耗时的一环。你想想看,一台Android手机里有几十个硬件设备,每个都要初始化一遍。

内核使用device_initcall宏来标记驱动的初始化函数。这些函数按照优先级分成几个等级:

优先级 宏定义 典型设备
最高 pure_initcall 架构相关、核心总线
core_initcall 中断控制器、时钟
postcore_initcall DMA、定时器
arch_initcall PCI、平台设备
最低 device_initcall 普通外设驱动

驱动初始化时,内核会遍历所有注册的驱动,调用它们的probe函数。如果驱动和设备匹配成功,就完成初始化。这个过程在Android启动时特别明显——你会看到内核日志里一堆[drm][i2c]之类的信息刷屏。

我个人习惯在调试驱动问题时,先看dmesg输出。如果某个驱动的probe函数没被调用,多半是设备树(Device Tree)的配置有问题。

3.5 根文件系统的挂载

所有核心子系统初始化完成后,内核会尝试挂载根文件系统。这一步由prepare_namespace()函数完成。

挂载流程:

  1. 解析内核参数中的root=选项,确定根设备
  2. 调用文件系统的mount函数
  3. 如果失败,尝试使用initramfs
  4. 挂载成功后,切换到根文件系统

在Android设备上,根文件系统通常是ramdisk或者system.img。我记得有一次调试启动失败,发现是fstab文件里指定的分区路径不对,导致根文件系统挂载不上。嗯,这种问题排查起来其实不难,看内核日志里的VFS: Cannot open root device错误就行。

注意事项:根文件系统挂载失败是内核启动中最常见的错误之一。如果你在调试时遇到Kernel panic - not syncing: VFS: Unable to mount root fs,先检查内核参数里的root=是否正确,再确认文件系统驱动是否编译进了内核。

3.6 启动用户空间

根文件系统挂载成功后,内核会执行run_init_process(),启动用户空间的第一个进程——init。在Android系统里,这个进程是/init(也就是我们常说的init进程)。

从这一刻起,内核的初始化工作基本完成,控制权交给了用户空间。但内核并没有闲着——它继续提供系统调用、管理内存、调度进程,默默支撑着整个Android系统的运行。

说实话,每次看到内核启动完成的那一刻,我都觉得挺有成就感的。从CPU上电到用户空间启动,中间经历了成千上万行代码的执行,每一个环节都不能出错。这就是操作系统的魅力所在。

下一章,我们会深入分析init进程的启动过程,看看Android系统是如何从内核态切换到用户态的。到时候再聊。