1. start_kernel:Linux内核启动的起点

当按下电源键的那一刻,计算机开始了一段奇妙的旅程。而在这段旅程中,start_kernel函数扮演着至关重要的角色——它是Linux内核初始化的起点,也是内核从汇编代码跳转到C语言世界的转折点。作为内核开发者,理解start_kernel的运作机制,就像掌握了打开内核大门的钥匙。

start_kernel定义在init/main.c中,这个函数在内核解压完成后被调用,负责初始化内核的各个子系统。从内存管理到进程调度,从设备驱动到文件系统,几乎所有核心功能都在这里开始它们的生命周期。有趣的是,虽然名为"start",但它并不是真正的起点——在它之前还有一段汇编代码负责最基础的硬件初始化。

2. start_kernel的核心职责解析

2.1 硬件抽象层初始化

start_kernel首先会调用setup_arch(),这个函数是硬件相关的初始化入口。它会根据不同的CPU架构(x86、ARM等)执行特定的设置工作:

void __init setup_arch(char **cmdline_p)
{
    /* 架构特定的早期初始化 */
    early_cpu_init();
    early_mem_init();
    
    /* 解析内核命令行参数 */
    parse_early_param();
    
    /* 内存布局设置 */
    setup_memory();
}

在ARM架构中,这个函数会初始化处理器特性、设置内存分页、建立设备树等。而在x86架构中,则会处理ACPI表、设置IO端口等。这种设计体现了Linux内核"抽象与具体分离"的哲学——公共逻辑在start_kernel中处理,而硬件细节则交给各架构自己实现。

注意:不同架构的setup_arch()实现差异很大,阅读代码时需要结合具体架构的文档。

2.2 关键子系统初始化顺序

start_kernel中的初始化顺序经过精心设计,因为某些子系统依赖于其他子系统的先期初始化。典型的初始化流程如下:

  1. 打印系统信息 :printk()的早期初始化,使得后续可以输出调试信息
  2. 内存管理初始化 :page_alloc_init(), setup_per_cpu_pageset()
  3. 调度器初始化 :sched_init()
  4. 中断系统初始化 :init_IRQ()
  5. 定时器初始化 :time_init()
  6. 控制台初始化 :console_init()
  7. 进程管理初始化 :fork_init()
  8. 文件系统初始化 :vfs_caches_init()

这个顺序不是随意安排的。例如,内存管理必须在几乎所有其他子系统之前初始化,因为后续操作都需要分配内存。而控制台初始化相对靠后,是因为它依赖于前面的内存和中断系统。

3. start_kernel的详细执行流程

3.1 早期初始化阶段

start_kernel的开头部分处理最基本的系统设置:

asmlinkage __visible void __init start_kernel(void)
{
    char *command_line;
    
    /* 设置栈保护机制 */
    boot_init_stack_canary();
    
    /* 初始化调试子系统 */
    debug_objects_early_init();
    
    /* 设置每CPU变量区域 */
    setup_per_cpu_areas();
    
    /* 初始化NUMA策略 */
    numa_policy_init();
}

这个阶段有几个关键点值得注意:

  • 栈保护(canary) :用于检测栈溢出攻击,在内核早期就设置好安全机制
  • 每CPU变量 :为SMP系统中每个CPU核心分配独立的变量存储空间
  • NUMA初始化 :为非统一内存访问架构设置内存分配策略

3.2 内存管理初始化

内存是内核最重要的资源之一,其初始化分为多个阶段:

void __init mm_init(void)
{
    /* 初始化页面分配器 */
    page_alloc_init();
    
    /* 设置内存热插拔支持 */
    mem_init();
    
    /* 初始化slab分配器 */
    kmem_cache_init();
    
    /* 设置虚拟内存区域 */
    vmalloc_init();
}

内存初始化的一个关键挑战是"鸡生蛋蛋生鸡"问题:在内存管理系统完全启动前,如何分配内存?内核通过以下方式解决:

  1. 使用早期的bootmem分配器
  2. 逐步过渡到伙伴系统(buddy system)
  3. 最后初始化更高级的slab分配器

3.3 进程管理和调度初始化

进程是Linux的基本执行单元,其初始化过程包括:

void __init fork_init(void)
{
    /* 初始化task_struct的slab缓存 */
    task_struct_cachep = kmem_cache_create("task_struct",
            sizeof(struct task_struct), ARCH_MIN_TASKALIGN,
            SLAB_PANIC|SLAB_ACCOUNT, NULL);
    
    /* 设置init进程的栈大小 */
    arch_task_cache_init();
}

