Linux 系统启动流程详细解析

1. 系统启动概述

1.1 启动流程总览

Linux系统启动是一个多阶段的过程,从硬件上电到用户空间应用运行,经历了以下主要阶段:

scss 复制代码
硬件上电
    ↓
Bootloader (U-Boot/BootROM)
    ↓
内核解压 (如果是压缩内核)
    ↓
内核早期汇编初始化 (head.S)
    ↓
C语言初始化 (start_kernel)
    ↓
子系统初始化 (initcalls)
    ↓
Init进程启动
    ↓
用户空间应用启动

1.2 各阶段的作用

  1. 硬件上电: CPU从固定地址开始执行
  2. Bootloader: 初始化硬件,加载内核
  3. 内核解压: 解压压缩的内核镜像
  4. 汇编初始化: 设置CPU、MMU、页表等底层设施
  5. C语言初始化: 初始化内核各个子系统
  6. Init进程: 第一个用户空间进程,启动其他应用

1.3 关键文件位置

  • Bootloader : arch/arm/boot/compressed/head.S (压缩内核入口)
  • 内核入口 : arch/arm/kernel/head.S (内核主入口)
  • C语言入口 : init/main.c (start_kernel函数)
  • Init进程 : init/main.c (kernel_init函数)

2. 硬件上电阶段

2.1 上电复位

2.1.1 复位向量

当系统上电或复位时,CPU会从复位向量(Reset Vector)地址开始执行代码。

ARM架构的复位向量:

  • 地址 : 通常是 0x000000000xFFFF0000(取决于配置)
  • 内容: 一条跳转指令,跳转到Bootloader代码

2.1.2 复位后的CPU状态

复位后,CPU处于以下状态:

  1. MMU关闭: 内存管理单元未启用,使用物理地址
  2. Cache关闭: 缓存未启用
  3. 中断关闭: 所有中断被禁用
  4. 特权模式: 通常处于Supervisor模式(ARM)或类似特权模式
  5. 寄存器: 大部分寄存器未定义,需要初始化

2.1.3 为什么需要Bootloader?

CPU复位后只能执行简单的指令,无法直接:

  • 访问复杂的存储设备(如eMMC、NAND Flash)
  • 解析文件系统
  • 加载和验证内核镜像
  • 设置复杂的硬件配置

因此需要Bootloader作为中间层。

2.2 BootROM阶段

2.2.1 什么是BootROM?

BootROM是芯片内部固化的只读存储器,包含:

  • 芯片厂商提供的启动代码
  • 基本的硬件初始化代码
  • 从存储设备加载Bootloader的代码

2.2.2 BootROM的工作流程

markdown 复制代码
上电复位
    ↓
执行BootROM代码
    ↓
初始化基本硬件(时钟、DDR等)
    ↓
从存储设备读取Bootloader
    ↓
验证Bootloader(可选)
    ↓
跳转到Bootloader

2.2.3 BootROM的存储位置

  • 位置: 芯片内部ROM,不可修改
  • 大小: 通常几KB到几十KB
  • 功能: 最小化的启动代码

3. Bootloader阶段

3.1 Bootloader概述

3.1.1 什么是Bootloader?

Bootloader是系统启动的第一个软件层,负责:

  1. 硬件初始化: 初始化CPU、内存、存储、串口等
  2. 加载内核: 从存储设备读取内核镜像
  3. 传递参数: 向内核传递设备树、命令行参数等
  4. 启动内核: 跳转到内核入口地址

3.1.2 常见的Bootloader

  • U-Boot: 最常用的开源Bootloader
  • GRUB: 主要用于x86架构
  • RedBoot: 嵌入式系统常用
  • BootROM: 芯片厂商提供的固件

3.1.3 Bootloader的存储位置

  • NOR Flash: 可以直接执行(XIP)
  • NAND Flash: 需要加载到RAM执行
  • eMMC/SD卡: 需要加载到RAM执行
  • 网络: 通过网络加载(PXE、TFTP等)

3.2 U-Boot启动流程

3.2.1 U-Boot的阶段划分

U-Boot通常分为两个阶段:

Stage 1 (SPL - Secondary Program Loader):

  • 最小化的初始化
  • 初始化DDR内存
  • 加载完整的U-Boot到DDR

Stage 2 (Full U-Boot):

  • 完整的硬件初始化
  • 文件系统支持
  • 网络支持
  • 加载内核

3.2.2 U-Boot的主要任务

  1. 硬件初始化

    c 复制代码
    // 初始化时钟
    clock_init();
    
    // 初始化DDR
    ddr_init();
    
    // 初始化串口(用于调试输出)
    uart_init();
    
    // 初始化存储设备
    storage_init();
  2. 加载内核镜像

    c 复制代码
    // 从存储设备读取内核
    load_kernel_image();
    
    // 验证内核(可选)
    verify_kernel();
    
    // 解压内核(如果是压缩镜像)
    decompress_kernel();
  3. 准备启动参数

    c 复制代码
    // 准备设备树(Device Tree)
    setup_fdt();
    
    // 准备命令行参数
    setup_cmdline();
    
    // 准备ATAGs(旧式参数传递方式)
    setup_atags();
  4. 跳转到内核

    c 复制代码
    // 设置CPU状态
    // - 关闭MMU
    // - 关闭Cache
    // - 设置寄存器
    
    // 跳转到内核入口
    jump_to_kernel();

3.3 内核启动参数传递

3.3.1 参数传递方式

Linux内核支持两种参数传递方式:

1. ATAGs (ARM Tags) - 旧式方式

  • 用于ARM架构的传统方式
  • 通过内存中的数据结构传递
  • 逐步被设备树替代

2. Device Tree (设备树) - 现代方式

  • 描述硬件配置的树形结构
  • 更灵活,易于维护
  • 现代ARM系统的主流方式

3.3.2 ATAGs结构

c 复制代码
struct atag_header {
    u32 size;      // 整个tag的大小(以字为单位)
    u32 tag;       // tag类型
};

// 常见的ATAG类型
#define ATAG_CORE       0x54410001  // 核心tag
#define ATAG_MEM        0x54410002  // 内存信息
#define ATAG_CMDLINE    0x54410009  // 命令行参数
#define ATAG_NONE       0x00000000  // 结束标记

3.3.3 Device Tree结构

设备树是描述硬件配置的树形数据结构:

dts 复制代码
/dts-v1/;

/ {
    compatible = "allwinner,sunxi";
    
    memory@40000000 {
        device_type = "memory";
        reg = <0x40000000 0x20000000>;
    };
    
    cpus {
        cpu@0 {
            compatible = "arm,cortex-a7";
        };
    };
    
    uart0: serial@01c28000 {
        compatible = "allwinner,sunxi-uart";
        reg = <0x01c28000 0x400>;
    };
};

3.3.4 Bootloader传递给内核的寄存器

ARM架构下,Bootloader通过寄存器传递参数:

  • r0: 通常为0
  • r1: 机器类型ID(Machine Type ID)
  • r2: ATAGs或设备树的物理地址

这些寄存器值在内核启动时会被使用。


4. 内核解压阶段

4.1 压缩内核概述

4.1.1 为什么需要压缩?

