Linux内核启动:start_kernel函数深度解析
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中的初始化顺序经过精心设计,因为某些子系统依赖于其他子系统的先期初始化。典型的初始化流程如下:
- 打印系统信息 :printk()的早期初始化,使得后续可以输出调试信息
- 内存管理初始化 :page_alloc_init(), setup_per_cpu_pageset()
- 调度器初始化 :sched_init()
- 中断系统初始化 :init_IRQ()
- 定时器初始化 :time_init()
- 控制台初始化 :console_init()
- 进程管理初始化 :fork_init()
- 文件系统初始化 :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();
}
内存初始化的一个关键挑战是"鸡生蛋蛋生鸡"问题:在内存管理系统完全启动前,如何分配内存?内核通过以下方式解决:
- 使用早期的bootmem分配器
- 逐步过渡到伙伴系统(buddy system)
- 最后初始化更高级的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程序:
- 命令行指定的init程序(通过init=参数)
- /sbin/init
- /etc/init
- /bin/init
- /bin/sh
如果所有这些尝试都失败,内核会panic。
6. start_kernel的定制与扩展
6.1 添加自定义初始化代码
开发者有时需要在start_kernel中添加自己的初始化代码。推荐的做法是:
- 定义初始化函数:
static void __init my_custom_init(void)
{
/* 自定义初始化逻辑 */
}
- 在start_kernel的适当位置调用:
asmlinkage __visible void __init start_kernel(void)
{
/* ... */
my_custom_init();
/* ... */
}
注意:添加代码时要考虑初始化顺序的依赖关系,通常放在相关子系统初始化之后。
6.2 内核启动时间优化
对于嵌入式系统等对启动时间敏感的场景,可以优化start_kernel:
- 并行初始化 :将没有依赖关系的子系统初始化并行化
- 延迟初始化 :将非关键路径的初始化推迟到用户空间
- 裁剪不需要的功能 :通过内核配置移除不需要的子系统
例如,可以修改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中的内存相关参数:
- 减少初始内存分配 :
void __init mem_init(void)
{
/* 减小初始内存保留区域 */
memblock_reserve(0, SZ_1M);
}
- 优化slab分配器 :
void __init kmem_cache_init(void)
{
/* 减小slab缓存大小 */
kmem_cache_size = min(kmem_cache_size, (size_t)SZ_32K);
}
- 调整页面分配器参数 :
void __init page_alloc_init(void)
{
/* 减少伙伴系统的最大阶数 */
MAX_ORDER = min(MAX_ORDER, 8);
}
7. 常见问题与调试技巧
7.1 启动时崩溃诊断
如果内核在start_kernel阶段崩溃,可以采取以下诊断步骤:
-
启用早期控制台输出 :
- 在内核配置中启用CONFIG_EARLY_PRINTK
- 添加命令行参数earlyprintk=serial,ttyS0,115200
-
分析Oops消息 :
- 注意PC(程序计数器)值和调用栈
- 使用addr2line工具将地址转换为代码位置
-
使用JTAG调试器 :
- 对于嵌入式系统,硬件调试器可以捕获早期崩溃
7.2 初始化顺序问题
当添加新的初始化代码时,可能会遇到顺序依赖问题。解决方法包括:
- 使用initcall机制 :
/* 在子系统初始化后调用 */
late_initcall(my_init_func);
- 显式依赖声明 :
void __init my_init_func(void)
{
if (!slab_is_available())
panic("需要slab分配器");
}
- 使用完成量(completion) :
static DECLARE_COMPLETION(my_subsystem_ready);
/* 在依赖的子系统初始化完成后 */
complete(&my_subsystem_ready);
/* 在需要等待的地方 */
wait_for_completion(&my_subsystem_ready);
7.3 内存相关问题的调试
start_kernel阶段的内存问题可以通过以下方式诊断:
-
启用内存调试选项 :
- CONFIG_DEBUG_PAGEALLOC
- CONFIG_DEBUG_SLAB
- CONFIG_DEBUG_VM
-
使用memblock调试 :
/* 在内核命令行添加 */
memblock=debug
- 检查内存布局 :
/* 在setup_arch中打印内存信息 */
pr_info("Memory: %luMB available\n", memblock_phys_mem_size() >> 20);
8. 性能分析与优化
8.1 启动时间测量
可以使用多种工具测量start_kernel的执行时间:
- printk时间戳 :
/* 在内核命令行添加 */
printk.time=1
- 使用ftrace :
# 在内核命令行添加
ftrace=function
ftrace_filter=start_kernel*
-
硬件性能计数器
:
- 使用PMU(Performance Monitoring Unit)记录CPU周期
8.2 热点函数分析
通过性能分析可以找出start_kernel中的热点:
-
使用gprof :
- 编译内核时添加-pg选项
- 分析生成的gmon.out文件
-
使用perf :
perf record -g -e cycles:u -- ./vmlinux
perf report
- 使用systemtap :
probe kernel.function("*@init/main.c").call {
println(pp(), " -> ", probefunc())
}
8.3 优化案例:减少TLB失效
在x86架构上,可以通过以下方式优化start_kernel的TLB使用:
- 使用大页 :
/* 在setup_arch中 */
set_memory_hugetlb(__pa_symbol(&_text), PFN_UP(_end - _text));
- 优化页表遍历 :
/* 启用PCID(Process Context ID) */
cr4_set_bits(X86_CR4_PCIDE);
- 预取页表项 :
/* 在访问内存前预取 */
prefetchw(&init_mm.pgd[pgd_index(address)]);
9. 跨架构实现差异
9.1 ARM架构的特殊处理
在ARM架构中,start_kernel有一些特殊处理:
- 设备树支持 :
void __init setup_arch(char **cmdline_p)
{
/* 早期设备树扫描 */
setup_machine_fdt(__atags_pointer);
}
- CPU特性检测 :
/* 检测NEON、VFP等扩展 */
elf_hwcap = get_cpu_features();
- 多核启动 :
/* 启动从核 */
smp_init_cpus();
9.2 x86架构的特性
x86架构的start_kernel也有其特点:
- ACPI初始化 :
void __init setup_arch(char **cmdline_p)
{
/* 解析ACPI表 */
acpi_boot_table_init();
}
- 微码更新 :
/* 应用CPU微码更新 */
microcode_init();
- SMP准备 :
/* 准备SMP启动 */
smp_prepare_boot_cpu();
9.3 RISC-V架构的新变化
RISC-V作为新兴架构,其start_kernel实现也有特色:
- SBI调用 :
/* 通过SBI接口与监控程序通信 */
sbi_console_putchar(ch);
- 扩展检测 :
/* 检测标准扩展 */
riscv_isa_init();
- 页表初始化 :
/* RISC-V特有的页表设置 */
setup_vm();
10. 安全加固考虑
10.1 栈保护机制
start_kernel中实现了多种栈保护技术:
- 栈金丝雀(stack canary) :
/* 在start_kernel开头 */
boot_init_stack_canary();
- 内核栈溢出检测 :
/* 配置内核栈大小 */
THREAD_SIZE_ORDER = 2;
- 特权级分离 :
/* x86上的SMAP/SMEP保护 */
cr4_set_bits(X86_CR4_SMAP | X86_CR4_SMEP);
10.2 内存保护
内核启动时的内存保护措施包括:
- W^X保护 :
/* 设置文本段为只读 */
set_memory_ro((unsigned long)_text, (_etext - _text) >> PAGE_SHIFT);
- KASLR(内核地址空间布局随机化) :
/* 随机化内核地址 */
kernel_randomize_memory();
- 敏感数据保护 :
/* 保护init_task等关键数据结构 */
mark_rodata_ro();
10.3 启动时安全扩展
可以添加自定义安全检查:
- 启动时完整性检查 :
static void __init security_init(void)
{
/* 验证内核完整性 */
if (verify_kernel_code())
panic("Kernel integrity check failed");
}
- 硬件信任链验证 :
/* 验证启动加载器签名 */
if (!verify_bootloader())
panic("Bootloader verification failed");
- 敏感操作记录 :
/* 记录关键初始化操作 */
audit_init();
11. 嵌入式系统特殊考量
11.1 最小化内核尺寸
对于资源受限的嵌入式系统:
- 裁剪不需要的功能 :
# 内核配置选项
CONFIG_EMBEDDED=y
CONFIG_EXPERT=y
- 优化数据结构大小 :
/* 减小pid最大值 */
#define PID_MAX_DEFAULT 0x1000
- 静态分配关键结构 :
/* 静态分配init_task */
struct task_struct init_task = INIT_TASK(init_task);
11.2 快速启动技术
嵌入式系统常用的快速启动技术:
- XIP(Execute In Place) :
/* 配置XIP内核 */
CONFIG_XIP_KERNEL=y
- 休眠到内存 :
/* 支持快速休眠恢复 */
pm_set_ops(&early_suspend_ops);
- 延迟初始化 :
/* 将非关键初始化推迟 */
late_initcall_sync(non_critical_init);
11.3 实时性增强
对于实时嵌入式系统:
- 实时补丁应用 :
/* 应用PREEMPT_RT补丁 */
preempt_enable();
- 中断线程化 :
/* 将硬件中断转为内核线程 */
irq_setup_forced_threading();
- 优先级继承 :
/* 启用优先级继承互斥锁 */
mutex_init(&lock, &pi_mutex_ops);
12. 未来演进方向
12.1 并行初始化趋势
现代内核正在向更并行的初始化发展:
- 异步initcall :
/* 异步执行初始化函数 */
async_schedule(async_init_fn, NULL);
- 多核启动优化 :
/* 早期唤醒从核 */
smp_prepare_cpus();
- 依赖关系自动化 :
/* 自动解决initcall依赖 */
initcall_debug=1
12.2 安全增强方向
start_kernel的安全增强方向包括:
- 更细粒度的权限控制 :
/* 限制早期初始化权限 */
security_initcall_restrict();
- 启动时验证扩展 :
/* 验证内核模块签名 */
module_sig_enforce=1
- 内存安全语言引入 :
/* 使用Rust编写部分启动代码 */
rust_init();
12.3 硬件支持演进
随着新硬件出现,start_kernel需要适应:
- 异构计算支持 :
/* 初始化协处理器 */
coprocessor_init();
- 新型内存架构 :
/* 持久内存初始化 */
pmem_init();
- 量子计算准备 :
/* 量子随机数生成器初始化 */
qrandom_init();
理解start_kernel的实现和演进,不仅对内核开发者至关重要,对于系统级程序员和性能优化工程师也同样重要。这个函数就像一面镜子,反映了Linux内核的设计哲学和实现艺术——从硬件抽象到资源管理,从安全考虑到性能优化,几乎所有的核心思想都在这个启动过程中有所体现。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐



所有评论(0)