第三章 Linux内核启动:内核初始化流程
好,咱们继续往下走。上一章我们聊了引导加载程序怎么把内核请进内存,现在内核终于要开始自己干活了。说实话,内核初始化这部分,我当年啃源码的时候啃得头皮发麻——代码量太大了。但后来我发现,抓住几个核心子系统的启动顺序,整个脉络就清晰了。
3.1 从start_kernel说起
内核启动的入口函数是start_kernel,在init/main.c里。这函数就像交响乐的指挥,把所有子系统按顺序叫醒。我个人习惯把它的工作分成三个阶段:
- 早期初始化:架构相关、内存管理基础、中断
- 核心子系统初始化:进程调度、内存管理、设备驱动
- 用户空间启动:挂载根文件系统、启动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_init和kthreadd。前者负责启动用户空间的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分配器。
流程大致是这样的:
- 建立页表:把物理内存映射到内核虚拟地址空间
- 初始化伙伴系统:管理物理页面的分配和回收
- 初始化slab分配器:管理小内存对象的分配
- 设置内存水线:控制内存回收的阈值
来看一段关键代码:
// 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()函数完成。
挂载流程:
- 解析内核参数中的
root=选项,确定根设备 - 调用文件系统的mount函数
- 如果失败,尝试使用initramfs
- 挂载成功后,切换到根文件系统
在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系统是如何从内核态切换到用户态的。到时候再聊。