你的电脑是怎么启动的? 一起来学习Linux的启动过程

263 阅读30分钟

概述

Linux系统的启动过程是一个复杂而精密的过程,从硬件上电到用户登录界面,涉及多个阶段和大量的源码。本文档将结合Linux内核源码,详细分析每个启动阶段的实现原理、关键函数和数据结构。

Linux-Linux系统启动过程详细时序图.png

image.png


BIOS/UEFI启动阶段

硬件上电和POST

当计算机上电时,CPU会从固定的地址开始执行代码,这个地址通常是BIOS/UEFI固件的入口点。

BIOS启动流程

1. 上电自检(POST) - 检测硬件组件
2. 初始化基本硬件 - CPU、内存、基本I/O
3. 枚举启动设备 - 硬盘、USB、网络等
4. 读取MBR - 从启动设备的第一个扇区读取512字节
5. 跳转执行 - 将控制权交给引导程序

UEFI启动流程

1. SEC阶段 - 安全验证
2. PEI阶段 - 预EFI初始化
3. DXE阶段 - 驱动执行环境
4. BDS阶段 - 启动设备选择
5. RT阶段 - 运行时服务

关键概念

实模式: BIOS启动时CPU处于16位实模式,只能访问1MB内存。 保护模式: 现代操作系统需要切换到32位保护模式。 长模式: 64位系统需要进一步切换到长模式。


引导加载器阶段

MBR结构分析

主引导记录(MBR)位于磁盘的第一个扇区,结构如下:

// MBR结构定义
struct mbr {
    uint8_t  boot_code[446];    // 引导代码
    struct {
        uint8_t  status;        // 分区状态
        uint8_t  start_head;    // 起始磁头
        uint16_t start_sector;  // 起始扇区
        uint8_t  type;          // 分区类型
        uint8_t  end_head;      // 结束磁头
        uint16_t end_sector;    // 结束扇区
        uint32_t start_lba;     // 起始LBA
        uint32_t size;          // 分区大小
    } partition[4];             // 4个主分区
    uint16_t signature;         // 魔数 0x55AA
};

这个MBR结构就像是硬盘的"身份证",总共512字节。想象一下:

  • boot_code[446]:这是一段446字节的小程序,就像是"启动管家",负责找到并启动操作系统
  • partition[4]:这是4个分区的信息表,每个分区记录了"我在硬盘的哪里,有多大"
  • signature:这是个魔数0x55AA,就像是"合法标记",BIOS看到这个数字才知道"哦,这是个可启动的硬盘"

简单说,MBR就是硬盘开头的一个小"目录",告诉计算机"启动程序在这里,分区信息在那里"。

GRUB引导加载器

GRUB(GRand Unified Bootloader)是最常用的Linux引导加载器。

GRUB Stage 1

位于MBR的446字节引导代码:

# arch/x86/boot/header.S (简化版本)
.section ".bstext", "ax"
.global bootsect_start
bootsect_start:
    # 设置段寄存器
    movw    $0x07c0, %ax
    movw    %ax, %ds
    movw    %ax, %es
    movw    %ax, %ss

    # 设置栈指针
    movw    $0x7c00, %sp

    # 加载Stage 2
    movw    $0x0201, %ax        # 读取1个扇区
    movw    $0x0002, %cx        # 从第2扇区开始
    movw    $0x0000, %dx        # 磁头0,驱动器0
    movw    $0x7e00, %bx        # 加载到0x7e00
    int     $0x13               # BIOS中断调用

    # 跳转到Stage 2
    jmp     0x7e00

这段汇编代码就是GRUB Stage 1的核心,只有446字节,但麻雀虽小五脏俱全:

  1. 设置工作环境:前几行是在"整理桌面",设置段寄存器和栈指针,就像搬家后先整理好房间
  2. 读取更多代码int $0x13是调用BIOS的"搬运工",把硬盘第2扇区的内容搬到内存0x7e00位置
  3. 交接工作:最后jmp 0x7e00就是"我的任务完成了,接下来交给你"

想象这就像接力赛:Stage 1是第一棒,它的任务很简单——把第二棒(Stage 2)找来并交给他。因为MBR只有446字节空间,装不下复杂的启动程序,所以只能先装个"搬运工"。

GRUB Stage 2

Stage 2具有文件系统支持,可以读取配置文件:

// grub-core/kern/main.c (简化)
void grub_main(void) {
    // 初始化内存管理
    grub_mm_init_regions();

    // 初始化设备
    grub_device_initialize();

    // 加载配置文件
    grub_load_config();

    // 显示启动菜单
    grub_show_menu();

    // 加载选定的内核
    grub_load_kernel();
}

GRUB Stage 2就像一个"智能管家",功能比Stage 1强大多了:

  1. grub_mm_init_regions() :先整理内存,就像管家清点家里有多少房间可以用
  2. grub_device_initialize() :认识所有设备,"这是硬盘,这是U盘,这是光驱"
  3. grub_load_config() :读取配置文件(grub.cfg),就像看菜单知道今天有什么菜
  4. grub_show_menu() :显示启动菜单,让用户选择"今天想吃什么"(启动哪个系统)
  5. grub_load_kernel() :根据用户选择,把对应的内核加载到内存,准备"上菜"

这就是为什么我们开机时能看到选择界面,能选择不同的Linux版本或Windows——都是GRUB Stage 2的功劳!

内核参数传递

GRUB通过boot_params结构传递参数给内核:

