4. 启动CPU(BSP)初始化:start_kernel函数、trap_init与mm_init、调度器初始化
当引导加载程序将控制权交给内核并完成早期架构设置后,执行流进入体系结构无关的C语言入口——start_kernel。这是BSP(Boot Strap Processor)初始化的核心阶段,负责搭建操作系统的基础设施:中断向量表、内存管理子系统、进程调度器以及SMP启动的准备工作。
4.1 start_kernel:内核初始化的总控枢纽
start_kernel 位于 init/main.c,是内核启动的“总导演”。它按严格的依赖顺序调用各个子系统的初始化函数,确保每个模块在其依赖就绪后才被激活。BSP在此阶段完成单核环境下的全部基础设置,为后续AP(Application Processor)的唤醒铺平道路。
关键特性: start_kernel 执行期间系统仍处于单处理器模式,中断处于关闭状态(local_irq_disabled),所有操作都是顺序、原子的。
| 初始化阶段 | 核心函数 | 职责描述 | SMP相关性 |
|---|---|---|---|
| 早期跟踪 | trace_init | 初始化内核跟踪基础设施 | 低 |
| 中断向量 | trap_init | 设置IDT,注册异常/中断处理门 | 高(AP需共享IDT) |
| 内存管理 | mm_init | 建立页表、内存节点、伙伴系统 | 高(NUMA感知) |
| 调度器 | sched_init | 初始化核心调度队列与调度类 | 高(负载均衡基础) |
| 中断控制器 | init_IRQ | 初始化本地APIC和I/O APIC | 极高(IPI通信) |
| 时间子系统 | time_init | 设置时钟源与时钟事件设备 | 高(tick广播) |
| 控制台 | console_init | 激活早期串口/VGA输出 | 低 |
启动顺序原则: 先中断后内存,先内存后调度,先调度后多核。任何违反依赖顺序的初始化都会导致系统崩溃。
4.2 trap_init:中断向量表的建立
trap_init 负责初始化中断描述符表(IDT),为所有异常(如缺页、除零)和外部中断注册处理门。在x86架构上,它调用 idt_setup_traps 填充预定义的异常向量,并设置系统调用门(entry_INT80_32 或 entry_SYSCALL_64)。
架构差异: ARM64 使用异常向量表(Exception Vector Table),通过
__vectors_start 和 __vectors_end 定义;RISC-V 则使用 stvec 寄存器指向 trap 处理入口。
// arch/x86/kernel/traps.c (简化)
void __init trap_init(void)
{
/* 设置早期 IDT,仅包含必要的异常门 */
idt_setup_traps();
/* 设置系统调用门 */
set_system_intr_gate(IA32_SYSCALL_VECTOR, &entry_INT80_32);
/* 设置调试寄存器与 IST(Interrupt Stack Table) */
ist_alloc();
cpu_init_exception_handling();
/* 设置浮点/MMX/SSE 异常 */
math_state_restore_init();
}
| 向量范围 | 类型 | 处理函数 | SMP 注意事项 |
|---|---|---|---|
| 0 – 19 | CPU 异常(Fault/Trap/Abort) | do_divide_error, do_page_fault 等 | 所有 CPU 共享同一 IDT |
| 32 – 255 | 外部中断(IRQ) | irq_entries_start 跳转表 | 每个 CPU 有独立 TSS,但 IDT 全局 |
| 0x80 | 系统调用门 | entry_INT80_32 / entry_SYSCALL_64 | 所有 CPU 通过同一入口处理 |
关键约束: IDT 必须在所有 CPU 启动前完全初始化。AP 启动时直接加载 BSP 已建立的 IDT 基地址(通过
lidt 指令),不会重新创建。因此,trap_init 中的任何遗漏都会导致 AP 上发生异常时系统崩溃。
4.3 mm_init:内存管理子系统的奠基
mm_init 是内存管理初始化的核心入口,它依次建立页表、引导内存分配器、伙伴系统和 slab 分配器。在 SMP 系统中,mm_init 还负责初始化 NUMA 节点信息和每 CPU 页帧缓存。
// init/main.c (简化)
static void __init mm_init(void)
{
/* 建立内核页表,开启分页 */
paging_init();
/* 初始化引导内存分配器 (memblock) */
memblock_init();
/* 初始化伙伴系统 */
free_area_init(max_zone_pfns);
/* 初始化 slab 分配器 */
kmem_cache_init();
/* 初始化每 CPU 页面缓存 */
setup_per_cpu_pageset();
}
NUMA 感知:
free_area_init 会根据 SRAT(System Resource Affinity Table)为每个内存节点创建 pg_data_t 结构,并初始化节点内的 zone。AP 启动时通过 set_cpu_numa_node 绑定到对应节点。
4.4 调度器初始化:sched_init
sched_init 是进程调度器的初始化入口,它建立核心调度数据结构,包括运行队列(struct rq)、调度类(stop、dl、rt、cfs、idle)以及每 CPU 的 idle 线程。在 SMP 系统中,sched_init 还初始化负载均衡所需的域和组。
// kernel/sched/core.c (简化)
void __init sched_init(void)
{
int i;
/* 初始化每个 CPU 的运行队列 */
for_each_possible_cpu(i) {
struct rq *rq = cpu_rq(i);
raw_spin_lock_init(&rq->lock);
rq->nr_running = 0;
rq->calc_load_active = 0;
init_cfs_rq(&rq->cfs);
init_rt_rq(&rq->rt);
init_dl_rq(&rq->dl);
}
/* 设置 root_task_group 的调度参数 */
init_tg_cfs_entry(&root_task_group, &rq_global.cfs, NULL, NULL);
init_tg_rt_entry(&root_task_group, &rq_global.rt, NULL, NULL);
/* 初始化 idle 线程 */
for_each_possible_cpu(i) {
struct task_struct *idle = idle_thread_get(i);
__sched_fork(0, idle);
idle->__state = TASK_RUNNING;
set_task_cpu(idle, i);
}
/* 初始化负载均衡域(SMP 关键) */
sched_init_domains(cpu_active_mask);
}
| 调度类 | 优先级范围 | 运行队列结构 | SMP 负载均衡策略 |
|---|---|---|---|
| stop | 最高(-1) | 每 CPU 单链表 | 不参与均衡,独占 CPU |
| dl (Deadline) | 0 – 99 | 红黑树 + 双链表 | 全局 EDF,推拉迁移 |
| rt (Real-Time) | 100 – 139 | 优先级数组位图 | push/pull 迁移 |
| cfs (Completely Fair) | 140 – 139 | 红黑树(vruntime 排序) | load_balance() 周期性均衡 |
| idle | 最低 | 每 CPU 单 idle 线程 | 不参与均衡 |
调度器初始化与 CPU 热插拔的关系:
sched_init_domains 在启动时基于 cpu_active_mask 构建调度域。当后续 AP 上线时,sched_cpu_activate 回调会动态重建域,将新 CPU 纳入负载均衡范围。
4.5 BSP 初始化完成后的状态
当 start_kernel 执行到 arch_call_rest_init 时,BSP 已完成以下关键状态:
- 中断系统: IDT 就绪,本地 APIC 已配置,但全局中断仍关闭。
- 内存系统: 页表激活,伙伴系统和 slab 分配器可用,NUMA 节点信息已建立。
- 调度系统: 每 CPU 运行队列初始化完成,idle 线程就绪,调度域已构建。
- 时间系统: 时钟源注册,jiffies 开始递增。
- 控制台: 早期串口或 VGA 可用,内核日志可输出。
下一步: BSP 将调用
rest_init 创建内核初始化线程(kernel_init),然后自身转化为 idle 线程。随后 kernel_init 将调用 smp_init 唤醒所有 AP,进入多核协同启动阶段。