内核镜像通常很大(几MB到几十MB),压缩可以:

  • 减少存储空间: 节省Flash空间
  • 加快加载速度: 减少从存储设备读取的时间
  • 节省内存: 解压前占用更少内存

4.1.2 压缩格式

Linux内核支持多种压缩格式:

  • gzip: 最常用,压缩率高
  • bzip2: 压缩率更高,但解压慢
  • lzma/xz: 压缩率最高,解压较慢
  • lzo: 压缩率低,但解压快

4.1.3 压缩内核的结构

scss 复制代码
[压缩内核头部]
    ↓
[解压代码] (arch/arm/boot/compressed/head.S)
    ↓
[压缩的内核数据]
    ↓
[解压后的内核大小] (可选)

4.2 解压代码入口

4.2.1 入口点位置

压缩内核的入口点在 arch/arm/boot/compressed/head.S:

assembly 复制代码
.section ".start", "ax"
.align
start:
    .type start,#function
    // 解压代码从这里开始

4.2.2 解压代码的主要任务

  1. 保存Bootloader传递的参数

    assembly 复制代码
    // 保存r0, r1, r2寄存器
    // r1 = 机器类型ID
    // r2 = ATAGs/DTB地址
  2. 设置栈指针

    assembly 复制代码
    // 需要栈来调用C函数
    ldr sp, =stack_base
  3. 检查是否需要解压

    assembly 复制代码
    // 检查内核是否已解压
    // 如果已解压,直接跳转
  4. 解压内核

    c 复制代码
    // 调用解压函数
    decompress_kernel();
  5. 跳转到解压后的内核

    assembly 复制代码
    // 跳转到内核入口
    b   stext  // 跳转到arch/arm/kernel/head.S

4.2.3 解压过程详解

解压代码的工作流程:

scss 复制代码
start (压缩内核入口)
    ↓
保存寄存器 (r0, r1, r2)
    ↓
设置栈指针
    ↓
检查内核是否已解压
    ├─ 已解压 → 直接跳转
    └─ 未解压 → 继续
    ↓
调用解压函数 (C代码)
    ↓
解压内核到目标地址
    ↓
验证解压结果
    ↓
跳转到内核入口 (stext)

4.2.4 解压函数实现

解压函数在 arch/arm/boot/compressed/decompress.c:

c 复制代码
void decompress_kernel(unsigned long output_start,
                       unsigned long free_mem_ptr_p,
                       unsigned long free_mem_ptr_end_p,
                       int arch_id)
{
    // 选择解压算法
    // 调用相应的解压函数
    // 解压内核到output_start
}

5. 内核早期汇编初始化

5.1 内核入口点

5.1.1 入口点位置

内核的汇编入口点在 arch/arm/kernel/head.S:

assembly 复制代码
__HEAD
ENTRY(stext)
    // 内核从这里开始执行
    // MMU关闭,使用物理地址

5.1.2 入口时的CPU状态

内核入口时,CPU处于以下状态:

  • MMU: 关闭
  • Cache: 关闭
  • 中断: 关闭
  • 寄存器 :
    • r0 = 0
    • r1 = 机器类型ID
    • r2 = ATAGs/DTB物理地址

5.1.3 为什么需要汇编初始化?

在启用MMU和Cache之前,必须使用汇编代码:

  • C编译器生成的代码依赖栈和全局变量
  • 栈和全局变量需要MMU映射
  • 因此需要汇编代码先设置MMU

5.2 处理器类型检测

5.2.1 检测目的

内核需要知道运行在哪种CPU上,以:

  • 选择正确的CPU特定代码
  • 设置CPU特定的寄存器
  • 启用CPU特定的功能

5.2.2 检测过程

assembly 复制代码
// 读取CPU ID
mrc p15, 0, r9, c0, c0    @ 读取处理器ID到r9

// 查找处理器类型
bl  __lookup_processor_type  @ 查找处理器信息

// 检查是否支持
movs r10, r5              @ r5是处理器信息指针
beq  __error_p             @ 如果不支持,报错

5.2.3 处理器信息结构

c 复制代码
struct proc_info_list {
    unsigned int    cpu_val;      // CPU ID值
    unsigned int    cpu_mask;     // CPU ID掩码
    unsigned long   __cpu_mm_mmu_flags;
    unsigned long   __cpu_io_mmu_flags;
    unsigned long   __cpu_flush;  // 缓存刷新函数
    const char      *arch_name;
    const char      *elf_name;
    unsigned int    elf_hwcap;
    const char      *cpu_name;
    struct processor *proc;
    struct cpu_tlb_fns *tlb;
    struct cpu_user_fns *user;
    struct cpu_cache_fns *cache;
};

5.3 ATAGs/DTB验证

5.3.1 验证目的

确保Bootloader传递的参数有效:

  • ATAGs指针有效
  • 或设备树指针有效
  • 数据结构格式正确

5.3.2 验证代码