// arch/x86/include/uapi/asm/bootparam.h
struct boot_params {
    struct screen_info screen_info;        // 屏幕信息
    struct apm_bios_info apm_bios_info;    // APM BIOS信息
    uint8_t  _pad2[4];
    uint64_t tboot_addr;
    struct ist_info ist_info;              // Intel SpeedStep信息
    uint8_t  _pad3[16];
    uint8_t  hd0_info[16];                 // 硬盘0信息
    uint8_t  hd1_info[16];                 // 硬盘1信息
    struct sys_desc_table sys_desc_table;  // 系统描述符表
    struct olpc_ofw_header olpc_ofw_header;
    uint32_t ext_ramdisk_image;            // 扩展ramdisk镜像
    uint32_t ext_ramdisk_size;             // 扩展ramdisk大小
    uint32_t ext_cmd_line_ptr;             // 扩展命令行指针
    uint8_t  _pad4[116];
    struct edid_info edid_info;            // EDID信息
    struct efi_info efi_info;              // EFI信息
    uint32_t alt_mem_k;                    // 备用内存大小
    uint32_t scratch;                      // 临时空间
    uint8_t  e820_entries;                 // E820内存映射条目数
    uint8_t  eddbuf_entries;               // EDD缓冲区条目数
    uint8_t  edd_mbr_sig_buf_entries;      // EDD MBR签名缓冲区条目数
    uint8_t  kbd_status;                   // 键盘状态
    uint8_t  secure_boot;                  // 安全启动状态
    uint8_t  _pad5[2];
    uint8_t  sentinel;                     // 哨兵值
    uint8_t  _pad6[1];
    struct setup_header hdr;               // 设置头部
    uint8_t  _pad7[0x290-0x1f1-sizeof(struct setup_header)];
    uint32_t edd_mbr_sig_buffer[EDD_MBR_SIG_MAX]; // EDD MBR签名缓冲区
    struct e820_entry e820_table[E820_MAX_ENTRIES_ZEROPAGE]; // E820内存映射表
    uint8_t  _pad8[48];
    struct edd_info eddbuf[EDDMAXNR];      // EDD信息缓冲区
    uint8_t  _pad9[276];
} __attribute__((packed));

内核解压和早期启动

内核镜像格式

Linux内核通常以bzImage格式分发,这是一个自解压的镜像:

bzImage结构:
+------------------+
| 实模式代码       |  <- arch/x86/boot/
+------------------+
| 设置头部         |  <- setup_header
+------------------+
| 压缩内核        |  <- 实际的内核代码(压缩)
+------------------+
| 解压代码        |  <- arch/x86/boot/compressed/
+------------------+

实模式启动代码

内核的实模式启动代码位于arch/x86/boot/目录:

# arch/x86/boot/header.S
.section ".bstext", "ax"
.global _start
_start:
    # 设置段寄存器
    movw    %cs, %ax
    movw    %ax, %ds
    movw    %ax, %es
    movw    %ax, %ss

    # 检查启动签名
    cmpw    $0xAA55, (0x1FE)
    jne     setup_bad

    # 跳转到主设置代码
    jmp     main

这是内核自己的启动代码,就像内核的"开场白":

  1. 设置段寄存器:还是在"整理房间",让CPU知道数据在哪里
  2. 检查启动签名cmpw $0xAA55, (0x1FE)就像检查"通行证",看看0x1FE位置是不是有0xAA55这个魔数
  3. 验证通过就继续:如果签名对了,就跳转到main函数;如果不对,就跳到setup_bad(出错处理)

这就像门卫检查身份证:看到正确的证件号码才让你进门,否则就说"你走错地方了"。内核很谨慎,不会随便启动!

