1. 系统启动概述
1.1 启动流程总览
Linux系统启动是一个多阶段的过程,从硬件上电到用户空间应用运行,经历了以下主要阶段:
scss
硬件上电
↓
Bootloader (U-Boot/BootROM)
↓
内核解压 (如果是压缩内核)
↓
内核早期汇编初始化 (head.S)
↓
C语言初始化 (start_kernel)
↓
子系统初始化 (initcalls)
↓
Init进程启动
↓
用户空间应用启动
1.2 各阶段的作用
- 硬件上电: CPU从固定地址开始执行
- Bootloader: 初始化硬件,加载内核
- 内核解压: 解压压缩的内核镜像
- 汇编初始化: 设置CPU、MMU、页表等底层设施
- C语言初始化: 初始化内核各个子系统
- 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架构的复位向量:
- 地址 : 通常是
0x00000000或0xFFFF0000(取决于配置) - 内容: 一条跳转指令,跳转到Bootloader代码
2.1.2 复位后的CPU状态
复位后,CPU处于以下状态:
- MMU关闭: 内存管理单元未启用,使用物理地址
- Cache关闭: 缓存未启用
- 中断关闭: 所有中断被禁用
- 特权模式: 通常处于Supervisor模式(ARM)或类似特权模式
- 寄存器: 大部分寄存器未定义,需要初始化
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是系统启动的第一个软件层,负责:
- 硬件初始化: 初始化CPU、内存、存储、串口等
- 加载内核: 从存储设备读取内核镜像
- 传递参数: 向内核传递设备树、命令行参数等
- 启动内核: 跳转到内核入口地址
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的主要任务
-
硬件初始化
c// 初始化时钟 clock_init(); // 初始化DDR ddr_init(); // 初始化串口(用于调试输出) uart_init(); // 初始化存储设备 storage_init(); -
加载内核镜像
c// 从存储设备读取内核 load_kernel_image(); // 验证内核(可选) verify_kernel(); // 解压内核(如果是压缩镜像) decompress_kernel(); -
准备启动参数
c// 准备设备树(Device Tree) setup_fdt(); // 准备命令行参数 setup_cmdline(); // 准备ATAGs(旧式参数传递方式) setup_atags(); -
跳转到内核
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 解压代码的主要任务
-
保存Bootloader传递的参数
assembly// 保存r0, r1, r2寄存器 // r1 = 机器类型ID // r2 = ATAGs/DTB地址 -
设置栈指针
assembly// 需要栈来调用C函数 ldr sp, =stack_base -
检查是否需要解压
assembly// 检查内核是否已解压 // 如果已解压,直接跳转 -
解压内核
c// 调用解压函数 decompress_kernel(); -
跳转到解压后的内核
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代码前,需要:
- 启用MMU: 已完成
- 设置栈指针: 已完成
- 清除BSS段: 需要完成
- 保存启动参数: 已完成
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);
作用: 执行架构相关的初始化。
主要任务:
- 解析启动参数: 解析ATAGs或设备树
- 内存初始化: 初始化内存管理
- CPU初始化: 初始化CPU特定功能
- 设备树解析: 解析设备树,初始化设备
- 命令行参数: 提取内核命令行参数
代码位置 : 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();
作用: 完整初始化内存管理子系统。
主要任务:
- 页扩展初始化 :
page_ext_init_flatmem() - 内存调试 :
init_mem_debugging_and_hardening() - KASAN初始化 :
kfence_alloc_pool()(如果启用) - 内存初始化报告 :
report_meminit() - 栈仓库初始化 :
stack_depot_init() - 内存初始化 :
mem_init() - SLAB分配器初始化 :
kmem_cache_init() - Kmemleak初始化 :
kmemleak_init() - 页表初始化 :
pgtable_init() - vmalloc初始化 :
vmalloc_init()
6.7 调度器初始化
6.7.1 调度器概述
调度器负责决定哪个进程在CPU上运行。
6.7.2 调度器初始化
c
sched_init();
作用: 初始化进程调度器。
主要任务:
- 初始化运行队列: 为每个CPU创建运行队列
- 初始化调度类: 初始化CFS、RT等调度类
- 初始化负载均衡: 初始化负载均衡器
- 设置调度域: 设置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();
作用: 初始化控制台系统。
任务:
- 注册控制台驱动: 注册串口、VGA等控制台
- 设置默认控制台: 选择默认输出设备
- 启用控制台输出: 允许内核打印信息
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();
执行顺序:
- pure_initcall: 最基础的初始化
- core_initcall: 核心子系统
- postcore_initcall: 核心子系统之后
- arch_initcall: 架构相关
- subsys_initcall: 子系统
- fs_initcall: 文件系统
- device_initcall: 设备驱动
- 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();
作用: 执行基本的系统设置。
包括:
- CPU集合初始化 :
cpuset_init_smp() - 驱动初始化 :
driver_init() - 中断proc文件 :
init_irq_proc() - 构造函数 :
do_ctors() - Initcalls :
do_initcalls()
6.12.2 驱动初始化
c
driver_init();
作用: 初始化驱动子系统。
任务:
- 初始化设备模型
- 初始化总线
- 初始化设备类
6.13 准备启动用户空间
6.13.1 rest_init
c
rest_init();
作用: 启动剩余的初始化工作。
主要任务:
- 启动init进程: 创建第一个用户进程
- 启动kthreadd: 创建内核线程守护进程
- 进入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 主要任务
-
设置内存分配掩码
cgfp_allowed_mask = __GFP_BITS_MASK;允许使用所有内存分配标志。
-
设置内存节点
cset_mems_allowed(node_states[N_MEMORY]);允许在任意内存节点分配内存。
-
SMP准备
csmp_prepare_cpus(setup_max_cpus);准备其他CPU。
-
工作队列初始化
cworkqueue_init();初始化工作队列系统。
-
内存管理内部初始化
cinit_mm_internals();初始化内存管理内部结构。
-
早期Initcalls
cdo_pre_smp_initcalls();执行SMP之前的initcalls。
-
锁检测器初始化
clockup_detector_init();初始化死锁检测器。
-
SMP初始化
csmp_init();启动其他CPU。
-
调度器SMP初始化
csched_init_smp();初始化SMP调度。
-
基本设置
cdo_basic_setup();执行基本系统设置。
-
等待initramfs
cwait_for_initramfs();等待initramfs就绪(如果使用)。
-
控制台就绪
cconsole_on_rootfs();在根文件系统上打开控制台。
-
准备命名空间
cprepare_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 根设备指定
根设备可以通过以下方式指定:
- 内核参数 :
root=/dev/sda1 - UUID :
root=UUID=xxx - PARTUUID :
root=PARTUUID=xxx - 设备树: 设备树中指定
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:
- ramdisk_execute_command : 通常是
/init(initramfs中的init) - execute_command : 内核参数
init=指定的程序 - CONFIG_DEFAULT_INIT: 编译时指定的默认init
- 标准路径 :
/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的特点
- Android风格语法: 使用类似Android的init.rc语法
- 服务定义: 可以定义系统服务
- 触发器: 支持on init、on boot、on startup等触发器
- 进程管理: 使用procd进行进程管理和监控
- 依赖管理: 支持服务依赖关系
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杀死
- 环境设置: 设置服务运行环境
- 日志管理: 管理服务的标准输出和错误输出
服务启动顺序
服务通过START和DEPEND参数控制启动顺序:
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
}
关键点:
- OOM_ADJ=-17: 设置OOM调整值为-17,防止被OOM killer杀死
- ROS环境: 加载ROS(Robot Operating System)环境
- Python路径: 设置Python模块搜索路径
- 执行init.rc : 执行
/usr/bin/init.rc程序
Init.rc程序的工作
/usr/bin/init.rc程序(通常是Android的init程序或兼容实现)会:
- 解析init.rc文件 : 读取
/etc/init.rc并解析 - 执行触发器: 按顺序执行on init、on boot等触发器
- 启动服务: 启动定义的服务
- 监控服务: 监控服务状态,崩溃时重启
- 处理属性: 处理系统属性的变化
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的优势
- 灵活性: 可以定义复杂的启动逻辑
- 服务管理: 自动监控和重启服务
- 依赖管理: 支持服务依赖关系
- 触发器: 支持多种启动时机
- 资源控制: 可以设置资源限制
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):
- emergency.target: 紧急模式
- rescue.target: 救援模式
- multi-user.target: 多用户模式(无图形界面)
- graphical.target: 图形界面模式
8.3.3 Systemd单元类型
- service: 系统服务
- target: 启动目标(类似运行级别)
- mount: 挂载点
- socket: 套接字
- timer: 定时器
8.3.4 Systemd启动顺序
markdown
systemd启动
↓
加载单元文件
↓
解析依赖关系
↓
按依赖顺序启动服务
↓
达到默认target
↓
系统就绪
8.4 系统服务启动
8.4.1 服务启动顺序
在本系统中,服务按以下顺序启动:
- Boot服务 (START=10): 挂载文件系统、初始化硬件
- 系统服务 (START=12-13): syslogd、klogd等日志服务
- Init.rc服务 (START=15): 启动init.rc,执行init.rc脚本
- 应用服务 (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 用户会话启动
用户登录后,启动用户会话:
- 加载用户配置 : 加载
.bashrc、.profile等 - 设置环境变量: 设置PATH、ROS环境等
- 启动应用: 根据配置启动应用
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 内核阶段里程碑
- MMU启用: 内核可以使用虚拟地址
- 调度器就绪: 可以运行多个任务
- 中断启用: 可以响应硬件事件
- 根文件系统挂载: 可以访问文件系统
9.2.2 用户空间阶段里程碑(本系统)
- Init进程启动: 用户空间开始运行
- Boot服务完成: 文件系统挂载完成,硬件初始化完成
- Init.rc服务启动: init.rc解析和执行
- Init.rc触发器执行: on init、on boot等触发器执行
- 系统服务启动: 所有定义的服务启动
- 自动启动应用: 应用自动启动
- 系统就绪: 系统完全启动,可以交互
9.2.3 通用系统里程碑
- Init进程启动: 用户空间开始运行
- Systemd启动: systemd进程启动
- 系统服务启动: 系统功能就绪
- 用户登录: 用户可以交互
9.3 启动优化
9.3.1 内核启动优化
- 减少initcalls: 移除不必要的初始化
- 并行初始化: 并行执行没有依赖的初始化
- 延迟初始化: 延迟非关键初始化
- 压缩内核: 使用压缩内核减少加载时间
9.3.2 用户空间启动优化(本系统)
- 优化Boot脚本: 减少文件系统检查和挂载时间
- 减少启动服务: 只启动必要的服务
- 优化Init.rc: 减少不必要的触发器和服务
- 并行启动: 利用procd的并行启动能力
- 延迟启动: 将非关键服务标记为disabled,需要时再启动
- 优化依赖关系: 合理设置DEPEND,避免不必要的等待
9.3.3 通用系统启动优化
- 并行启动服务: 并行启动没有依赖的服务
- 延迟启动: 延迟非关键服务
- 减少启动服务: 只启动必要的服务
- 优化启动脚本: 优化启动脚本执行时间
9.4 启动问题排查
9.4.1 常见问题
- 内核panic: 检查内核日志
- 根文件系统挂载失败: 检查根设备配置
- Init进程无法启动: 检查init程序路径
- 服务启动失败: 检查服务配置和依赖
9.4.2 调试方法
- 内核日志 :
dmesg查看内核日志 - 启动日志: 查看systemd日志
- 单用户模式: 进入单用户模式排查
- 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官方文档