Linux 内核启动流程详解(基于 5.15.119 源码)

本文基于 Linux 5.15.119 实际源码,从 GRUB 加载到第一个用户进程 /sbin/init 启动,完整梳理 x86_64 架构的内核启动流程。


目录

  • [0. bzImage 的物理结构](#0. bzImage 的物理结构)
  • [1. GRUB 加载 bzImage](#1. GRUB 加载 bzImage)
  • [2. 实模式(16 位)------ 收集硬件信息](#2. 实模式(16 位)—— 收集硬件信息)
  • [3. 切换到保护模式(32 位)](#3. 切换到保护模式(32 位))
  • [4. 32 位解压入口](#4. 32 位解压入口)
  • [5. 64 位模式下的解压](#5. 64 位模式下的解压)
  • [6. 真正的内核 64 位入口](#6. 真正的内核 64 位入口)
  • [7. x86 架构的 C 语言入口](#7. x86 架构的 C 语言入口)
  • [8. 通用内核初始化 ------ start_kernel()](#8. 通用内核初始化 —— start_kernel())
  • [9. rest_init() ------ 创建两个"祖先进程"](#9. rest_init() —— 创建两个"祖先进程")
  • [10. kernel_init() ------ 启动第一个用户程序](#10. kernel_init() —— 启动第一个用户程序)
  • 总结:完整启动路线图

0. bzImage 的物理结构

你执行 make bzImage 编译出来的文件,其实不是单一文件,而是两部分拼接而成:

复制代码
┌─────────────────┬─────────────────────────────┐
│   setup.bin     │    compressed/vmlinux       │
│  (实模式代码)    │   (压缩的真正的内核)         │
│   ~30KB         │      几MB ~ 几十MB           │
└─────────────────┴─────────────────────────────┘
        ↑                              ↑
   从 bootsect 开始              从 startup_32 开始

Bootloader(GRUB)把它加载到内存后,CPU 先执行左边的 setup.bin(还是古老的 16 位实模式),然后才进入右边的压缩内核。

拼接工作在 arch/x86/boot/tools/build.c 中完成。


1. GRUB 加载 bzImage

GRUB 读取内核文件开头的 内核头部(Kernel Header),获取启动协议信息。

源码位置arch/x86/boot/header.S

这个文件开头定义了一个兼容 MS-DOS 的头部(MZ/PE 头),让 GRUB 能识别它:

asm 复制代码
#ifdef CONFIG_EFI_STUB
    .word   MZ_MAGIC            # "MZ" 魔数
#endif

头部中包含了 GRUB 需要的关键字段:

字段 含义
setup_sects setup 部分占多少扇区
syssize 内核大小
version 启动协议版本
cmd_line_ptr 命令行参数地址
code32_start 32 位代码入口点

GRUB 根据这些信息把 bzImage 加载到内存,然后把控制权交给 header.S 中的入口。


2. 实模式(16 位)------ 收集硬件信息

CPU 当前状态:16 位实模式,只能直接访问 1MB 内存,没有内存保护。

入口函数:main()

源码位置arch/x86/boot/main.c

这是整个内核启动的第一个 C 函数

c 复制代码
void main(void)
{
    /* 把 header 里的信息复制到 boot_params */
    copy_boot_params();

    /* 初始化早期控制台 */
    console_init();

    /* 初始化堆内存 */
    init_heap();

    /* 检查 CPU 是否支持运行这个内核 */
    if (validate_cpu()) {
        puts("Unable to boot...");
        die();
    }

    /* 告诉 BIOS 我们要进 64 位模式 */
    set_bios_mode();

    /* 用 BIOS 中断探测内存布局 */
    detect_memory();

    /* 设置键盘 */
    keyboard_init();

    /* 查询各种 BIOS 信息 */
    query_ist();
#if defined(CONFIG_APM) || defined(CONFIG_APM_MODULE)
    query_apm_bios();
#endif
#if defined(CONFIG_EDD) || defined(CONFIG_EDD_MODULE)
    query_edd();
#endif

    /* 设置显示模式 */
    set_video();

    /* ⭐ 进入保护模式 */
    go_to_protected_mode();
}

这些代码看起来古老,但非常关键------它用 BIOS 中断(int 0x10int 0x15 等)收集硬件信息,全部存进全局变量 boot_params,后面内核会一直用它。


3. 切换到保护模式(32 位)

go_to_protected_mode() 定义在 arch/x86/boot/pm.c

c 复制代码
void go_to_protected_mode(void)
{
    /* 关闭中断,执行实模式最后的钩子 */
    realmode_switch_hook();

    /* 开启 A20 地址线(历史遗留,为了访问 1MB 以上内存) */
    if (enable_a20()) {
        puts("A20 gate not responding, unable to boot...\n");
        die();
    }

    /* 重置协处理器 */
    reset_coprocessor();

    /* 屏蔽所有中断 */
    mask_all_interrupts();

    /* 设置 IDT 和 GDT */
    setup_idt();
    setup_gdt();

    /* ⭐ 真正跳转到保护模式 */
    protected_mode_jump(boot_params.hdr.code32_start,
                        (u32)&boot_params + (ds() << 4));
}

真正的切换发生在汇编文件 pmjump.S 中:

asm 复制代码
SYM_FUNC_START(protected_mode_jump)
    movl    %edx, %esi          # %edx = boot_params 指针

    movl    %cr0, %edx
    orb     $X86_CR0_PE, %dl    # 设置 CR0 的 PE (Protection Enable) 位
    movl    %edx, %cr0          # 打开保护模式!

    # 长跳转到 32 位代码段
    .byte   0x66, 0xea          # ljmpl 指令
    .long   .Lin_pm32           # 目标偏移
    .word   __BOOT_CS           # 代码段选择子

.Lin_pm32:
    # 设置 32 位平坦模式的数据段
    movl    %ecx, %ds
    movl    %ecx, %es
    movl    %ecx, %fs
    movl    %ecx, %gs
    movl    %ecx, %ss

    jmpl    *%eax               # 跳转到 32 位入口点

发生了什么 :CPU 从 16 位实模式 → 32 位保护模式。现在能访问 4GB 内存了,但还不是 64 位。


4. 32 位解压入口

现在 CPU 开始执行 arch/x86/boot/compressed/head_64.S 中的 startup_32

注意:文件名虽然是 head_64.S,但前面一大段是 .code32(32 位代码)。

asm 复制代码
    .code32
SYM_FUNC_START(startup_32)
    cld
    cli                         # 关中断

    # 计算"实际加载地址"与"编译时地址"的偏差
    # 因为 bootloader 可能把内核加载到了意料之外的位置
    leal    (BP_scratch+4)(%esi), %esp
    call    1f
1:  popl    %ebp
    subl    $ rva(1b), %ebp

    # 加载新的 GDT
    leal    rva(gdt)(%ebp), %eax
    movl    %eax, 2(%eax)
    lgdt    (%eax)

    # 设置数据段寄存器
    movl    $__BOOT_DS, %eax
    movl    %eax, %ds
    movl    %eax, %es
    movl    %eax, %fs
    movl    %eax, %gs
    movl    %eax, %ss

然后它继续:

  1. 建立临时的 4GB 页表(为进入长模式做准备)
  2. 打开 PAE (Physical Address Extension,cr4 |= X86_CR4_PAE
  3. 设置 EFER.LME(Long Mode Enable)
  4. 启用分页 (设置 cr0.PG = 1),CPU 自动进入兼容模式
  5. 长跳转到 64 位代码

5. 64 位模式下的解压

继续在同一份 head_64.S 中,现在 CPU 运行在 64 位模式了。

这段代码的任务是:

  1. 找到压缩的内核数据(piggy.o 里包着 vmlinux.bin.gz.xz 等)
  2. 把它解压到最终运行地址
  3. 跳转到解压后的内核入口

解压完成后,它跳转到真正的内核入口------startup_64(注意:这是另一个文件里的同名符号!)。

相关文件

  • arch/x86/boot/compressed/head_64.S
  • arch/x86/boot/compressed/misc.c(解压算法的 C 实现)
  • arch/x86/boot/compressed/piggy.S(把压缩后的内核二进制打包成汇编对象)

6. 真正的内核 64 位入口

现在进入 arch/x86/kernel/head_64.S 中的 startup_64

这是编译后的 vmlinux 的第一条指令 ,也是内核真正开始建立自己世界的地方。

asm 复制代码
    .code64
SYM_CODE_START_NOALIGN(startup_64)
    /* 设置栈(用 init_task 的栈) */
    leaq    (__end_init_task - FRAME_SIZE)(%rip), %rsp

    /* 调用 startup_64_setup_env 和 verify_cpu */
    leaq    _text(%rip), %rdi
    pushq   %rsi
    call    startup_64_setup_env
    popq    %rsi

    call    verify_cpu

    /* 修正页表(内核可能被加载到了与编译地址不同的位置) */
    leaq    _text(%rip), %rdi
    pushq   %rsi
    call    __startup_64
    popq    %rsi

    /* 形成 CR3 值(页表基址) */
    addq    $(early_top_pgt - __START_KERNEL_map), %rax

    /* 设置 CR4(PAE + PGE) */
    movl    $(X86_CR4_PAE | X86_CR4_PGE), %ecx
    movq    %rcx, %cr4

    /* 切换到新的页表 */
    movq    %rax, %cr3

    /* 加载内核的 GDT */
    lgdt    early_gdt_descr(%rip)

    /* 清空数据段寄存器 */
    xorl    %eax, %eax
    movl    %eax, %ds
    movl    %eax, %ss
    movl    %eax, %es
    movl    %eax, %fs
    movl    %eax, %gs

    /* 设置 IDT(早期中断描述符表) */
    pushq   %rsi
    call    early_setup_idt
    popq    %rsi

    /* 设置 EFER(使能 SYSCALL 等) */
    movl    $MSR_EFER, %ecx
    rdmsr
    btsl    $_EFER_SCE, %eax       /* Enable System Call */
    ...
    wrmsr

    /* 设置 CR0 */
    movl    $CR0_STATE, %eax
    movq    %rax, %cr0

    /* 清空中断标志 */
    pushq   $0
    popfq

    /* 准备跳转到 C 代码 */
    movq    %rsi, %rdi            # real_mode_data 作为参数

.Ljump_to_C_code:
    xorl    %ebp, %ebp            # 清空帧指针
    movq    initial_code(%rip), %rax    # rax = x86_64_start_kernel
    pushq   $__KERNEL_CS
    pushq   %rax
    lretq                         # 远返回,进入 x86_64_start_kernel

关键点:

  • initial_code 是一个全局变量,值为 x86_64_start_kernel
  • initial_stack 指向 init_thread_union(这是 PID 0,也就是 swapper/idle 进程的栈)
  • lretq 完成一次远返回,同时切换代码段和跳转到目标地址

7. x86 架构的 C 语言入口

lretq 把你带到了 arch/x86/kernel/head64.c

c 复制代码
asmlinkage __visible void __init x86_64_start_kernel(char * real_mode_data)
{
    /* 编译期检查 */
    BUILD_BUG_ON(MODULES_VADDR < __START_KERNEL_map);
    ...

    /* 初始化 CR4 影子寄存器 */
    cr4_init_shadow();

    /* 清除临时页表(去掉 identity map) */
    reset_early_page_tables();

    /* 清零 BSS 段 */
    clear_bss();

    /* 复制 boot_params 到内核空间 */
    copy_bootdata(__va(real_mode_data));

    /* 加载 CPU 微码 */
    load_ucode_bsp();

    /* 设置正式页表的高地址映射 */
    init_top_pgt[511] = early_top_pgt[511];

    x86_64_start_reservations(real_mode_data);
}

void __init x86_64_start_reservations(char *real_mode_data)
{
    if (!boot_params.hdr.version)
        copy_bootdata(__va(real_mode_data));

    x86_early_init_platform_quirks();

    /* 特殊平台处理 */
    switch (boot_params.hdr.hardware_subarch) {
    case X86_SUBARCH_INTEL_MID:
        x86_intel_mid_early_setup();
        break;
    default:
        break;
    }

    start_kernel();                 /* ⭐ 进入体系无关的通用初始化! */
}

从这里开始,代码变得体系无关 了------不管是 x86、ARM 还是 RISC-V,最终都会调用 start_kernel()


8. 通用内核初始化 ------ start_kernel()

这是整个内核最著名的函数,位于 init/main.c

c 复制代码
asmlinkage __visible void __init start_kernel(void)
{
    char *command_line;
    char *after_dashes;

    /* 设置 init_task(PID 0)的栈结束魔数 */
    set_task_stack_end_magic(&init_task);

    smp_setup_processor_id();
    cgroup_init_early();

    /* 关中断,初始化期间不允许打扰 */
    local_irq_disable();
    early_boot_irqs_disabled = true;

    boot_cpu_init();
    page_address_init();

    /* 打印 "Linux version 5.15.119 ..." */
    pr_notice("%s", linux_banner);

    early_security_init();

    /* ⭐ 架构相关初始化(内存、CPU、ACPI 等) */
    setup_arch(&command_line);

    setup_boot_config();
    setup_command_line(command_line);
    setup_nr_cpu_ids();
    setup_per_cpu_areas();
    smp_prepare_boot_cpu();
    boot_cpu_hotplug_init();

    /* 建立内存管理区 */
    build_all_zonelists(NULL);
    page_alloc_init();

    /* 打印命令行 */
    pr_notice("Kernel command line: %s\n", saved_command_line);

    /* 解析早期参数(如 mem=, console=) */
    parse_early_param();
    parse_args("Booting kernel", static_command_line, ...);

    /* ⭐ 初始化内存管理 */
    mm_init();

    ftrace_init();
    sched_init();                   /* ⭐ 初始化调度器 */
    radix_tree_init();
    workqueue_init_early();
    rcu_init();
    trace_init();
    context_tracking_init();
    early_irq_init();
    init_IRQ();                     /* 初始化中断 */
    tick_init();
    init_timers();
    timekeeping_init();
    time_init();                    /* 初始化时间子系统 */
    random_init(command_line);
    boot_init_stack_canary();

    /* 开中断 */
    local_irq_enable();
    early_boot_irqs_disabled = false;

    console_init();                 /* 正式初始化控制台 */

    /* ⭐ 初始化 VFS */
    vfs_caches_init();
    signals_init();
    pagecache_init();
    proc_root_init();               /* 创建 /proc */
    nsfs_init();
    cgroup_init();
    ...
    check_bugs();                   /* 检查 CPU bug */

    /* ⭐ 调用 rest_init() */
    arch_call_rest_init();
}

start_kernel() 初始化了内核的几乎所有核心子系统

  • 内存管理(MM)
  • 进程调度
  • 中断、定时器
  • 文件系统缓存
  • procfs、cgroups
  • ...

但最后它自己不变成用户进程,而是调用 rest_init()


9. rest_init() ------ 创建两个"祖先进程"

源码位置init/main.c

c 复制代码
noinline void __ref rest_init(void)
{
    struct task_struct *tsk;
    int pid;

    rcu_scheduler_starting();

    /*
     * 创建 PID 1:init 进程(用户态的祖先)
     */
    pid = kernel_thread(kernel_init, NULL, CLONE_FS);

    /* 把 init 固定在 boot CPU 上 */
    rcu_read_lock();
    tsk = find_task_by_pid_ns(pid, &init_pid_ns);
    tsk->flags |= PF_NO_SETAFFINITY;
    set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id()));
    rcu_read_unlock();

    /*
     * 创建 PID 2:kthreadd(内核线程的祖先)
     */
    pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);

    rcu_read_lock();
    kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns);
    rcu_read_unlock();

    /*
     * 启用调度器检查
     */
    system_state = SYSTEM_SCHEDULING;

    complete(&kthreadd_done);

    /*
     * 当前线程(PID 0)变成 idle 进程,永远执行 schedule()
     */
    schedule_preempt_disabled();
    cpu_startup_entry(CPUHP_ONLINE);   /* 进入 idle 循环 */
}

这是 Linux 进程树的起点

PID 进程 作用
0 swapper/idle 当前线程变身而来,没事干时执行它
1 kernel_init 接下来启动第一个用户程序(/sbin/init
2 kthreadd 专门"收养"内核线程,所有内核线程的父进程

10. kernel_init() ------ 启动第一个用户程序

源码位置init/main.c

c 复制代码
static int __ref kernel_init(void *unused)
{
    /* 等 kthreadd 准备好 */
    wait_for_completion(&kthreadd_done);

    /* 加载真正的根文件系统 */
    kernel_init_freeable();

    /* 等所有异步 __init 代码完成 */
    async_synchronize_full();

    /* 释放 __init 段内存(启动完成后这些代码不再需要) */
    free_initmem();
    mark_readonly();
    pti_finalize();

    /* 系统正式运行 */
    system_state = SYSTEM_RUNNING;
    numa_default_policy();
    rcu_end_inkernel_boot();

    /* 按优先级尝试启动第一个用户进程 */

    /* 1. initrd 中的 /init */
    if (ramdisk_execute_command) {
        ret = run_init_process(ramdisk_execute_command);
        if (!ret)
            return 0;
    }

    /* 2. 命令行 init=xxx 指定的程序 */
    if (execute_command) {
        ret = run_init_process(execute_command);
        if (!ret)
            return 0;
    }

    /* 3. 编译时默认的 init */
    if (CONFIG_DEFAULT_INIT[0] != '\0') {
        ret = run_init_process(CONFIG_DEFAULT_INIT);
        if (!ret)
            return 0;
    }

    /* 4. 默认路径 */
    if (!try_to_run_init_process("/sbin/init") ||
        !try_to_run_init_process("/etc/init") ||
        !try_to_run_init_process("/bin/init") ||
        !try_to_run_init_process("/bin/sh"))
        return 0;

    /* 全都失败 */
    panic("No working init found.  Try passing init= option to kernel.");
}

一旦 run_init_process("/sbin/init") 成功,内核就完成了它的启动使命。

从这一刻起,系统控制权交给了用户空间。

你看到的 systemd、sysvinit、或 bash,就是从这里开始的。


总结:完整启动路线图

Mermaid 流程图

GRUB/Bootloader 加载 bzImage
arch/x86/boot/header.S 内核头部
arch/x86/boot/main.c main()
收集硬件信息 copy_boot_params / detect_memory
arch/x86/boot/pm.c + pmjump.S go_to_protected_mode()
arch/x86/boot/compressed/head_64.S startup_32/startup_64 建立页表 + 解压内核
arch/x86/kernel/head_64.S startup_64 设置页表/GDT/IDT
arch/x86/kernel/head64.c x86_64_start_kernel()
init/main.c start_kernel() 初始化所有子系统
rest_init()
PID 0 cpu_startup_entry() swapper/idle
PID 1 kernel_init() 用户态祖先
PID 2 kthreadd 内核线程祖先
run_init_process() /sbin/init /etc/init /bin/init /bin/sh
用户空间 systemd / init / bash

ASCII 路线图(备用)

复制代码
GRUB 加载 bzImage
    │
    ▼
┌─────────────────────────────────────────────┐
│  arch/x86/boot/header.S                     │  内核头部,存放启动协议信息
└─────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────┐
│  arch/x86/boot/main.c   main()              │  ◄── 16位实模式
│      └── go_to_protected_mode()             │      收集硬件信息到 boot_params
└─────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────┐
│  arch/x86/boot/pmjump.S                     │  ◄── 打开 CR0.PE
│      protected_mode_jump()                  │      进入 32 位保护模式
└─────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────┐
│  arch/x86/boot/compressed/head_64.S         │  ◄── 32→64位切换
│      startup_32 → startup_64                │      建立页表,解压内核
└─────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────┐
│  arch/x86/kernel/head_64.S                  │  ◄── 真正的内核 64 位入口
│      startup_64 → lretq                     │      设置页表/GDT/IDT
└─────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────┐
│  arch/x86/kernel/head64.c                   │  ◄── x86 架构 C 入口
│      x86_64_start_kernel()                  │      清零 BSS,加载微码
│      └── start_kernel()                     │
└─────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────┐
│  init/main.c   start_kernel()               │  ◄── 初始化所有子系统
│      └── arch_call_rest_init()              │      MM、调度、IRQ、VFS...
│          └── rest_init()                    │
│              ├── kernel_thread(kernel_init) │  PID 1
│              ├── kernel_thread(kthreadd)    │  PID 2
│              └── cpu_startup_entry()        │  PID 0 (idle)
└─────────────────────────────────────────────┘
    │
    ▼
┌─────────────────────────────────────────────┐
│  init/main.c   kernel_init()                │  ◄── 启动 /sbin/init
│      └── run_init_process()                 │
└─────────────────────────────────────────────┘
    │
    ▼
  用户空间(systemd / init / bash)

相关推荐
2401_840192271 小时前
k8s的crd、operator、cr分别是什么?
运维·分布式·kubernetes·prometheus
草木深雨纷纷1 小时前
mt管理器手机版下载2026最新版更新下载分享
linux·运维·网络·智能手机
扛枪的书生1 小时前
ELK 学习总结
linux
OYangxf1 小时前
对TinyRedis中主从复制的理解
运维·服务器
Irene19912 小时前
大数据开发面试常问的 Linux 命令 总结
大数据·linux
銳昊城2 小时前
项目六: 配置与管理DNS服务器(2) C2
运维·服务器
辰尘_星启2 小时前
【Linux】Python Socket编程指南
linux·python·socket·系统·通信
搞科研的小刘选手2 小时前
【天津市电源学会主办】第七届能源电力与自动化工程国际学术会议(ICEPAE 2026)
运维·自动化·能源·电力·电气·控制工程·节能
恋奴娇2 小时前
ubuntu 25 突破pipewire 不能以root帐号运行 系统没有声音输入输出设备
linux·运维·ubuntu