// arch/x86/boot/main.c
void main(void) {
    // 复制启动参数
    copy_boot_params();

    // 初始化控制台
    console_init();

    // 检测内存
    detect_memory();

    // 设置键盘
    keyboard_init();

    // 检查CPU特性
    validate_cpu();

    // 设置视频模式
    set_video();

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

这个main函数就像内核的"体检和准备工作",每一步都很重要:

  1. copy_boot_params() :把GRUB传来的参数抄一遍,就像"记下老师布置的作业"
  2. console_init() :准备控制台,让内核能够"说话"(显示信息)
  3. detect_memory() :探测内存大小,"看看家里有多少钱可以花"
  4. keyboard_init() :初始化键盘,"确保能听到用户的指令"
  5. validate_cpu() :检查CPU功能,"看看这台电脑能不能跑我这个内核"
  6. set_video() :设置显示模式,"调整好屏幕显示"
  7. go_to_protected_mode() :最后进入保护模式,"从16位升级到32位,准备干大事"

这就像搬进新房子前的准备工作:检查水电、测试网络、调试家电,一切就绪后才正式入住!

保护模式切换

从实模式切换到保护模式的关键代码:

# arch/x86/boot/pmjump.S
.section ".text32","ax"
.code32
.global protected_mode_jump
protected_mode_jump:
    # 设置数据段
    movl    $__BOOT_DS, %eax
    movl    %eax, %ds
    movl    %eax, %es
    movl    %eax, %fs
    movl    %eax, %gs
    movl    %eax, %ss

    # 设置栈指针
    movl    %edx, %esp

    # 跳转到32位代码
    jmpl    *%eax

这段代码是CPU的"升级仪式",从16位实模式跳转到32位保护模式:

  1. 设置数据段:前面几行是在"重新装修房间",把所有段寄存器都设置成32位保护模式的格式
  2. 设置栈指针movl %edx, %esp是"搬家具",把栈指针也升级到32位
  3. 正式跳转jmpl *%eax就是"搬进新房子",正式进入32位世界

想象这就像从"平房"搬到"高楼":

  • 实模式像平房,只能用1MB内存,功能有限
  • 保护模式像高楼,可以用4GB内存,还有内存保护功能

这一跳,CPU就从"古代"进入了"现代"!

内核解压

压缩内核的解压代码位于arch/x86/boot/compressed/

// arch/x86/boot/compressed/misc.c
asmlinkage __visible void *extract_kernel(void *rmode, memptr heap,
                                         unsigned char *input_data,
                                         unsigned long input_len,
                                         unsigned char *output,
                                         unsigned long output_len) {
    // 初始化控制台
    console_init();

    // 解析ELF头部
    parse_elf(output);

    // 解压内核
    __decompress(input_data, input_len, NULL, NULL, output, output_len, NULL, error);

    // 解析重定位
    handle_relocations(output, output_len, virt_addr);

    // 返回内核入口点
    return output;
}

这个函数就像"拆快递"的过程,内核是被压缩打包的,需要解压才能用:

  1. console_init() :先准备好"工作台",让解压过程能显示进度
  2. parse_elf(output) :检查"包装标签",确认这确实是个ELF格式的内核文件
  3. __decompress(...) :这是核心步骤,"拆包装,取出真货",把压缩的内核数据解压到指定位置
  4. handle_relocations(...) :"重新贴标签",因为内核可能被加载到不同的内存位置,需要调整内部的地址引用
  5. return output:最后返回解压后内核的地址,"告诉大家货物在哪里"

就像网购收到压缩包:先确认是你的包裹,然后解压,最后整理好放在合适的位置。内核解压完成后,就可以正式启动了!

64位长模式切换

对于64位系统,需要切换到长模式:

# arch/x86/boot/compressed/head_64.S
.code32
startup_32:
    # 设置页表
    leal    pgtable(%ebx), %edi
    xorl    %eax, %eax
    movl    $(BOOT_INIT_PGT_SIZE/4), %ecx
    rep     stosl

    # 设置PML4
    leal    pgtable + 0(%ebx), %edi
    leal    0x1007 (%edi), %eax
    movl    %eax, 0(%edi)

    # 启用PAE
    movl    %cr4, %eax
    orl     $X86_CR4_PAE, %eax
    movl    %eax, %cr4

    # 设置长模式位
    movl    $MSR_EFER, %ecx
    rdmsr
    btsl    $_EFER_LME, %eax
    wrmsr

    # 启用分页
    movl    $(X86_CR0_PG | X86_CR0_PE), %eax
    movl    %eax, %cr0

    # 跳转到64位代码
    ljmp    $__KERNEL_CS, $startup_64

这段代码是CPU的"第二次升级",从32位跳到64位,就像从"普通楼房"搬到"摩天大楼":

  1. 设置页表:前几行是"画地图",建立虚拟内存到物理内存的映射表
  2. 设置PML4:这是64位系统的"总目录",告诉CPU怎么找到各个内存页面
  3. 启用PAE:打开"物理地址扩展",让CPU能处理更大的内存地址
  4. 设置长模式位:在EFER寄存器里设置标志,告诉CPU"我要进入64位模式了"
  5. 启用分页:打开分页机制,让虚拟内存系统开始工作
  6. 跳转到64位:最后一跳,正式进入64位世界

想象这就像:

  • 32位是"4层楼房"(4GB内存)
  • 64位是"无限高的摩天大楼"(理论上无限内存)

这一跳,CPU就有了处理现代大内存系统的能力!


内核主初始化

start_kernel函数

内核的C语言入口点是start_kernel()函数,位于init/main.c

start_kernel()就像内核的"总指挥官",负责协调整个启动过程。这个函数有100多行代码,每一行都在做重要的初始化工作,就像一个大型工厂的开工仪式,需要按顺序启动各个车间。

// init/main.c
asmlinkage __visible void __init start_kernel(void)
{
    char *command_line;
    char *after_dashes;

    // 设置任务栈结束魔数
    set_task_stack_end_magic(&init_task);

    // 设置SMP处理器ID
    smp_setup_processor_id();

    // 调试对象早期初始化
    debug_objects_early_init();

    // 初始化控制组
    cgroup_init_early();

    // 禁用本地中断
    local_irq_disable();

    // 早期启动中断禁用
    early_boot_irqs_disabled = true;

    // 启用早期启动
    boot_cpu_init();

    // 页地址初始化
    page_address_init();

    // 打印内核版本信息
    pr_notice("%s", linux_banner);

    // 架构相关的早期设置
    setup_arch(&command_line);

    // 设置命令行
    setup_command_line(command_line);

    // 设置每CPU区域
    setup_per_cpu_areas();

    // SMP准备启动
    smp_prepare_boot_cpu();

    // 构建所有区域列表
    build_all_zonelists(NULL);

    // 页分配器初始化
    page_alloc_init();

    // 解析早期参数
    parse_early_param();

    // 解析参数后的设置
    after_dashes = parse_args("Booting kernel",
                             static_command_line, __start___param,
                             __stop___param - __start___param,
                             -1, -1, NULL, &unknown_bootoption);

    // 跳转标签初始化
    jump_label_init();

    // 设置日志缓冲区
    setup_log_buf(0);

    // 虚拟文件系统缓存初始化
    vfs_caches_init_early();

    // 排序异常表
    sort_main_extable();

    // 陷阱初始化
    trap_init();

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

    // 调度器初始化
    sched_init();

    // 禁用抢占
    preempt_disable();

    // 检查中断是否被错误启用
    if (WARN(!irqs_disabled(),
             "Interrupts were enabled *very* early, fixing it\n"))
        local_irq_disable();

    // 基数树初始化
    radix_tree_init();

    // 工作队列早期初始化
    workqueue_init_early();

    // RCU初始化
    rcu_init();

    // 跟踪初始化
    trace_init();

    // 上下文跟踪初始化
    context_tracking_init();

    // 早期中断和异常处理初始化
    early_irq_init();

    // 中断初始化
    init_IRQ();

    // 时钟初始化
    tick_init();

    // RCU初始化后处理
    rcu_init_nohz();

    // 时钟源和时钟事件初始化
    init_timers();

    // 高分辨率时钟初始化
    hrtimers_init();

    // 软中断初始化
    softirq_init();

    // 时间保持初始化
    timekeeping_init();

    // 时间初始化
    time_init();

    // 打印CPU信息
    printk_safe_init();

    // 性能事件初始化
    perf_event_init();

    // 分析器初始化
    profile_init();

    // 调用构造函数
    call_function_init();

    // 启用中断
    local_irq_enable();

    // 内存管理后期初始化
    kmem_cache_init_late();

    // 控制台初始化
    console_init();

    // 如果启用了panic超时,设置panic超时
    if (panic_later)
        panic("Too many boot %s vars at `%s'", panic_later,
              panic_param);

    // 锁依赖初始化
    lockdep_init();

    // 内存泄漏检测初始化
    kmemleak_init();

    // 设置每CPU页框架
    setup_per_cpu_pageset();

    // NUMA策略初始化
    numa_policy_init();

    // 调度器后期初始化
    sched_clock_init();

    // 校准延迟
    calibrate_delay();

    // PID哈希表初始化
    pid_idr_init();

    // 匿名VMA初始化
    anon_vma_init();

    // 凭证初始化
    cred_init();

    // 分叉初始化
    fork_init();

    // 进程初始化
    proc_caches_init();

    // 缓冲区初始化
    buffer_init();

    // 密钥初始化
    key_init();

    // 安全初始化
    security_init();

    // 调试对象内存初始化
    dbg_late_init();

    // VFS缓存初始化
    vfs_caches_init();

    // 页写回初始化
    pagecache_init();

    // 信号初始化
    signals_init();

    // 序列文件初始化
    seq_file_init();

    // 进程文件系统初始化
    proc_root_init();

    // 网络套接字初始化
    nsfs_init();

    // CPU热插拔初始化
    cpuset_init();

    // 控制组初始化
    cgroup_init();

    // 任务统计初始化
    taskstats_init_early();

    // 分隔符初始化
    delayacct_init();

    // 检查错误
    check_bugs();

    // ACPI早期初始化
    acpi_subsystem_init();

    // 架构调用
    arch_call_rest_init();
}

start_kernel函数详细讲解:

这个函数就像"开工厂"的完整流程,每个步骤都有特定的目的:

基础设施阶段(前10步):

  • set_task_stack_end_magic():给第一个任务的栈底放个"魔数",就像在地基上做标记
  • smp_setup_processor_id():给CPU编号,"我是1号CPU"
  • local_irq_disable():先关闭中断,"施工期间禁止打扰"

核心系统阶段(中间部分):

  • mm_init():建立内存管理系统,"规划土地使用"
  • sched_init():建立调度器,"安排工人轮班制度"
  • trap_init():设置异常处理,"安装安全系统"

时间和中断阶段:

  • init_IRQ():初始化中断系统,"安装电话系统"
  • time_init():初始化时间系统,"安装时钟"
  • tick_init():设置时钟节拍,"设定工作节奏"

高级功能阶段(后面部分):

  • vfs_caches_init():文件系统缓存,"建立仓库管理"
  • signals_init():信号系统,"建立通信系统"
  • proc_root_init():/proc文件系统,"建立信息公告板"

整个过程就像建设一个现代化工厂:先打地基、建框架,再安装设备、调试系统,最后开始正常运营!

setup_arch函数

setup_arch()是架构相关的初始化函数:

// arch/x86/kernel/setup.c
void __init setup_arch(char **cmdline_p)
{
    // 设置内存管理
    memblock_reserve(__pa_symbol(_text),
                     (unsigned long)__bss_stop - (unsigned long)_text);

    // 早期保留内存
    early_reserve_memory();

    // 解析设置数据
    parse_setup_data();

    // 复制启动命令行
    copy_edd();

    // 根文件系统设置
    ROOT_DEV = old_decode_dev(boot_params.hdr.root_dev);

    // 屏幕信息设置
    screen_info = boot_params.screen_info;

    // 复制EDD信息
    copy_edd();

    // 解析早期参数
    parse_early_param();

    // 内存映射初始化
    init_memory_mapping();

    // IO APIC初始化
    idt_setup_early_handler();

    // 早期平台设备初始化
    early_platform_quirks();

    // 根据E820映射初始化内存
    e820__memory_setup();

    // 解析内存映射
    parse_setup_data();

    // 复制启动参数
    copy_edd();

    // 初始化内存映射
    init_memory_mapping();

    // 早期中断描述符表设置
    idt_setup_early_traps();

    // 早期CPU初始化
    early_cpu_init();

    // 架构初始化
    arch_init_ideal_nops();

    // 跳转标签初始化
    jump_label_init();

    // 静态键初始化
    static_key_init();

    // 早期PCI初始化
    early_quirks();

    // 初始化内存映射
    init_memory_mapping();

    // 内存块设置
    memblock_set_current_limit(ISA_END_ADDRESS);

    // 早期IO APIC初始化
    early_trap_pf_init();

    // 设置实模式陷阱
    setup_real_mode();

    // 内存块设置
    memblock_set_current_limit(get_max_mapped());

    // 解析ACPI表
    acpi_table_upgrade();

    // ACPI启动表初始化
    acpi_boot_table_init();

    // 早期ACPI启动初始化
    early_acpi_boot_init();

    // 中断初始化
    initmem_init();

    // DMA连续内存分配器初始化
    dma_contiguous_reserve(max_pfn_mapped << PAGE_SHIFT);

    // 如果是EFI启动,初始化EFI
    if (efi_enabled(EFI_BOOT))
        efi_init();

    // 保留标准IO端口
    reserve_standard_io_resources();

    // 早期平台设备初始化
    early_platform_quirks();

    // 内存测试
    memtest();

    // 早期内存分配器转换到buddy分配器
    mm_init();

    // 设置每CPU区域
    setup_per_cpu_areas();

    // 最大内存映射设置
    max_pfn_mapped = KERNEL_IMAGE_SIZE >> PAGE_SHIFT;

    // 清理高内存
    cleanup_highmap();

    // 内存映射初始化
    memblock_set_current_limit(ISA_END_ADDRESS);

    // 早期IO APIC初始化
    early_trap_pf_init();
}

子系统初始化

内存管理初始化

内存管理子系统的初始化是内核启动的关键部分:

// init/main.c
static void __init mm_init(void)
{
    // 页分配器初始化
    page_ext_init_flatmem();

    // 内存初始化
    mem_init();

    // 内存块释放
    memblock_discard();

    // Slab分配器初始化
    kmem_cache_init();

    // 每CPU页框架初始化
    percpu_init_late();

    // 页表锁初始化
    pgtable_init();

    // 虚拟内存区域缓存初始化
    vmalloc_init();

    // IO映射初始化
    ioremap_huge_init();

    // 页面所有者初始化
    page_owner_init();
}

mm_init()就像"建立银行系统",管理内存这个"钱财":

  1. page_ext_init_flatmem() :建立"账本扩展页",记录每个内存页的额外信息
  2. mem_init() :正式启动内存管理,"银行开门营业"
  3. memblock_discard() :丢弃早期的临时内存管理器,"旧系统下线"
  4. kmem_cache_init() :初始化SLAB分配器,"建立小额快速取款机"
  5. percpu_init_late() :为每个CPU准备专用内存区域,"每个柜台都有自己的钱箱"
  6. vmalloc_init() :初始化虚拟内存分配器,"建立信用卡系统"
  7. ioremap_huge_init() :初始化大页IO映射,"建立大额转账通道"

整个过程就是从"原始的物物交换"升级到"现代银行系统",让内存管理变得高效、安全、有序!

调度器初始化

调度器是内核的核心组件:

// kernel/sched/core.c
void __init sched_init(void)
{
    int i, j;
    unsigned long alloc_size = 0, ptr;

    // 等待位初始化
    wait_bit_init();

    // 分配根任务组
    alloc_size += 2 * nr_cpu_ids * sizeof(void **);

    // 分配内存
    if (alloc_size) {
        ptr = (unsigned long)kzalloc(alloc_size, GFP_NOWAIT);

        // 根任务组初始化
        root_task_group.se = (struct sched_entity **)ptr;
        ptr += nr_cpu_ids * sizeof(void **);

        root_task_group.cfs_rq = (struct cfs_rq **)ptr;
        ptr += nr_cpu_ids * sizeof(void **);
    }

    // 初始化任务组
    init_tg_cfs_entry(&root_task_group, &rq->cfs, NULL, i, NULL);

    // 运行队列初始化
    for_each_possible_cpu(i) {
        struct rq *rq;

        rq = cpu_rq(i);
        raw_spin_lock_init(&rq->lock);
        rq->nr_running = 0;
        rq->calc_load_active = 0;
        rq->calc_load_update = jiffies + LOAD_FREQ;
        init_cfs_rq(&rq->cfs);
        init_rt_rq(&rq->rt);
        init_dl_rq(&rq->dl);

        // 根任务组设置
        root_task_group.shares = ROOT_TASK_GROUP_LOAD;

        // 初始化运行队列
        rq->tmp_alone_branch = &rq->leaf_cfs_rq_list;

        // 负载平衡初始化
        rq->sd = NULL;
        rq->rd = NULL;
        rq->cpu_capacity = rq->cpu_capacity_orig = SCHED_CAPACITY_SCALE;
        rq->balance_callback = NULL;
        rq->active_balance = 0;
        rq->next_balance = jiffies;
        rq->push_cpu = 0;
        rq->cpu = i;
        rq->online = 0;
        rq->idle_stamp = 0;
        rq->avg_idle = 2*sysctl_sched_migration_cost;
        rq->max_idle_balance_cost = sysctl_sched_migration_cost;

        INIT_LIST_HEAD(&rq->cfs_tasks);

        rq_attach_root(rq, &def_root_domain);
    }

    // 设置负载权重
    set_load_weight(&init_task, false);

    // 初始化任务的调度实体
    for_each_possible_cpu(i) {
        struct rq *rq = cpu_rq(i);
        init_tg_cfs_entry(&root_task_group, &rq->cfs, NULL, i, NULL);
    }

    // 调度器统计初始化
    scheduler_running = 1;
}

sched_init()就像"建立工厂的轮班制度",让多个任务能够有序地使用CPU:

建立管理架构:

  • wait_bit_init():初始化等待机制,"建立排队系统"
  • 分配根任务组内存:为调度器准备"管理办公室"

为每个CPU建立运行队列:

  • cpu_rq(i):获取每个CPU的运行队列,"每个车间都有自己的任务清单"
  • init_cfs_rq():初始化CFS队列,"建立公平排班制度"
  • init_rt_rq():初始化实时队列,"建立紧急任务通道"
  • init_dl_rq():初始化截止时间队列,"建立限时任务管理"

设置负载均衡:

  • rq->cpu_capacity:设置CPU能力,"记录每个工人的工作能力"
  • rq->next_balance:设置下次负载均衡时间,"定期检查工作分配是否合理"

最终启动:

  • scheduler_running = 1:标记调度器开始工作,"工厂正式开工"

整个过程就像建立一个高效的工厂管理系统:每个车间(CPU)都有自己的任务队列,有公平制度、紧急通道,还有定期的工作量平衡检查!

中断子系统初始化

中断处理是系统的基础:

// kernel/irq/irqdesc.c
void __init early_irq_init(void)
{
    int count;
    int i;

    // 初始化中断描述符
    initcnt = arch_probe_nr_irqs();

    if (initcnt > nr_irqs) {
        nr_irqs = initcnt;
        printk(KERN_INFO "nr_irqs: %d\n", nr_irqs);
    }

    // 分配中断描述符数组
    for (i = 0; i < initcnt; i++) {
        struct irq_desc *desc = alloc_desc(i, NUMA_NO_NODE, 0, NULL);
        set_bit(i, allocated_irqs);
        irq_insert_desc(i, desc);
    }
}

// arch/x86/kernel/irqinit.c
void __init init_IRQ(void)
{
    int i;

    // 执行架构特定的中断初始化
    x86_init.irqs.intr_init();

    // 为每个可能的中断向量设置中断门
    for (i = FIRST_EXTERNAL_VECTOR; i < NR_VECTORS; i++) {
        if (i != SYSCALL_VECTOR)
            set_intr_gate(i, irq_entries_start +
                         8 * (i - FIRST_EXTERNAL_VECTOR));
    }

    // 设置系统调用向量
    set_system_intr_gate(SYSCALL_VECTOR, entry_INT80_32);

    // 设置用于IRQ的向量
    set_intr_gate(IRQ_MOVE_CLEANUP_VECTOR, irq_move_cleanup_interrupt);
    set_intr_gate(REBOOT_VECTOR, reboot_interrupt);
}

early_irq_init() - 建立"呼叫中心": 这个函数就像建立一个大型呼叫中心:

  1. arch_probe_nr_irqs() :先调查"需要多少个电话线"
  2. 分配中断描述符:为每个中断号准备一个"接线员工位",每个工位都有编号和处理规则

init_IRQ() - 安装"电话交换机": 这个函数负责具体的"电话线路连接":

  1. x86_init.irqs.intr_init() :执行硬件相关的初始化,"安装交换机硬件"
  2. 设置中断门:为每个中断向量设置处理入口,就像"给每个电话号码分配接线员"
  3. set_system_intr_gate(SYSCALL_VECTOR, ...) :专门为系统调用设置特殊通道,"VIP专线"
  4. 设置特殊中断:为重启、清理等特殊操作设置专用中断,"紧急热线"

整个过程就像建立现代化的客服中心:

  • 每个硬件设备(键盘、鼠标、网卡等)都有自己的"专线电话"
  • 当设备需要CPU注意时,就"打电话"(发送中断)
  • 内核的中断系统就是"总机",负责接听并转接到正确的处理程序

这样,CPU就能及时响应各种硬件事件,而不用一直"轮询"检查!

时钟子系统初始化

时钟和定时器管理:

// kernel/time/tick-common.c
void __init tick_init(void)
{
    // 时钟事件设备初始化
    tick_broadcast_init();

    // 时钟源初始化
    tick_nohz_init();
}

// kernel/time/timer.c
void __init init_timers(void)
{
    // 初始化定时器基础
    init_timer_stats();

    // 为每个CPU初始化定时器
    timer_cpu_notify(&timers_nb, (unsigned long)CPU_UP_PREPARE,
                     (void *)(long)smp_processor_id());

    // 注册CPU通知器
    register_cpu_notifier(&timers_nb);

    // 打开软中断
    open_softirq(TIMER_SOFTIRQ, run_timer_softirq);
}

用户空间启动

init进程创建

内核创建第一个用户空间进程:

// init/main.c
static int __ref kernel_init(void *unused)
{
    int ret;

    // 等待kthreadd启动
    kernel_init_freeable();

    // 需要在这里完成init,因为它是一个异步调用
    async_synchronize_full();

    // 释放所有非__init内存
    free_initmem();

    // 标记系统运行
    mark_readonly();

    // 设置系统状态
    system_state = SYSTEM_RUNNING;

    // 初始化命名空间
    numa_default_policy();

    // 刷新延迟工作
    flush_delayed_fput();

    // 运行init进程
    rcu_end_inkernel_boot();

    if (ramdisk_execute_command) {
        ret = run_init_process(ramdisk_execute_command);
        if (!ret)
            return 0;
        pr_err("Failed to execute %s (error %d)\n",
               ramdisk_execute_command, ret);
    }

    // 尝试执行init程序
    if (execute_command) {
        ret = run_init_process(execute_command);
        if (!ret)
            return 0;
        panic("Requested init %s failed (error %d).",
              execute_command, ret);
    }

    // 尝试默认的init程序
    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. "
          "See Linux Documentation/admin-guide/init.rst for guidance.");
}

