4. 启动流程第三阶段:start_kernel函数解析、早期控制台初始化与架构特定设置

当解压缩后的内核映像被引导加载程序(如GRUB或U-Boot)跳转到入口点后,执行流会迅速进入与体系结构无关的C语言世界。这一阶段的起点是 start_kernel 函数,它位于 init/main.c 中,是整个内核初始化的“总调度器”。本章将深入解析该函数的核心执行路径,重点分析早期控制台的初始化机制以及架构特定设置的衔接方式。

关键认知: start_kernel 并非内核启动的“第一行代码”,但它标志着从汇编语言到C语言、从架构相关到架构无关的转折点。理解它的执行顺序,是分析启动性能瓶颈的基础。

4.1 start_kernel 函数执行全景

start_kernel 函数负责初始化内核的核心子系统,其执行顺序严格遵循依赖关系。以下SVG图展示了其主要调用链和阶段划分。

start_kernel 函数主要执行流程 start_kernel 函数主要执行流程 setup_arch() trap_init() mm_init() early_console_init() sched_init() preempt_disable() vfs_caches_init() signals_init() workqueue_init() do_initcalls() console_init() rest_init() cpu_startup_entry() — 进入 idle 循环 说明:箭头表示执行顺序。早期控制台在调度器初始化之前启用,用于输出早期调试信息。

从SVG图中可以看出,start_kernel 的执行可以划分为四个主要阶段:

  1. 早期架构与陷阱设置:包括 setup_arch()、trap_init() 和 mm_init(),负责解析内核命令行、初始化页表、设置中断向量表。
  2. 控制台与调度器:early_console_init() 启用早期打印能力,sched_init() 初始化进程调度器。
  3. 核心子系统:VFS、信号、工作队列等通用子系统的初始化。
  4. 驱动与最终启动:通过 do_initcalls() 执行各级驱动初始化,最终由 rest_init() 创建 init 进程并进入空闲循环。

4.2 早期控制台初始化机制

早期控制台(Early Console)是内核在标准控制台驱动完全初始化之前,用于输出日志和调试信息的机制。它在启动性能分析中至关重要,因为启动早期的许多关键事件(如内存初始化、调度器设置)都依赖它来记录时间戳。

4.2.1 早期控制台与标准控制台的对比

特性 早期控制台 (early console) 标准控制台 (console)
初始化时机 start_kernel 早期,sched_init() 之前 console_init() 调用,通常在 do_initcalls() 阶段
驱动依赖 通常使用架构特定的简单驱动(如 8250 UART 的轮询模式) 依赖完整的设备驱动模型、中断和 DMA
输出能力 仅支持同步轮询输出,无缓冲,速度较慢 支持缓冲、中断驱动、多路复用
性能影响 低(但频繁输出会显著拖慢启动) 中等(通常只在启动后期启用)
配置方式 内核命令行参数 earlycon 内核命令行参数 console
性能陷阱: 在调试阶段,如果 earlycon 被设置为非常慢的串口(如 115200 bps),并且内核启用了大量的 pr_early() 或 early_printk() 调用,启动时间可能会增加数百毫秒甚至数秒。在生产环境的性能分析中,建议禁用早期控制台或将其输出重定向到快速虚拟设备(如 QEMU 的 virtio-console)。

4.2.2 早期控制台的初始化流程

早期控制台的初始化通常通过 early_console_init() 函数完成,其内部逻辑如下:

// 简化后的 early_console_init 逻辑
void __init early_console_init(void)
{
    struct earlycon_device *dev;
    const char *p;

    // 1. 解析内核命令行中的 "earlycon" 参数
    p = strstr(boot_command_line, "earlycon");
    if (!p)
        return; // 未指定 earlycon,跳过

    // 2. 根据参数匹配注册的 earlycon 驱动
    dev = earlycon_match(p);
    if (!dev) {
        early_printk("No matching earlycon driver found\n");
        return;
    }

    // 3. 调用驱动的 setup 函数,初始化硬件
    if (dev->setup(dev)) {
        early_printk("Failed to setup earlycon\n");
        return;
    }

    // 4. 注册 early_console 到内核打印系统
    register_early_console(dev);
    early_printk("Early console initialized on %s\n", dev->name);
}

在 QEMU 环境中,常用的早期控制台配置包括:

  • 串口 (serial):earlycon=uart8250,io,0x3f8 或 earlycon=uart,mmio,0x3f8
  • virtio 控制台:earlycon=virtio,mmio,0x10000000(需要 QEMU 设备支持)
  • QEMU 调试控制台:earlycon=qemu,io,0x3f8(QEMU 特有的快速输出通道)