void __init sched_init(void)
{
    /* 初始化运行队列 */
    for_each_possible_cpu(i) {
        struct rq *rq = cpu_rq(i);
        
        rq->nr_running = 0;
        rq->calc_load_active = 0;
        /* ... */
    }
    
    /* 设置调度类 */
    sched_clock_init();
    init_sched_fair_class();
}

这里有几个有意思的实现细节:

  • 进程描述符缓存 :使用slab分配器高效分配task_struct
  • 运行队列(runqueue) :每个CPU有自己的运行队列,减少锁竞争
  • 调度类 :完全公平调度(CFS)、实时调度等类的注册

4. start_kernel中的关键调试技术

4.1 早期打印输出

在内核完全初始化前,常规的printk可能无法工作。内核提供了几种早期调试手段:

/* 使用early_printk输出到串口 */
early_printk("Early debug message\n");

/* 使用printk的早期版本 */
early_printk_setup(console_port);

这些早期调试手段对于诊断启动问题至关重要。例如,如果内核在start_kernel的早期就崩溃,常规的日志系统可能还不可用。

4.2 内核参数解析

start_kernel会解析从bootloader传递来的命令行参数:

/* 在setup_arch中 */
parse_early_param();

/* 在start_kernel主体中 */
parse_args("Booting kernel", static_command_line,
           __start___param, __stop___param - __start___param,
           -1, -1, NULL, &unknown_bootoption);

这些参数可以控制内核的各种行为,例如:

  • init= :指定第一个用户空间进程
  • root= :指定根文件系统设备
  • console= :指定控制台设备
  • loglevel= :设置内核日志级别

4.3 内核符号表导出

为了支持模块加载和调试,内核需要导出符号表:

void __init init_export_table(void)
{
    /* 处理EXPORT_SYMBOL宏定义的符号 */
    kernel_symbols_setup();
    
    /* 为kallsyms准备数据 */
    kallsyms_init();
}

这个机制使得:

  • 内核模块可以引用内核符号
  • 调试工具可以解析内核地址对应的函数名
  • oops消息可以显示有意义的调用栈

5. 从start_kernel到用户空间

start_kernel的最终目标是启动第一个用户空间进程(通常是init)。这个过程分为几个阶段:

5.1 内核线程创建

在rest_init()函数中,内核会创建两个关键线程:

static noinline void __init_refok rest_init(void)
{
    /* 创建内核init线程 */
    pid = kernel_thread(kernel_init, NULL, CLONE_FS);
    
    /* 创建kthreadd线程 */
    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
    
    /* 当前线程变为idle线程 */
    cpu_startup_entry(CPUHP_ONLINE);
}
  • kernel_init :负责初始化用户空间
  • kthreadd :内核守护线程,负责创建其他内核线程
  • idle线程 :当没有其他任务时运行的节能线程

5.2 用户空间初始化

kernel_init线程最终会尝试执行用户空间初始化程序:

static int __ref kernel_init(void *unused)
{
    /* 等待驱动程序初始化完成 */
    wait_for_completion(&kthreadd_done);
    
    /* 尝试执行用户指定的init程序 */
    if (execute_command) {
        ret = run_init_process(execute_command);
    }
    
    /* 尝试默认的init路径 */
    if (!ret)
        ret = run_init_process("/sbin/init");
    /* ... */
}

内核会依次尝试以下init程序:

  1. 命令行指定的init程序(通过init=参数)
  2. /sbin/init
  3. /etc/init
  4. /bin/init
  5. /bin/sh

如果所有这些尝试都失败,内核会panic。

6. start_kernel的定制与扩展

6.1 添加自定义初始化代码

开发者有时需要在start_kernel中添加自己的初始化代码。推荐的做法是:

  1. 定义初始化函数:
static void __init my_custom_init(void)
{
    /* 自定义初始化逻辑 */
}
  1. 在start_kernel的适当位置调用:
asmlinkage __visible void __init start_kernel(void)
{
    /* ... */
    my_custom_init();
    /* ... */
}

注意:添加代码时要考虑初始化顺序的依赖关系,通常放在相关子系统初始化之后。

6.2 内核启动时间优化

对于嵌入式系统等对启动时间敏感的场景,可以优化start_kernel:

  1. 并行初始化 :将没有依赖关系的子系统初始化并行化
  2. 延迟初始化 :将非关键路径的初始化推迟到用户空间
  3. 裁剪不需要的功能 :通过内核配置移除不需要的子系统

例如,可以修改init/main.c中的do_basic_setup():

static void __init do_basic_setup(void)
{
    /* 并行初始化驱动程序 */
    async_synchronize_full();
    
    /* 延迟文件系统初始化 */
    if (!initcall_debug)
        do_initcalls();
}

6.3 启动时内存管理技巧