kernel_init()就像"工厂建成后的开业仪式",内核要把控制权交给用户空间:

最后的准备工作:

  1. kernel_init_freeable() :完成最后的初始化,"做最后的检查"
  2. async_synchronize_full() :等待所有异步任务完成,"确保所有工人都到位"
  3. free_initmem() :释放初始化内存,"拆除施工脚手架"
  4. mark_readonly() :标记系统为只读,"锁定重要设施"
  5. system_state = SYSTEM_RUNNING:宣布系统正式运行,"挂上'开业大吉'的牌子"

寻找第一个用户程序: 内核像"招聘经理"一样,按优先级寻找合适的init程序:

  1. 先看ramdisk:如果有ramdisk指定的程序,优先使用
  2. 再看命令行:如果启动时指定了init程序,就用它
  3. 最后试默认位置:按顺序尝试/sbin/init/etc/init/bin/init/bin/sh

找不到就崩溃: 如果所有尝试都失败,内核就panic:"没有合适的管理员,工厂无法运营!"

这就像建好工厂后,必须找到一个总经理来管理日常运营。内核的任务是建工厂,init进程的任务是运营工厂!

static int try_to_run_init_process(const char *init_filename)
{
    int ret;

    ret = run_init_process(init_filename);

    if (ret && ret != -ENOENT) {
        pr_err("Starting init: %s exists but couldn't execute it (error %d)\n",
               init_filename, ret);
    }

    return ret;
}