4.3 架构特定设置:setup_arch 深度解析

setup_arch() 是 start_kernel 中第一个被调用的架构相关函数。以 x86 架构为例,它负责以下关键任务:

setup_arch (x86) 核心任务分解 setup_arch (x86) 核心任务分解 setup_arch() e820 内存映射解析 max_pfn / max_low_pfn 计算 parse_early_param() ACPI 表解析 init_mm 初始化 内核页表 / 保护设置 返回 start_kernel → mm_init() SMP 引导 (AP 处理器启动) 说明:x86 架构下 setup_arch 还负责解析 MP 表、初始化 NMI 看门狗等。ARM64 架构则侧重于设备树解析和 PCIe 域初始化。

4.3.1 关键数据结构:boot_params 与 e820 表

在 x86 架构中,引导加载程序通过 boot_params 结构体将硬件信息传递给内核。该结构体包含:

  • e820_map:物理内存布局表,描述哪些区域是可用 RAM、保留区域或 ACPI 数据。
  • screen_info:显示模式信息,用于早期 framebuffer 控制台。
  • hdr:引导协议头部,包含内核入口点、加载地址等。

setup_arch() 会解析 e820 表,并调用 sanitize_e820_map() 进行校验和合并,最终计算出 max_pfn(最大物理页框号)和 max_low_pfn(低端内存最大页框号),这些值将直接影响后续的内存管理初始化。

4.3.2 架构特定设置的性能影响

架构 关键耗时操作 QEMU 调优建议
x86 ACPI 表解析、SMP 启动 AP 处理器、MTRR 设置 使用 -machine q35,accel=kvm 加速 ACPI;限制 CPU 数量以减少 AP 启动开销
ARM64 设备树 (DTB) 解析、PCIe 域枚举、GIC 初始化 使用预编译的 DTB 文件(-dtb 参数);避免不必要的 PCIe 设备
RISC-V 设备树解析、CLINT/PLIC 初始化 使用 -machine virt 并精简设备树节点
性能分析切入点: 在 setup_arch() 中,ACPI 表解析和 SMP AP 处理器启动是 x86 架构下最常见的启动延迟来源。可以通过内核启动参数 acpi=off 或 maxcpus=1 来快速验证这些阶段对启动时间的影响。在 QEMU 中,使用 -smp cores=1,threads=1 可以强制单核启动,从而消除 AP 启动开销。

4.4 从 start_kernel 到 rest_init 的过渡

当 start_kernel 完成所有核心子系统的初始化后,它会调用 rest_init()。这个函数标志着内核从“单线程初始化”阶段进入“多进程并发”阶段:

// rest_init 的核心逻辑
static noinline void __init rest_init(void)
{
    struct task_struct *tsk;

    // 1. 创建内核线程 kernel_init (PID=1)
    tsk = kernel_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND);
    if (IS_ERR(tsk))
        panic("Failed to create init process");

    // 2. 创建内核线程 kthreadd (PID=2)
    tsk = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
    if (IS_ERR(tsk))
        panic("Failed to create kthreadd");

    // 3. 调度器准备就绪,允许抢占
    preempt_enable_no_resched();

    // 4. 调用 cpu_startup_entry() 进入 idle 循环
    cpu_startup_entry(CPUHP_ONLINE);
}

此时,kernel_init 线程将负责执行剩余的驱动初始化(通过 do_initcalls())、挂载根文件系统,并最终执行用户空间的 /sbin/init 程序。而 cpu_startup_entry() 则使当前 CPU 进入空闲循环,等待调度器分配任务。

启动性能关键指标: 从 start_kernel 入口到 rest_init 调用之间的时间差,通常被称为“内核核心初始化时间”。在 QEMU 中,通过 -trace events=kvm_exit 或 ftrace 可以精确测量这一阶段的耗时。对于性能敏感的虚拟化场景,这一时间应控制在 50ms 以内。

4.5 本章小结

本章深入剖析了 start_kernel 函数的执行流程,重点强调了早期控制台初始化与架构特定设置 setup_arch() 的作用。理解这些阶段对于定位启动性能瓶颈至关重要:

  • 早期控制台 是启动早期唯一的输出通道,但其轮询模式可能成为性能瓶颈,在 QEMU 中应优先使用快速虚拟设备。
  • setup_arch() 的执行效率高度依赖架构和硬件配置,ACPI 解析和 SMP 启动是常见的延迟来源。
  • rest_init() 标志着初始化阶段的结束和用户空间启动的开始,是测量内核核心初始化时间的自然分界点。

在下一阶段,我们将进入驱动初始化与根文件系统挂载的详细分析,进一步探索用户空间启动前的最后准备步骤。