在内存受限的系统上,可以调整start_kernel中的内存相关参数:

  1. 减少初始内存分配 :
void __init mem_init(void)
{
    /* 减小初始内存保留区域 */
    memblock_reserve(0, SZ_1M);
}
  1. 优化slab分配器 :
void __init kmem_cache_init(void)
{
    /* 减小slab缓存大小 */
    kmem_cache_size = min(kmem_cache_size, (size_t)SZ_32K);
}
  1. 调整页面分配器参数 :
void __init page_alloc_init(void)
{
    /* 减少伙伴系统的最大阶数 */
    MAX_ORDER = min(MAX_ORDER, 8);
}

7. 常见问题与调试技巧

7.1 启动时崩溃诊断

如果内核在start_kernel阶段崩溃,可以采取以下诊断步骤:

  1. 启用早期控制台输出 :

    • 在内核配置中启用CONFIG_EARLY_PRINTK
    • 添加命令行参数earlyprintk=serial,ttyS0,115200
  2. 分析Oops消息 :

    • 注意PC(程序计数器)值和调用栈
    • 使用addr2line工具将地址转换为代码位置
  3. 使用JTAG调试器 :

    • 对于嵌入式系统,硬件调试器可以捕获早期崩溃

7.2 初始化顺序问题

当添加新的初始化代码时,可能会遇到顺序依赖问题。解决方法包括:

  1. 使用initcall机制 :
/* 在子系统初始化后调用 */
late_initcall(my_init_func);
  1. 显式依赖声明 :
void __init my_init_func(void)
{
    if (!slab_is_available())
        panic("需要slab分配器");
}
  1. 使用完成量(completion) :
static DECLARE_COMPLETION(my_subsystem_ready);

/* 在依赖的子系统初始化完成后 */
complete(&my_subsystem_ready);

/* 在需要等待的地方 */
wait_for_completion(&my_subsystem_ready);

7.3 内存相关问题的调试

start_kernel阶段的内存问题可以通过以下方式诊断:

  1. 启用内存调试选项 :

    • CONFIG_DEBUG_PAGEALLOC
    • CONFIG_DEBUG_SLAB
    • CONFIG_DEBUG_VM
  2. 使用memblock调试 :

/* 在内核命令行添加 */
memblock=debug
  1. 检查内存布局 :
/* 在setup_arch中打印内存信息 */
pr_info("Memory: %luMB available\n", memblock_phys_mem_size() >> 20);

8. 性能分析与优化

8.1 启动时间测量

可以使用多种工具测量start_kernel的执行时间:

  1. printk时间戳 :
/* 在内核命令行添加 */
printk.time=1
  1. 使用ftrace :
# 在内核命令行添加
ftrace=function
ftrace_filter=start_kernel*
  1. 硬件性能计数器 :
    • 使用PMU(Performance Monitoring Unit)记录CPU周期

8.2 热点函数分析

通过性能分析可以找出start_kernel中的热点:

  1. 使用gprof :

    • 编译内核时添加-pg选项
    • 分析生成的gmon.out文件
  2. 使用perf :

perf record -g -e cycles:u -- ./vmlinux
perf report
  1. 使用systemtap :
probe kernel.function("*@init/main.c").call {
    println(pp(), " -> ", probefunc())
}

8.3 优化案例:减少TLB失效

在x86架构上,可以通过以下方式优化start_kernel的TLB使用:

  1. 使用大页 :
/* 在setup_arch中 */
set_memory_hugetlb(__pa_symbol(&_text), PFN_UP(_end - _text));
  1. 优化页表遍历 :
/* 启用PCID(Process Context ID) */
cr4_set_bits(X86_CR4_PCIDE);
  1. 预取页表项 :
/* 在访问内存前预取 */
prefetchw(&init_mm.pgd[pgd_index(address)]);

9. 跨架构实现差异

9.1 ARM架构的特殊处理

在ARM架构中,start_kernel有一些特殊处理:

  1. 设备树支持 :
void __init setup_arch(char **cmdline_p)
{
    /* 早期设备树扫描 */
    setup_machine_fdt(__atags_pointer);
}
  1. CPU特性检测 :
/* 检测NEON、VFP等扩展 */
elf_hwcap = get_cpu_features();
  1. 多核启动 :
/* 启动从核 */
smp_init_cpus();

9.2 x86架构的特性

x86架构的start_kernel也有其特点:

  1. ACPI初始化 :
void __init setup_arch(char **cmdline_p)
{
    /* 解析ACPI表 */
    acpi_boot_table_init();
}
  1. 微码更新 :
/* 应用CPU微码更新 */
microcode_init();
  1. SMP准备 :
/* 准备SMP启动 */
smp_prepare_boot_cpu();