static int run_init_process(const char *init_filename)
{
    argv_init[0] = init_filename;
    return do_execve(getname_kernel(init_filename),
                     (const char __user *const __user *)argv_init,
                     (const char __user *const __user *)envp_init);
}

内核线程创建

内核创建各种内核线程:

// kernel/kthread.c
static __initdata DECLARE_COMPLETION(kthreadd_done);

static int __init kernel_init(void *unused)
{
    // 创建kthreadd内核线程
    pid_t 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();
    complete(&kthreadd_done);

    // 等待所有异步调用完成
    async_synchronize_full();

    // 创建工作队列
    workqueue_init();

    // 初始化RCU
    rcu_init_tasks_generic();

    return 0;
}

这个kernel_init函数就像"工厂的人事部门",负责招聘和管理所有的内核工作人员:

  1. kernel_thread(kthreadd, ...) :创建kthreadd线程,就像"招聘一个人事经理"
  2. find_task_by_pid_ns() :找到刚创建的线程,"确认人事经理已经到岗"
  3. complete(&kthreadd_done) :通知其他部门"人事经理已就位"
  4. async_synchronize_full() :等待所有异步任务完成,"确保所有准备工作都做完了"
  5. workqueue_init() :初始化工作队列,"建立任务分配系统"

这就像公司开业前的最后准备:先安排好人事经理,再建立完整的工作分配体系。