assembly 复制代码
__vet_atags:
    // 检查对齐
    tst r2, #0x3           @ 检查是否4字节对齐
    bne 1f                 @ 不对齐则无效
    
    // 检查ATAG_CORE
    ldr r5, [r2, #0]       @ 读取第一个tag
    cmp r5, #ATAG_CORE_SIZE
    bne 1f                 @ 不是ATAG_CORE则无效
    
    // 检查ATAG_CORE标记
    ldr r5, [r2, #4]
    ldr r6, =ATAG_CORE
    cmp r5, r6
    bne 1f                 @ 标记不匹配则无效
    
    // 或者检查设备树
    ldr r6, =OF_DT_MAGIC
    cmp r5, r6
    beq 2f                 @ 是设备树则有效
    
1:  mov r2, #0             @ 无效,清空r2
2:  ret lr                 @ 返回

5.4 页表创建

5.4.1 为什么需要页表?

MMU需要页表来:

  • 将虚拟地址转换为物理地址
  • 设置内存访问权限
  • 启用内存保护

5.4.2 早期页表结构

早期页表只映射必要的区域:

assembly 复制代码
__create_page_tables:
    // 1. 清除页表
    mov r0, r4              @ r4是页表地址
    mov r3, #0
    add r6, r0, #PG_DIR_SIZE
1:  str r3, [r0], #4       @ 清零页表
    teq r0, r6
    bne 1b
    
    // 2. 映射内核代码段
    // 创建页表项,映射内核的.text段
    
    // 3. 映射页表自身
    // 创建页表项,映射页表区域

5.4.3 页表映射范围

早期页表通常映射:

  • 内核代码段 (.text): 可执行、只读
  • 内核数据段 (.data, .bss): 可读写
  • 页表区域: 可读写
  • 设备树/ATAGs: 只读

5.5 MMU启用

5.5.1 MMU启用的步骤

assembly 复制代码
__enable_mmu:
    // 1. 设置页表基址
    mov r4, #swapper_pg_dir  @ 页表地址
    mcr p15, 0, r4, c2, c0, 0 @ 写入TTBR0
    
    // 2. 设置域访问控制
    mov r5, #0x1fffffff      @ 所有域为客户端
    mcr p15, 0, r5, c3, c0, 0 @ 写入DACR
    
    // 3. 设置MMU控制寄存器
    mrc p15, 0, r0, c1, c0, 0 @ 读取SCTLR
    orr r0, r0, #0x1         @ 启用MMU
    mcr p15, 0, r0, c1, c0, 0 @ 写入SCTLR
    
    // 4. 跳转到虚拟地址
    b   __mmap_switched

5.5.2 MMU启用后的变化

启用MMU后:

  • 地址空间: 从物理地址切换到虚拟地址
  • 内存保护: 可以设置访问权限
  • Cache: 可以启用Cache加速

5.5.3 地址切换

assembly 复制代码
__mmap_switched:
    // 现在使用虚拟地址
    // 设置栈指针(虚拟地址)
    ldr sp, =init_thread_union + THREAD_START_SP
    
    // 保存处理器ID、机器类型、ATAGs地址
    str r9, [r0]            @ 保存处理器ID
    str r7, [r1]            @ 保存机器类型
    str r8, [r2]            @ 保存ATAGs地址
    
    // 跳转到C代码
    b   start_kernel

5.6 跳转到C代码

5.6.1 跳转前的准备

在跳转到C代码前,需要:

  1. 启用MMU: 已完成
  2. 设置栈指针: 已完成
  3. 清除BSS段: 需要完成
  4. 保存启动参数: 已完成

5.6.2 BSS段清除

BSS段包含未初始化的全局变量,需要清零:

assembly 复制代码
// 清除BSS段
ldr r0, =__bss_start
ldr r1, =__bss_stop
mov r2, #0
1:  cmp r0, r1
    strlt r2, [r0], #4
    blt  1b

5.6.3 跳转到start_kernel

assembly 复制代码
// 跳转到C代码入口
mov lr, #0
b   start_kernel

此时:

  • MMU已启用
  • 栈已设置
  • BSS已清除
  • 可以安全地调用C函数

6. C语言初始化 - start_kernel

6.1 start_kernel概述

6.1.1 start_kernel的作用

start_kernel是内核C语言代码的入口点,负责:

  • 初始化内核各个子系统
  • 设置内核运行环境
  • 启动第一个用户进程

6.1.2 start_kernel的位置

c 复制代码
// init/main.c
asmlinkage __visible void __init __no_sanitize_address start_kernel(void)
{
    // 内核初始化代码
}

6.1.3 start_kernel的执行环境

执行start_kernel时:

  • MMU: 已启用
  • 中断: 已禁用
  • 当前进程: init_task(内核的第一个任务)
  • : 使用init_task的栈

6.2 早期初始化

6.2.1 栈溢出检测设置

c 复制代码
set_task_stack_end_magic(&init_task);

作用: 在init_task栈的末尾设置魔数,用于检测栈溢出。

原理: 如果栈溢出,会覆盖栈末尾的魔数,内核可以检测到。

6.2.2 处理器ID设置

c 复制代码
smp_setup_processor_id();

作用: 设置当前CPU的ID。

用途: 用于SMP(多核)系统中识别当前CPU。

6.2.3 调试对象早期初始化

c 复制代码
debug_objects_early_init();

作用: 初始化调试对象跟踪系统。

用途: 用于检测内核对象的使用错误(如使用已释放的对象)。

6.2.4 构建ID初始化

c 复制代码
init_vmlinux_build_id();

作用: 初始化内核构建ID。

用途: 用于调试,标识内核版本和构建信息。

6.2.5 Cgroup早期初始化

c 复制代码
cgroup_init_early();

作用: 初始化控制组(cgroup)子系统。

用途: 用于资源限制和进程分组。

6.3 中断和锁初始化

6.3.1 禁用中断

c 复制代码
local_irq_disable();
early_boot_irqs_disabled = true;

作用: 确保在早期初始化期间中断被禁用。

原因: 早期初始化时,中断处理可能还未准备好。

6.3.2 引导CPU初始化

c 复制代码
boot_cpu_init();

作用: 初始化引导CPU(第一个CPU)。

任务:

  • 标记CPU为在线状态
  • 设置CPU的掩码
  • 初始化CPU相关的数据结构

6.3.3 页地址初始化

c 复制代码
page_address_init();

作用: 初始化页地址映射系统。

用途: 用于将物理页映射到内核地址空间。

6.4 架构相关初始化

6.4.1 setup_arch

c 复制代码
setup_arch(&command_line);

作用: 执行架构相关的初始化。

主要任务:

  1. 解析启动参数: 解析ATAGs或设备树
  2. 内存初始化: 初始化内存管理
  3. CPU初始化: 初始化CPU特定功能
  4. 设备树解析: 解析设备树,初始化设备
  5. 命令行参数: 提取内核命令行参数

代码位置 : arch/arm/kernel/setup.c

6.4.2 内存初始化

setup_arch中,会初始化内存:

c 复制代码
// 解析内存信息(从ATAGs或设备树)
setup_machine_fdt(__atags_pointer);
// 或
setup_machine_tags(__atags_pointer);

// 初始化memblock(早期内存分配器)
arm_memblock_init();

6.4.3 设备树解析

c 复制代码
// 解析设备树
unflatten_device_tree();

// 初始化设备
of_platform_populate();

6.5 命令行参数处理

6.5.1 保存命令行

c 复制代码
setup_command_line(command_line);

作用: 保存原始和解析后的命令行参数。

保存的内容:

  • saved_command_line: 原始命令行
  • static_command_line: 解析后的命令行

6.5.2 解析早期参数

c 复制代码
parse_early_param();

作用: 解析需要在早期处理的参数。

早期参数示例:

  • console=: 设置控制台
  • earlyprintk=: 早期打印
  • mem=: 内存大小
  • initrd=: initrd地址

6.5.3 解析常规参数

c 复制代码
parse_args("Booting kernel", static_command_line, ...);

作用: 解析所有内核参数。

参数类型:

  • 模块参数
  • 内核选项
  • 传递给init的参数

6.6 内存管理初始化

6.6.1 内存管理概述

内存管理是内核的核心子系统,需要早期初始化。

6.6.2 构建内存区域列表

c 复制代码
build_all_zonelists(NULL);

作用: 构建所有节点的内存区域列表。

内存区域类型:

  • ZONE_DMA: DMA可用内存
  • ZONE_NORMAL: 普通内存
  • ZONE_HIGHMEM: 高端内存(32位系统)

6.6.3 页分配器初始化

c 复制代码
page_alloc_init();

作用: 初始化页分配器。

页分配器: 负责分配和释放物理页。

6.6.4 完整内存管理初始化

c 复制代码
mm_init();

作用: 完整初始化内存管理子系统。

主要任务:

  1. 页扩展初始化 : page_ext_init_flatmem()
  2. 内存调试 : init_mem_debugging_and_hardening()
  3. KASAN初始化 : kfence_alloc_pool()(如果启用)
  4. 内存初始化报告 : report_meminit()
  5. 栈仓库初始化 : stack_depot_init()
  6. 内存初始化 : mem_init()
  7. SLAB分配器初始化 : kmem_cache_init()
  8. Kmemleak初始化 : kmemleak_init()
  9. 页表初始化 : pgtable_init()
  10. vmalloc初始化 : vmalloc_init()

6.7 调度器初始化

6.7.1 调度器概述

调度器负责决定哪个进程在CPU上运行。

6.7.2 调度器初始化

c 复制代码
sched_init();

作用: 初始化进程调度器。

主要任务:

  1. 初始化运行队列: 为每个CPU创建运行队列
  2. 初始化调度类: 初始化CFS、RT等调度类
  3. 初始化负载均衡: 初始化负载均衡器
  4. 设置调度域: 设置CPU调度域

代码位置 : kernel/sched/core.c

6.7.3 为什么需要早期初始化调度器?

调度器需要早期初始化,因为:

  • 后续的初始化可能创建内核线程
  • 内核线程需要调度器来运行
  • 多核系统需要调度器来分配任务

6.8 中断系统初始化

6.8.1 中断概述

中断是硬件通知CPU有事件发生的方式。

6.8.2 早期中断初始化

c 复制代码
early_irq_init();

作用: 早期初始化中断系统。

任务:

  • 初始化中断描述符
  • 设置中断向量表
  • 初始化中断控制器

6.8.3 完整中断初始化

c 复制代码
init_IRQ();

作用: 完整初始化中断系统。

任务:

  • 初始化所有中断
  • 设置中断处理函数
  • 启用中断控制器

6.8.4 中断处理流程

markdown 复制代码
硬件产生中断
    ↓
中断控制器通知CPU
    ↓
CPU跳转到中断向量
    ↓
调用中断处理函数
    ↓
执行中断服务例程
    ↓
返回被中断的代码

6.9 定时器初始化

6.9.1 定时器概述

定时器用于:

  • 时间测量
  • 延迟执行
  • 周期性任务

6.9.2 定时器初始化步骤

c 复制代码
tick_init();           // 初始化tick
init_timers();         // 初始化定时器
hrtimers_init();       // 初始化高精度定时器
timekeeping_init();    // 初始化时间保持
time_init();           // 架构相关时间初始化

6.9.3 时间系统

c 复制代码
timekeeping_init();

作用: 初始化内核时间系统。

时间系统包括:

  • 系统时间: 从1970年1月1日开始的秒数
  • 单调时间: 从启动开始的纳秒数
  • 时钟源: 硬件时钟源(如RTC、TSC)

6.10 控制台初始化

6.10.1 控制台概述

控制台用于内核输出信息,是调试和监控的重要工具。

6.10.2 控制台初始化

c 复制代码
console_init();

作用: 初始化控制台系统。

任务:

  1. 注册控制台驱动: 注册串口、VGA等控制台
  2. 设置默认控制台: 选择默认输出设备
  3. 启用控制台输出: 允许内核打印信息

6.10.3 控制台类型

  • 串口控制台: 通过UART输出
  • VGA控制台: 通过VGA显示
  • 网络控制台: 通过网络输出
  • USB控制台: 通过USB输出

6.11 Initcall机制

6.11.1 Initcall概述

Initcall是内核的初始化调用机制,用于按顺序初始化各个子系统。

6.11.2 Initcall级别

内核定义了多个initcall级别:

c 复制代码
// 定义在 include/linux/init.h
#define __define_initcall(fn, id) \
    static initcall_t __initcall_##fn##id __used \
    __attribute__((__section__(".initcall" #id ".init"))) = fn

// Initcall级别
#define pure_initcall(fn)           __define_initcall(fn, 0)  // 纯初始化
#define core_initcall(fn)           __define_initcall(fn, 1)  // 核心初始化
#define postcore_initcall(fn)       __define_initcall(fn, 2)  // 核心后初始化
#define arch_initcall(fn)           __define_initcall(fn, 3)  // 架构初始化
#define subsys_initcall(fn)         __define_initcall(fn, 4)  // 子系统初始化
#define fs_initcall(fn)             __define_initcall(fn, 5)  // 文件系统初始化
#define device_initcall(fn)         __define_initcall(fn, 6)  // 设备初始化
#define late_initcall(fn)           __define_initcall(fn, 7)  // 晚期初始化

6.11.3 Initcall执行顺序

c 复制代码
do_initcalls();

执行顺序:

  1. pure_initcall: 最基础的初始化
  2. core_initcall: 核心子系统
  3. postcore_initcall: 核心子系统之后
  4. arch_initcall: 架构相关
  5. subsys_initcall: 子系统
  6. fs_initcall: 文件系统
  7. device_initcall: 设备驱动
  8. late_initcall: 最后初始化

6.11.4 Initcall示例

c 复制代码
// 设备驱动初始化
static int __init my_driver_init(void)
{
    // 初始化代码
    return 0;
}
device_initcall(my_driver_init);  // 在device_initcall级别执行

6.12 基本设置

6.12.1 do_basic_setup

c 复制代码
do_basic_setup();

作用: 执行基本的系统设置。

包括:

  1. CPU集合初始化 : cpuset_init_smp()
  2. 驱动初始化 : driver_init()
  3. 中断proc文件 : init_irq_proc()
  4. 构造函数 : do_ctors()
  5. Initcalls : do_initcalls()

6.12.2 驱动初始化

c 复制代码
driver_init();

作用: 初始化驱动子系统。

任务:

  • 初始化设备模型
  • 初始化总线
  • 初始化设备类

6.13 准备启动用户空间

6.13.1 rest_init

c 复制代码
rest_init();

作用: 启动剩余的初始化工作。

主要任务:

  1. 启动init进程: 创建第一个用户进程
  2. 启动kthreadd: 创建内核线程守护进程
  3. 进入idle: 当前任务进入idle循环

6.13.2 启动init进程

c 复制代码
pid = kernel_thread(kernel_init, NULL, CLONE_FS);

作用: 创建init进程(PID 1)。

kernel_init: init进程的主函数,负责启动用户空间。

6.13.3 启动kthreadd

c 复制代码
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);

作用: 创建kthreadd进程(内核线程守护进程)。

kthreadd: 负责创建和管理内核线程。

6.13.4 进入idle

c 复制代码
schedule_preempt_disabled();
cpu_startup_entry(CPUHP_ONLINE);

作用: 当前任务(init_task)进入idle循环。

idle循环: 当没有其他任务运行时,CPU执行idle任务。


7. Init进程启动

7.1 kernel_init概述

7.1.1 kernel_init的作用

kernel_init是init进程的主函数,负责:

  • 完成内核初始化
  • 挂载根文件系统
  • 启动用户空间的init程序

7.1.2 kernel_init的位置

c 复制代码
// init/main.c
static int __ref kernel_init(void *unused)
{
    // init进程代码
}

7.1.3 kernel_init的执行环境

执行kernel_init时:

  • 进程: 新的内核线程,将成为init进程
  • PID: 1
  • 权限: 内核权限(稍后会切换到用户空间)

7.2 等待kthreadd就绪

7.2.1 为什么需要等待?

c 复制代码
wait_for_completion(&kthreadd_done);

原因: kthreadd需要先就绪,因为后续初始化可能需要创建内核线程。

机制: 使用完成量(completion)同步。

7.3 kernel_init_freeable

7.3.1 可释放初始化

c 复制代码
kernel_init_freeable();

作用: 执行可以释放初始化内存的初始化工作。

为什么叫"freeable": 这些初始化完成后,可以释放初始化时使用的内存。

7.3.2 主要任务

  1. 设置内存分配掩码

    c 复制代码
    gfp_allowed_mask = __GFP_BITS_MASK;

    允许使用所有内存分配标志。

  2. 设置内存节点

    c 复制代码
    set_mems_allowed(node_states[N_MEMORY]);

    允许在任意内存节点分配内存。

  3. SMP准备

    c 复制代码
    smp_prepare_cpus(setup_max_cpus);

    准备其他CPU。

  4. 工作队列初始化

    c 复制代码
    workqueue_init();

    初始化工作队列系统。

  5. 内存管理内部初始化

    c 复制代码
    init_mm_internals();

    初始化内存管理内部结构。

  6. 早期Initcalls

    c 复制代码
    do_pre_smp_initcalls();

    执行SMP之前的initcalls。

  7. 锁检测器初始化

    c 复制代码
    lockup_detector_init();

    初始化死锁检测器。

  8. SMP初始化

    c 复制代码
    smp_init();

    启动其他CPU。

  9. 调度器SMP初始化

    c 复制代码
    sched_init_smp();

    初始化SMP调度。

  10. 基本设置

    c 复制代码
    do_basic_setup();

    执行基本系统设置。

  11. 等待initramfs

    c 复制代码
    wait_for_initramfs();

    等待initramfs就绪(如果使用)。

  12. 控制台就绪

    c 复制代码
    console_on_rootfs();

    在根文件系统上打开控制台。

  13. 准备命名空间

    c 复制代码
    prepare_namespace();

    准备命名空间(挂载根文件系统等)。

7.4 根文件系统挂载

7.4.1 prepare_namespace

c 复制代码
prepare_namespace();

作用: 准备命名空间,主要是挂载根文件系统。

7.4.2 根文件系统挂载流程

scss 复制代码
prepare_namespace()
    ↓
挂载initramfs (如果使用)
    ↓
执行initramfs中的init (如果存在)
    ↓
挂载真正的根文件系统
    ↓
切换到根文件系统
    ↓
卸载initramfs (如果使用)

7.4.3 根文件系统类型

内核支持多种根文件系统:

  • ext2/ext3/ext4: 传统的Linux文件系统
  • squashfs: 压缩的只读文件系统
  • initramfs: 初始RAM文件系统
  • NFS: 网络文件系统
  • 其他: 根据内核配置支持的文件系统

7.4.4 根设备指定

根设备可以通过以下方式指定:

  1. 内核参数 : root=/dev/sda1
  2. UUID : root=UUID=xxx
  3. PARTUUID : root=PARTUUID=xxx
  4. 设备树: 设备树中指定

7.5 释放初始化内存

7.5.1 释放目的

初始化完成后,可以释放:

  • 初始化代码段
  • 初始化数据段
  • 不再需要的初始化数据结构

7.5.2 释放过程

c 复制代码
// 等待所有异步初始化完成
async_synchronize_full();

// 释放各种初始化内存
kprobe_free_init_mem();
ftrace_free_init_mem();
kgdb_free_init_mem();
exit_boot_config();
free_initmem();

7.5.3 标记只读

c 复制代码
mark_readonly();

作用: 将内核代码段标记为只读。

目的: 防止内核代码被修改,提高安全性。

7.6 启动用户空间init

7.6.1 启动顺序

init进程按以下顺序尝试启动用户空间init:

  1. ramdisk_execute_command : 通常是/init(initramfs中的init)
  2. execute_command : 内核参数init=指定的程序
  3. CONFIG_DEFAULT_INIT: 编译时指定的默认init
  4. 标准路径 :
    • /sbin/init
    • /etc/init
    • /bin/init
    • /bin/sh

7.6.2 启动代码

c 复制代码
// 1. 尝试ramdisk_execute_command
if (ramdisk_execute_command) {
    ret = run_init_process(ramdisk_execute_command);
    if (!ret)
        return 0;
}

// 2. 尝试execute_command
if (execute_command) {
    ret = run_init_process(execute_command);
    if (!ret)
        return 0;
    panic("Requested init %s failed", execute_command);
}

// 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;

// 5. 如果都失败,panic
panic("No working init found");

7.6.3 run_init_process

c 复制代码
static int run_init_process(const char *init_filename)
{
    argv_init[0] = init_filename;
    return kernel_execve(init_filename, argv_init, envp_init);
}

作用: 执行用户空间的init程序。

关键: 这是内核执行的最后一个函数,之后控制权交给用户空间。


8. 用户空间应用启动

8.1 Init程序概述

8.1.1 Init程序的作用

Init程序是用户空间的第一个进程(PID 1),负责:

  • 启动系统服务
  • 管理进程
  • 处理孤儿进程
  • 系统关闭

8.1.2 常见的Init程序

  • SysV init: 传统的init系统
  • systemd: 现代的init系统(大多数Linux发行版使用)
  • Upstart: Ubuntu曾经使用的init系统
  • BusyBox init: 嵌入式系统常用(本系统使用)
  • OpenRC: Gentoo等发行版使用
  • init.rc: Android风格的启动脚本(本系统使用)

8.2 Init.rc启动机制(本系统使用)

8.2.1 Init.rc概述

本系统使用init.rc作为主要的启动脚本机制,这是一种类似Android的启动脚本系统,结合了OpenWrt的procd进程管理。

8.2.2 Init.rc的特点

  1. Android风格语法: 使用类似Android的init.rc语法
  2. 服务定义: 可以定义系统服务
  3. 触发器: 支持on init、on boot、on startup等触发器
  4. 进程管理: 使用procd进行进程管理和监控
  5. 依赖管理: 支持服务依赖关系

8.2.3 Init.rc文件位置

系统中存在多个init.rc相关文件:

  • /etc/init.rc: 主要的init.rc配置文件
  • /etc/init.d/init.rc: init.rc服务的启动脚本
  • /etc/rc.d/S15init.rc: 启动时的符号链接
  • /etc/rc.d/K15init.rc: 关闭时的符号链接

8.2.4 Init.rc语法

触发器(Trigger)

触发器定义了在特定时机执行的命令:

rc 复制代码
on init
    import /etc/cgroup.rc

on boot
    class_start default
    import /oem/node/nardds/conf/init/nardds.rc

on startup
    exec /usr/bin/startup_app.sh &

常见的触发器:

  • on init: 系统初始化时执行
  • on boot: 系统启动时执行
  • on startup: 系统就绪后执行
  • on property: 属性变化时执行
  • on fs: 文件系统挂载时执行
服务定义(Service)

服务定义了系统服务的启动方式:

rc 复制代码
service toplog /oem/script/utils/dump_top_log.sh
    oneshot
    disabled

service audio_server /oem/script/utils/audio_server
    retry 10
    disabled

service dhcpcd /sbin/dhcpcd -f /etc/dhcpcd.conf
    suc_oneshot
    retry 10
    disabled

服务属性:

  • oneshot: 服务只执行一次,不持续运行
  • suc_oneshot: 成功执行一次后退出
  • retry: 失败后重试次数
  • disabled: 默认不启动,需要手动启动
  • class: 服务所属的类
  • user: 运行服务的用户

8.2.5 Init.rc启动流程

完整的启动流程
csharp 复制代码
内核启动init进程
    ↓
读取/etc/inittab
    ↓
执行/etc/init.d/rcS S boot
    ↓
执行/etc/rc.d/下的启动脚本(按START顺序)
    ├─ S10boot (START=10)
    │   └─ 挂载文件系统、初始化硬件
    ├─ S12syslogd (START=12)
    │   └─ 启动系统日志服务
    ├─ S15init.rc (START=15)
    │   └─ 启动init.rc服务
    │       └─ 执行/usr/bin/init.rc
    │           └─ 解析/etc/init.rc
    │               ├─ 执行on init触发器
    │               ├─ 执行on boot触发器
    │               └─ 启动定义的服务
    └─ S80auto_startup (START=80)
        └─ 启动自动启动的应用
Inittab配置

/etc/inittab文件定义了init进程的行为:

ruby 复制代码
::sysinit:/etc/init.d/rcS S boot
::shutdown:/etc/init.d/rcS K shutdown
::askconsole:/bin/ash --login

说明:

  • sysinit : 系统初始化时执行/etc/init.d/rcS S boot
  • shutdown : 系统关闭时执行/etc/init.d/rcS K shutdown
  • askconsole : 请求控制台时执行/bin/ash --login
RcS脚本

/etc/init.d/rcS脚本负责执行启动和关闭脚本:

bash 复制代码
# 启动时执行
/etc/init.d/rcS S boot
    ↓
遍历/etc/rc.d/S*脚本
    ↓
按START值排序
    ↓
依次执行start函数

启动脚本命名规则:

  • S##name: 启动脚本,##是START值(两位数字)
  • K##name: 关闭脚本,##是STOP值(两位数字)

8.2.6 Init.rc服务管理

Procd进程管理

Init.rc服务使用procd(Process Daemon)进行管理:

bash 复制代码
# /etc/init.d/init.rc
start_service() {
    if [ -x "/usr/bin/$INITRC" ]; then
        procd_open_instance
        procd_set_param oom_adj $OOM_ADJ
        procd_set_param command /bin/sh -c "source /opt/ros/kinetic/setup.sh && export PYTHONPATH=/oem/lib/python3.10/dist-packages/:\$PYTHONPATH && exec /usr/bin/$INITRC"
        procd_close_instance
    fi
}

Procd的作用:

  • 进程监控: 监控服务进程,崩溃时自动重启
  • 资源限制: 设置OOM调整值,防止服务被OOM killer杀死
  • 环境设置: 设置服务运行环境
  • 日志管理: 管理服务的标准输出和错误输出
服务启动顺序

服务通过STARTDEPEND参数控制启动顺序:

bash 复制代码
# /etc/init.d/init.rc
START=15      # 启动顺序(数字越小越早启动)
STOP=15       # 停止顺序
DEPEND=boot   # 依赖boot服务
USE_PROCD=1   # 使用procd管理

依赖关系:

  • DEPEND=boot: 必须在boot服务启动后才能启动
  • DEPEND=init.rc: 必须在init.rc服务启动后才能启动

8.2.7 Boot脚本详解

Boot脚本(/etc/init.d/boot)是系统启动的基础脚本,负责:

1. 创建必要的目录
bash 复制代码
mkdir -p /var/run
mkdir -p /var/log
mkdir -p /var/lock
mkdir -p /var/state
mkdir -p /var/tmp
mkdir -p /tmp/.uci
2. 挂载虚拟文件系统
bash 复制代码
# 挂载debugfs(调试文件系统)
grep -q debugfs /proc/filesystems && /bin/mount -o noatime -t debugfs debugfs /sys/kernel/debug

# 挂载bpf文件系统
grep -q bpf /proc/filesystems && /bin/mount -o nosuid,nodev,noexec,noatime,mode=0700 -t bpf bpffs /sys/fs/bpf

# 挂载pstore(持久化存储)
grep -q pstore /proc/filesystems && /bin/mount -o noatime -t pstore pstore /sys/fs/pstore
3. 创建设备链接
bash 复制代码
link_by_name

作用 : 根据内核命令行参数创建/dev/by-name/下的设备链接,方便通过名称访问分区。

4. 加载内核模块
bash 复制代码
/sbin/kmodloader

作用: 加载需要的内核模块。

5. 初始化设备管理
bash 复制代码
echo /sbin/mdev > /proc/sys/kernel/hotplug
/sbin/mdev -s

作用: 使用mdev进行设备管理,创建设备节点。

6. 挂载文件系统
bash 复制代码
# 挂载robotdata分区
mount_robotdata

# 根据是否加密选择挂载方式
if grep -q "crypt" /proc/cmdline; then
    mount_data_enc      # 加密方式挂载data
    mount_oem_enc       # 加密方式挂载oem
    mapper_oem_update   # 准备oem更新分区
else
    mount_data_normal   # 普通方式挂载data
    mount_oem_normal    # 普通方式挂载oem
fi

# 挂载userdata分区
mount_userdata
7. 文件系统挂载详解

Robotdata分区:

  • 用途: 存储机器人数据
  • 挂载点 : /robotdata
  • 挂载选项 : ro,discard,sync,data=journal(只读,除非工厂模式)

Data分区:

  • 用途: 存储应用数据
  • 挂载点 : /data
  • 支持加密: 可以通过dm-crypt加密

OEM分区:

  • 用途: 存储OEM相关文件
  • 挂载点 : /oem
  • 支持A/B分区: 支持oemA和oemB两个分区,用于OTA更新

Userdata分区:

  • 用途: 存储用户数据
  • 挂载点 : /userdata
  • 自动清理: 当使用率超过90%时自动清理

8.2.8 Init.rc服务启动

Init.rc服务的启动脚本

/etc/init.d/init.rc脚本负责启动init.rc服务:

bash 复制代码
#!/bin/sh /etc/rc.common
START=15
STOP=15
DEPEND=boot
USE_PROCD=1

INITRC="init.rc"
OOM_ADJ=-17

start_service() {
    if [ -x "/usr/bin/$INITRC" ]; then
        echo -n "Starting : $INITRC" > /dev/kmsg
        procd_open_instance
        procd_set_param oom_adj $OOM_ADJ
        procd_set_param command /bin/sh -c "source /opt/ros/kinetic/setup.sh && export PYTHONPATH=/oem/lib/python3.10/dist-packages/:\$PYTHONPATH && exec /usr/bin/$INITRC"
        procd_close_instance
    fi
}

关键点:

  1. OOM_ADJ=-17: 设置OOM调整值为-17,防止被OOM killer杀死
  2. ROS环境: 加载ROS(Robot Operating System)环境
  3. Python路径: 设置Python模块搜索路径
  4. 执行init.rc : 执行/usr/bin/init.rc程序
Init.rc程序的工作

/usr/bin/init.rc程序(通常是Android的init程序或兼容实现)会:

  1. 解析init.rc文件 : 读取/etc/init.rc并解析
  2. 执行触发器: 按顺序执行on init、on boot等触发器
  3. 启动服务: 启动定义的服务
  4. 监控服务: 监控服务状态,崩溃时重启
  5. 处理属性: 处理系统属性的变化

8.2.9 自动启动应用

Auto_startup服务

/etc/init.d/auto_startup服务负责启动自动启动的应用:

bash 复制代码
#!/bin/sh /etc/rc.common
START=80
STOP=99
DEPEND=init.rc
USE_PROCD=1
PROG=/etc/init.d/app_start

start_service() {
    procd_open_instance
    procd_set_param command $PROG start
    procd_close_instance
}

启动顺序:

  • START=80: 在init.rc之后启动
  • DEPEND=init.rc: 依赖init.rc服务
  • 执行 : /etc/init.d/app_start start
App_start脚本

/etc/init.d/app_start脚本负责启动具体的应用:

bash 复制代码
# 通常包含:
# 1. 检查应用是否存在
# 2. 设置应用环境
# 3. 启动应用进程
# 4. 监控应用状态

8.2.10 Init.rc的优势

  1. 灵活性: 可以定义复杂的启动逻辑
  2. 服务管理: 自动监控和重启服务
  3. 依赖管理: 支持服务依赖关系
  4. 触发器: 支持多种启动时机
  5. 资源控制: 可以设置资源限制

8.2.11 Init.rc调试

查看服务状态
bash 复制代码
# 查看所有服务状态
/etc/init.d/<service> status

# 查看init.rc服务状态
/etc/init.d/init.rc status
手动控制服务
bash 复制代码
# 启动服务
/etc/init.d/<service> start

# 停止服务
/etc/init.d/<service> stop

# 重启服务
/etc/init.d/<service> restart

# 重新加载配置
/etc/init.d/<service> reload
查看日志
bash 复制代码
# 查看内核日志
dmesg

# 查看系统日志
logread

# 查看特定服务的日志
/etc/init.d/<service> log

8.2.12 Init.rc与Systemd的对比

特性 Init.rc Systemd
语法 Android风格 INI风格
进程管理 procd systemd
服务定义 service关键字 [Service]段
触发器 on init/boot等 target和依赖
适用场景 嵌入式、OpenWrt 桌面、服务器
资源占用 较小 较大
功能丰富度 中等 非常丰富

8.2.13 Init.rc配置示例

完整的Init.rc配置示例
rc 复制代码
# /etc/init.rc

# 初始化阶段
on init
    # 导入cgroup配置
    import /etc/cgroup.rc
    
    # 设置环境变量
    export PATH /usr/bin:/usr/sbin:/bin:/sbin

# 启动阶段
on boot
    # 启动默认类
    class_start default
    
    # 导入其他配置
    import /oem/node/nardds/conf/init/nardds.rc

# 启动后阶段
on startup
    # 后台启动启动脚本
    exec /usr/bin/startup_app.sh &

# 定义服务
service toplog /oem/script/utils/dump_top_log.sh
    oneshot
    disabled

service audio_server /oem/script/utils/audio_server
    retry 10
    disabled

service dhcpcd /sbin/dhcpcd -f /etc/dhcpcd.conf
    suc_oneshot
    retry 10
    disabled
服务定义详解
rc 复制代码
service <name> <path> [<argument>]*
    <option>
    <option>
    ...

选项说明:

  • oneshot: 服务只执行一次
  • suc_oneshot: 成功执行一次后退出
  • retry: 失败后重试N次
  • disabled: 默认不启动
  • class: 指定服务类
  • user: 指定运行用户
  • group: 指定运行组
  • seclabel: 指定SELinux标签

8.3 Systemd启动流程(参考)

8.3.1 Systemd概述

Systemd是现代Linux系统最常用的init系统,但本系统使用的是init.rc机制。

8.3.2 Systemd启动阶段

Systemd定义了多个启动目标(target):

  1. emergency.target: 紧急模式
  2. rescue.target: 救援模式
  3. multi-user.target: 多用户模式(无图形界面)
  4. graphical.target: 图形界面模式

8.3.3 Systemd单元类型

  • service: 系统服务
  • target: 启动目标(类似运行级别)
  • mount: 挂载点
  • socket: 套接字
  • timer: 定时器

8.3.4 Systemd启动顺序

markdown 复制代码
systemd启动
    ↓
加载单元文件
    ↓
解析依赖关系
    ↓
按依赖顺序启动服务
    ↓
达到默认target
    ↓
系统就绪

8.4 系统服务启动

8.4.1 服务启动顺序

在本系统中,服务按以下顺序启动:

  1. Boot服务 (START=10): 挂载文件系统、初始化硬件
  2. 系统服务 (START=12-13): syslogd、klogd等日志服务
  3. Init.rc服务 (START=15): 启动init.rc,执行init.rc脚本
  4. 应用服务 (START=80): 自动启动的应用

8.4.2 服务依赖

服务通过DEPEND参数定义依赖关系:

bash 复制代码
# /etc/init.d/init.rc
DEPEND=boot  # 依赖boot服务

# /etc/init.d/auto_startup
DEPEND=init.rc  # 依赖init.rc服务

依赖机制:

  • 被依赖的服务必须先启动
  • 如果依赖的服务启动失败,当前服务不会启动
  • 依赖关系通过procd管理

8.4.3 服务启动控制

服务通过START值控制启动顺序:

bash 复制代码
# START值越小,启动越早
S10boot      # START=10,最早启动
S12syslogd   # START=12
S15init.rc   # START=15
S80auto_startup  # START=80,较晚启动

8.5 登录和用户会话

8.5.1 登录管理器

在本系统中,通常使用:

  • getty: 文本终端登录(嵌入式系统常用)
  • 串口登录: 通过串口进行登录
  • SSH登录: 通过网络SSH登录

8.5.2 用户会话启动

用户登录后,启动用户会话:

  1. 加载用户配置 : 加载.bashrc.profile
  2. 设置环境变量: 设置PATH、ROS环境等
  3. 启动应用: 根据配置启动应用

8.6 启动脚本

8.6.1 启动脚本位置

在本系统中,启动脚本位于:

  • /etc/init.d/: 实际的启动脚本
  • /etc/rc.d/: 启动时的符号链接(S开头)和关闭时的符号链接(K开头)
  • /etc/init.rc: init.rc主配置文件
  • /oem/: OEM特定的启动脚本

8.6.2 启动脚本执行流程

bash 复制代码
系统启动
    ↓
读取/etc/inittab
    ↓
执行/etc/init.d/rcS S boot
    ↓
遍历/etc/rc.d/S*脚本
    ↓
按START值排序
    ↓
依次执行start函数
    ↓
所有服务启动完成

8.6.3 启动脚本结构

每个启动脚本都遵循相同的结构:

bash 复制代码
#!/bin/sh /etc/rc.common

START=10      # 启动顺序
STOP=98       # 停止顺序
DEPEND=boot   # 依赖关系
USE_PROCD=1   # 使用procd管理

start_service() {
    # 启动服务的代码
}

stop_service() {
    # 停止服务的代码
}

reload_service() {
    # 重新加载配置的代码
}

8.6.4 脚本执行顺序示例

以本系统为例:

markdown 复制代码
1. S10boot (START=10)
   - 挂载文件系统
   - 初始化硬件
   - 加载内核模块

2. S12syslogd (START=12)
   - 启动系统日志服务

3. S13klogd (START=13)
   - 启动内核日志服务

4. S15init.rc (START=15, DEPEND=boot)
   - 启动init.rc服务
   - 解析/etc/init.rc
   - 执行触发器
   - 启动定义的服务

5. S80auto_startup (START=80, DEPEND=init.rc)
   - 启动自动启动的应用

9. 启动流程总结

9.1 完整启动时间线

9.1.1 本系统(使用init.rc)的启动时间线

csharp 复制代码
时间    阶段                    关键操作
─────────────────────────────────────────────
0ms     硬件上电                 CPU复位
1ms     BootROM                执行芯片固件
10ms    Bootloader (U-Boot)    初始化硬件,加载内核
100ms   内核解压                解压内核镜像
150ms   汇编初始化              设置MMU、页表
200ms   start_kernel            初始化内核子系统
500ms   kernel_init             准备用户空间
600ms   BusyBox init            读取/etc/inittab
650ms   rcS脚本                 执行/etc/init.d/rcS S boot
700ms   Boot服务 (S10boot)       挂载文件系统、初始化硬件
800ms   日志服务 (S12-S13)       启动syslogd、klogd
900ms   Init.rc服务 (S15)       启动init.rc,解析/etc/init.rc
1000ms  Init.rc触发器           执行on init、on boot触发器
1100ms  Init.rc服务             启动定义的服务
1200ms  自动启动 (S80)          启动自动启动的应用
1500ms  系统就绪                系统完全启动

9.1.2 通用Linux系统(使用systemd)的启动时间线

csharp 复制代码
时间    阶段                    关键操作
─────────────────────────────────────────────
0ms     硬件上电                 CPU复位
1ms     BootROM                执行芯片固件
10ms    Bootloader              初始化硬件,加载内核
100ms   内核解压                解压内核镜像
150ms   汇编初始化              设置MMU、页表
200ms   start_kernel            初始化内核子系统
500ms   kernel_init             准备用户空间
600ms   用户空间init            启动systemd
1000ms  系统服务                启动各种服务
2000ms  登录界面                显示登录界面
3000ms  用户登录                用户登录
3500ms  用户会话                启动桌面环境
4000ms  系统就绪                系统完全启动

9.2 关键里程碑

9.2.1 内核阶段里程碑

  1. MMU启用: 内核可以使用虚拟地址
  2. 调度器就绪: 可以运行多个任务
  3. 中断启用: 可以响应硬件事件
  4. 根文件系统挂载: 可以访问文件系统

9.2.2 用户空间阶段里程碑(本系统)

  1. Init进程启动: 用户空间开始运行
  2. Boot服务完成: 文件系统挂载完成,硬件初始化完成
  3. Init.rc服务启动: init.rc解析和执行
  4. Init.rc触发器执行: on init、on boot等触发器执行
  5. 系统服务启动: 所有定义的服务启动
  6. 自动启动应用: 应用自动启动
  7. 系统就绪: 系统完全启动,可以交互

9.2.3 通用系统里程碑

  1. Init进程启动: 用户空间开始运行
  2. Systemd启动: systemd进程启动
  3. 系统服务启动: 系统功能就绪
  4. 用户登录: 用户可以交互

9.3 启动优化

9.3.1 内核启动优化

  • 减少initcalls: 移除不必要的初始化
  • 并行初始化: 并行执行没有依赖的初始化
  • 延迟初始化: 延迟非关键初始化
  • 压缩内核: 使用压缩内核减少加载时间

9.3.2 用户空间启动优化(本系统)

  • 优化Boot脚本: 减少文件系统检查和挂载时间
  • 减少启动服务: 只启动必要的服务
  • 优化Init.rc: 减少不必要的触发器和服务
  • 并行启动: 利用procd的并行启动能力
  • 延迟启动: 将非关键服务标记为disabled,需要时再启动
  • 优化依赖关系: 合理设置DEPEND,避免不必要的等待

9.3.3 通用系统启动优化

  • 并行启动服务: 并行启动没有依赖的服务
  • 延迟启动: 延迟非关键服务
  • 减少启动服务: 只启动必要的服务
  • 优化启动脚本: 优化启动脚本执行时间

9.4 启动问题排查

9.4.1 常见问题

  1. 内核panic: 检查内核日志
  2. 根文件系统挂载失败: 检查根设备配置
  3. Init进程无法启动: 检查init程序路径
  4. 服务启动失败: 检查服务配置和依赖

9.4.2 调试方法

  1. 内核日志 : dmesg查看内核日志
  2. 启动日志: 查看systemd日志
  3. 单用户模式: 进入单用户模式排查
  4. initramfs调试: 在initramfs中添加调试工具

10. 附录

10.1 关键数据结构

10.1.1 init_task

c 复制代码
struct task_struct init_task = {
    .pid = 0,
    .comm = "swapper",
    // ... 其他字段
};

内核的第一个任务,用于早期初始化。

10.1.2 启动参数

c 复制代码
char boot_command_line[COMMAND_LINE_SIZE];  // 启动命令行
char *saved_command_line;                   // 保存的命令行

保存内核启动参数。

10.2 关键函数调用链

bash 复制代码
硬件上电
    ↓
BootROM
    ↓
U-Boot
    ↓
arch/arm/boot/compressed/head.S:start
    ↓
解压内核
    ↓
arch/arm/kernel/head.S:stext
    ↓
arch/arm/kernel/head-common.S:__mmap_switched
    ↓
init/main.c:start_kernel
    ↓
init/main.c:rest_init
    ↓
init/main.c:kernel_init
    ↓
用户空间init程序

10.3 参考文档

  • Linux内核源码: linux-5.15-origin/
  • 内核文档: Documentation/
  • U-Boot文档: U-Boot官方文档
  • Systemd文档: systemd官方文档

相关推荐
Shawn_CH7 小时前
Linux top、mpstat、htop 原理详解
嵌入式
俊俊谢7 小时前
华大HC32F460配置JTAG调试引脚为普通GPIO(PB03、PA15等)
嵌入式硬件·嵌入式·arm·嵌入式软件·hc32f460
Shawn_CH1 天前
epoll_wait 及相关函数原理详解
嵌入式
Shawn_CH1 天前
Linux 进程冻结机制原理详解
嵌入式
黑客思维者2 天前
XGW-9000系列高端新能源电站边缘网关硬件架构设计
网络·架构·硬件架构·嵌入式·新能源·计算机硬件·电站
神圣的大喵3 天前
平台无关的嵌入式通用按键管理器
c语言·单片机·嵌入式硬件·嵌入式·按键库
网易独家音乐人Mike Zhou3 天前
【嵌入式模块芯片开发】LP87524电源PMIC芯片配置流程,给雷达供电的延时上电时序及API函数
c语言·stm32·单片机·51单片机·嵌入式·电源·毫米波雷达
Nerd Nirvana3 天前
WSL——Windows Subsystem for Linux流程一览
linux·运维·服务器·windows·嵌入式·wsl·wsl2
rechol3 天前
mcu启动流程
stm32·单片机·mcu·嵌入式