9.3 RISC-V架构的新变化

RISC-V作为新兴架构,其start_kernel实现也有特色:

  1. SBI调用 :
/* 通过SBI接口与监控程序通信 */
sbi_console_putchar(ch);
  1. 扩展检测 :
/* 检测标准扩展 */
riscv_isa_init();
  1. 页表初始化 :
/* RISC-V特有的页表设置 */
setup_vm();

10. 安全加固考虑

10.1 栈保护机制

start_kernel中实现了多种栈保护技术:

  1. 栈金丝雀(stack canary) :
/* 在start_kernel开头 */
boot_init_stack_canary();
  1. 内核栈溢出检测 :
/* 配置内核栈大小 */
THREAD_SIZE_ORDER = 2;
  1. 特权级分离 :
/* x86上的SMAP/SMEP保护 */
cr4_set_bits(X86_CR4_SMAP | X86_CR4_SMEP);

10.2 内存保护

内核启动时的内存保护措施包括:

  1. W^X保护 :
/* 设置文本段为只读 */
set_memory_ro((unsigned long)_text, (_etext - _text) >> PAGE_SHIFT);
  1. KASLR(内核地址空间布局随机化) :
/* 随机化内核地址 */
kernel_randomize_memory();
  1. 敏感数据保护 :
/* 保护init_task等关键数据结构 */
mark_rodata_ro();

10.3 启动时安全扩展

可以添加自定义安全检查:

  1. 启动时完整性检查 :
static void __init security_init(void)
{
    /* 验证内核完整性 */
    if (verify_kernel_code())
        panic("Kernel integrity check failed");
}
  1. 硬件信任链验证 :
/* 验证启动加载器签名 */
if (!verify_bootloader())
    panic("Bootloader verification failed");
  1. 敏感操作记录 :
/* 记录关键初始化操作 */
audit_init();

11. 嵌入式系统特殊考量

11.1 最小化内核尺寸

对于资源受限的嵌入式系统:

  1. 裁剪不需要的功能 :
# 内核配置选项
CONFIG_EMBEDDED=y
CONFIG_EXPERT=y
  1. 优化数据结构大小 :
/* 减小pid最大值 */
#define PID_MAX_DEFAULT 0x1000
  1. 静态分配关键结构 :
/* 静态分配init_task */
struct task_struct init_task = INIT_TASK(init_task);

11.2 快速启动技术

嵌入式系统常用的快速启动技术:

  1. XIP(Execute In Place) :
/* 配置XIP内核 */
CONFIG_XIP_KERNEL=y
  1. 休眠到内存 :
/* 支持快速休眠恢复 */
pm_set_ops(&early_suspend_ops);
  1. 延迟初始化 :
/* 将非关键初始化推迟 */
late_initcall_sync(non_critical_init);

11.3 实时性增强

对于实时嵌入式系统:

  1. 实时补丁应用 :
/* 应用PREEMPT_RT补丁 */
preempt_enable();
  1. 中断线程化 :
/* 将硬件中断转为内核线程 */
irq_setup_forced_threading();
  1. 优先级继承 :
/* 启用优先级继承互斥锁 */
mutex_init(&lock, &pi_mutex_ops);

12. 未来演进方向

12.1 并行初始化趋势

现代内核正在向更并行的初始化发展:

  1. 异步initcall :
/* 异步执行初始化函数 */
async_schedule(async_init_fn, NULL);
  1. 多核启动优化 :
/* 早期唤醒从核 */
smp_prepare_cpus();
  1. 依赖关系自动化 :
/* 自动解决initcall依赖 */
initcall_debug=1

12.2 安全增强方向

start_kernel的安全增强方向包括:

  1. 更细粒度的权限控制 :
/* 限制早期初始化权限 */
security_initcall_restrict();
  1. 启动时验证扩展 :
/* 验证内核模块签名 */
module_sig_enforce=1
  1. 内存安全语言引入 :
/* 使用Rust编写部分启动代码 */
rust_init();

12.3 硬件支持演进

随着新硬件出现,start_kernel需要适应:

  1. 异构计算支持 :
/* 初始化协处理器 */
coprocessor_init();
  1. 新型内存架构 :
/* 持久内存初始化 */
pmem_init();
  1. 量子计算准备 :
/* 量子随机数生成器初始化 */
qrandom_init();

理解start_kernel的实现和演进,不仅对内核开发者至关重要,对于系统级程序员和性能优化工程师也同样重要。这个函数就像一面镜子,反映了Linux内核的设计哲学和实现艺术——从硬件抽象到资源管理,从安全考虑到性能优化,几乎所有的核心思想都在这个启动过程中有所体现。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