// kthreadd是所有内核线程的父线程
int kthreadd(void *unused)
{
    struct task_struct *tsk = current;

    // 设置任务名称
    set_task_comm(tsk, "kthreadd");

    // 忽略所有信号
    ignore_signals(tsk);

    // 设置NUMA节点
    set_cpus_allowed_ptr(tsk, cpu_all_mask);
    set_mems_allowed(node_states[N_MEMORY]);

    current->flags |= PF_NOFREEZE;
    cgroup_init_kthreadd();

    for (;;) {
        set_current_state(TASK_INTERRUPTIBLE);
        if (list_empty(&kthread_create_list))
            schedule();
        __set_current_state(TASK_RUNNING);

        spin_lock(&kthread_create_lock);
        while (!list_empty(&kthread_create_list)) {
            struct kthread_create_info *create;

            create = list_entry(kthread_create_list.next,
                               struct kthread_create_info, list);
            list_del_init(&create->list);
            spin_unlock(&kthread_create_lock);

            create_kthread(create);

            spin_lock(&kthread_create_lock);
        }
        spin_unlock(&kthread_create_lock);
    }

    return 0;
}

kthreadd就像内核世界的"人事经理",专门负责创建和管理所有内核线程:

初始化阶段:

  1. set_task_comm(tsk, "kthreadd") :给自己起名叫"kthreadd",就像挂上"人事经理"的名牌
  2. ignore_signals(tsk) :忽略所有信号,"专心工作,不受外界干扰"
  3. set_cpus_allowed_ptr() :可以在任何CPU上运行,"哪里需要就去哪里"
  4. current->flags |= PF_NOFREEZE:设置为不可冻结,"关键岗位,不能停工"

工作循环: 这是一个永不停止的循环,就像人事经理的日常工作:

  1. set_current_state(TASK_INTERRUPTIBLE) :进入可中断睡眠,"没事的时候就休息"
  2. if (list_empty(&kthread_create_list)) :检查是否有创建线程的请求,"看看有没有招聘需求"
  3. schedule() :如果没有工作就让出CPU,"没事就让别人先干"
  4. 处理创建请求:当有请求时,从列表中取出并处理,"有招聘需求就立即处理"
  5. create_kthread(create) :实际创建新线程,"按要求招聘新员工"

这就像一个专业的人事经理:平时待命,一旦有招聘需求就立即行动,确保内核需要的各种工作线程都能及时创建!

常见的内核线程:

  • ksoftirqd:处理软中断,"专门处理软性任务的工人"
  • migration:负载均衡,"负责调配工作量的协调员"
  • rcu_gp:RCU垃圾回收,"负责清理垃圾的清洁工"
  • watchdog:系统监控,"负责安全巡逻的保安"
  • kswapd:内存回收,"负责内存整理的管理员"

每个内核线程都有特定的职责,就像工厂里的不同工种,共同维护着整个系统的正常运行!


关键数据结构

task_struct - 进程描述符

进程描述符是Linux中最重要的数据结构之一:

task_struct就像每个进程的"身份证+档案袋",记录了进程的所有重要信息。想象每个进程都是公司里的员工,这个结构就是他们的完整人事档案。

// include/linux/sched.h
struct task_struct {
    // 进程状态
    volatile long state;

    // 进程标志
    unsigned int flags;

    // 进程优先级
    int prio, static_prio, normal_prio;

    // 调度实体
    struct sched_entity se;
    struct sched_rt_entity rt;
    struct sched_dl_entity dl;

    // 调度策略
    unsigned int policy;

    // CPU掩码
    cpumask_t cpus_allowed;

    // 内存管理
    struct mm_struct *mm, *active_mm;

    // 进程状态
    int exit_state;
    int exit_code, exit_signal;

    // 进程ID
    pid_t pid;
    pid_t tgid;

    // 父进程
    struct task_struct __rcu *real_parent;
    struct task_struct __rcu *parent;

    // 子进程链表
    struct list_head children;
    struct list_head sibling;

    // 进程组领导者
    struct task_struct *group_leader;

    // 线程组
    struct list_head thread_group;
    struct list_head thread_node;

    // 完成事件
    struct completion *vfork_done;

    // 用户空间地址
    int __user *set_child_tid;
    int __user *clear_child_tid;

    // 时间统计
    u64 utime, stime;
    u64 utimescaled, stimescaled;
    u64 gtime;
    struct prev_cputime prev_cputime;

    // 虚拟内存统计
    unsigned long nvcsw, nivcsw;
    u64 start_time;
    u64 real_start_time;

    // 内存管理统计
    unsigned long min_flt, maj_flt;

    // 任务名称
    char comm[TASK_COMM_LEN];

    // 文件系统信息
    struct fs_struct *fs;

    // 打开文件信息
    struct files_struct *files;

    // 命名空间
    struct nsproxy *nsproxy;

    // 信号处理
    struct signal_struct *signal;
    struct sighand_struct *sighand;
    sigset_t blocked, real_blocked;
    sigset_t saved_sigmask;
    struct sigpending pending;

    // 内核栈
    void *stack;

    // 原子上下文计数
    int pagefault_disabled;

    // 审计上下文
    struct audit_context *audit_context;

    // 安全上下文
    void *security;

    // 控制组
    struct css_set __rcu *cgroups;
    struct list_head cg_list;

    // 性能事件
    struct perf_event_context *perf_event_ctxp[perf_nr_task_contexts];
    struct mutex perf_event_mutex;
    struct list_head perf_event_list;

    // NUMA信息
    struct mempolicy *mempolicy;
    short il_next;
    short pref_node_fork;

    // RCU信息
    int rcu_read_lock_nesting;
    union rcu_special rcu_read_unlock_special;
    struct list_head rcu_node_entry;
    struct rcu_node *rcu_blocked_node;

    // 实时互斥锁
    struct rt_mutex_waiter *pi_blocked_on;

    // 跟踪信息
    unsigned long trace;
    unsigned long trace_recursion;

    // 内存回收
    struct reclaim_state *reclaim_state;

    // 后备设备信息
    struct backing_dev_info *backing_dev_info;

    // IO上下文
    struct io_context *io_context;

    // 插件
    struct plug *plug;

    // 延迟统计
    struct task_delay_info *delays;

    // 故障注入
    int make_it_fail;

    // 进程内存保护键
    u32 pkey_allocation_map;
    int pkey_disable_write;
};

task_struct详细讲解:

这个结构包含了进程的"全部家当",主要分为几大类:

身份信息:

  • pidtgid:进程ID,就像"身份证号码"
  • comm[TASK_COMM_LEN]:进程名称,就像"姓名"
  • state:当前状态(运行、睡眠、停止等),就像"当前在做什么"

调度相关:

  • priostatic_prionormal_prio:各种优先级,就像"工作等级"
  • sertdl:不同的调度实体,就像"不同类型的工作合同"
  • policy:调度策略,就像"工作方式"(全职、兼职、临时工等)

内存管理:

  • mmactive_mm:内存描述符,就像"住址和房产证"
  • stack:内核栈,就像"个人办公桌"

家庭关系:

  • parentreal_parent:父进程,就像"父母"
  • childrensibling:子进程和兄弟进程链表,就像"家庭成员名单"

资源管理:

  • files:打开的文件,就像"借阅的图书清单"
  • fs:文件系统信息,就像"工作目录"
  • signalsighand:信号处理,就像"通信方式"

这就像一个超级详细的员工档案,记录了员工的所有信息,让系统能够完美地管理每个进程!

mm_struct - 内存管理结构

内存管理描述符:

mm_struct就像进程的"房产证和地图",详细记录了进程拥有的所有内存区域。想象每个进程都有自己的"虚拟城市",这个结构就是这个城市的"城市规划图"。

// include/linux/mm_types.h
struct mm_struct {
    // VMA链表和红黑树
    struct vm_area_struct *mmap;
    struct rb_root mm_rb;
    u32 vmacache_seqnum;

    // 查找VMA的函数
    unsigned long (*get_unmapped_area) (struct file *filp,
                                       unsigned long addr,
                                       unsigned long len,
                                       unsigned long pgoff,
                                       unsigned long flags);

    // 内存映射基地址
    unsigned long mmap_base;
    unsigned long mmap_legacy_base;
    unsigned long task_size;

    // 页全局目录
    pgd_t * pgd;

    // 用户数和内存映射数
    atomic_t mm_users;
    atomic_t mm_count;

    // 内存映射数量
    int map_count;

    // 页表锁
    spinlock_t page_table_lock;
    struct rw_semaphore mmap_sem;

    // VMA链表
    struct list_head mmlist;

    // 堆和栈的边界
    unsigned long hiwater_rss;
    unsigned long hiwater_vm;
    unsigned long total_vm;
    unsigned long locked_vm;
    unsigned long pinned_vm;
    unsigned long shared_vm;
    unsigned long exec_vm;
    unsigned long stack_vm;
    unsigned long def_flags;

    // 代码段和数据段
    unsigned long start_code, end_code, start_data, end_data;
    unsigned long start_brk, brk, start_stack;
    unsigned long arg_start, arg_end, env_start, env_end;

    // RSS统计
    unsigned long rss_stat[NR_MM_COUNTERS];

    // 内存管理上下文
    mm_context_t context;

    // 标志位
    unsigned long flags;

    // 核心转储过滤器
    unsigned int core_state;

    // AIO信息
    spinlock_t ioctx_lock;
    struct kioctx_table __rcu *ioctx_table;

    // 异步IO
    struct task_struct *owner;

    // 用户命名空间
    struct user_namespace *user_ns;

    // 可执行文件
    struct file __rcu *exe_file;

    // 内存管理通知器
    struct mmu_notifier_mm *mmu_notifier_mm;

    // 透明大页
    pgtable_t pmd_huge_pte;

    // NUMA平衡
    struct cpumask cpumask_allocation;

    // 内存压缩
    unsigned long numa_next_scan;
    unsigned long numa_scan_offset;
    int numa_scan_seq;

    // 内存回收
    bool tlb_flush_pending;

    // 内存保护键
    u16 pkey_allocation_map;
    s16 execute_only_pkey;
};

mm_struct详细讲解:

这个结构就像进程的"房地产档案",记录了所有内存使用情况:

内存地图:

  • mmap:VMA链表头,就像"房产清单的第一页"
  • mm_rb:红黑树根节点,就像"按地址排序的房产索引"
  • get_unmapped_area:寻找空闲内存的函数,就像"房产中介"

地址空间布局:

  • mmap_base:内存映射基地址,就像"商业区的起始地址"
  • task_size:进程地址空间大小,就像"城市的总面积"
  • start_codeend_code:代码段范围,就像"办公区域"
  • start_dataend_data:数据段范围,就像"仓库区域"
  • start_brkbrk:堆的范围,就像"可扩展的工业区"
  • start_stack:栈的起始地址,就像"临时存储区"

统计信息:

  • total_vm:总虚拟内存,就像"城市总面积"
  • locked_vm:锁定内存,就像"不能出售的保护区"
  • rss_stat:物理内存使用统计,就像"实际占用的土地"

保护机制:

  • page_table_lock:页表锁,就像"房产证的保险柜"
  • mmap_sem:内存映射信号量,就像"房产交易的许可证"

特殊区域:

  • exe_file:可执行文件,就像"主要建筑物的蓝图"
  • context:内存管理上下文,就像"城市管理的具体规则"

整个结构就像一个完整的城市规划档案,记录了进程这个"虚拟城市"的所有细节!


总结

Linux启动过程是一个复杂而精密的过程,涉及从硬件初始化到用户空间服务启动的多个阶段。通过深入理解源码实现,我们可以:

  1. 掌握系统启动流程: 从BIOS/UEFI到init进程的完整过程
  2. 理解内核架构: 内核各子系统的初始化顺序和依赖关系
  3. 优化启动性能: 识别启动瓶颈并进行针对性优化
  4. 排查启动故障: 快速定位和解决启动问题
  5. 定制启动过程: 根据需求修改启动流程和参数

关键要点

  • 分阶段初始化: 内核采用分阶段初始化策略,确保依赖关系正确
  • 架构抽象: 通过架构抽象层支持多种硬件平台
  • 模块化设计: 各子系统相对独立,便于维护和扩展
  • 错误处理: 完善的错误处理机制确保系统稳定性
  • 性能优化: 多种优化技术提高启动速度和运行效率