概述
Linux系统的启动过程是一个复杂而精密的过程,从硬件上电到用户登录界面,涉及多个阶段和大量的源码。本文档将结合Linux内核源码,详细分析每个启动阶段的实现原理、关键函数和数据结构。
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字节,但麻雀虽小五脏俱全:
- 设置工作环境:前几行是在"整理桌面",设置段寄存器和栈指针,就像搬家后先整理好房间
- 读取更多代码:
int $0x13是调用BIOS的"搬运工",把硬盘第2扇区的内容搬到内存0x7e00位置 - 交接工作:最后
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强大多了:
grub_mm_init_regions():先整理内存,就像管家清点家里有多少房间可以用grub_device_initialize():认识所有设备,"这是硬盘,这是U盘,这是光驱"grub_load_config():读取配置文件(grub.cfg),就像看菜单知道今天有什么菜grub_show_menu():显示启动菜单,让用户选择"今天想吃什么"(启动哪个系统)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
这是内核自己的启动代码,就像内核的"开场白":
- 设置段寄存器:还是在"整理房间",让CPU知道数据在哪里
- 检查启动签名:
cmpw $0xAA55, (0x1FE)就像检查"通行证",看看0x1FE位置是不是有0xAA55这个魔数 - 验证通过就继续:如果签名对了,就跳转到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函数就像内核的"体检和准备工作",每一步都很重要:
copy_boot_params():把GRUB传来的参数抄一遍,就像"记下老师布置的作业"console_init():准备控制台,让内核能够"说话"(显示信息)detect_memory():探测内存大小,"看看家里有多少钱可以花"keyboard_init():初始化键盘,"确保能听到用户的指令"validate_cpu():检查CPU功能,"看看这台电脑能不能跑我这个内核"set_video():设置显示模式,"调整好屏幕显示"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位保护模式:
- 设置数据段:前面几行是在"重新装修房间",把所有段寄存器都设置成32位保护模式的格式
- 设置栈指针:
movl %edx, %esp是"搬家具",把栈指针也升级到32位 - 正式跳转:
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;
}
这个函数就像"拆快递"的过程,内核是被压缩打包的,需要解压才能用:
console_init():先准备好"工作台",让解压过程能显示进度parse_elf(output):检查"包装标签",确认这确实是个ELF格式的内核文件__decompress(...):这是核心步骤,"拆包装,取出真货",把压缩的内核数据解压到指定位置handle_relocations(...):"重新贴标签",因为内核可能被加载到不同的内存位置,需要调整内部的地址引用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位,就像从"普通楼房"搬到"摩天大楼":
- 设置页表:前几行是"画地图",建立虚拟内存到物理内存的映射表
- 设置PML4:这是64位系统的"总目录",告诉CPU怎么找到各个内存页面
- 启用PAE:打开"物理地址扩展",让CPU能处理更大的内存地址
- 设置长模式位:在EFER寄存器里设置标志,告诉CPU"我要进入64位模式了"
- 启用分页:打开分页机制,让虚拟内存系统开始工作
- 跳转到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()就像"建立银行系统",管理内存这个"钱财":
page_ext_init_flatmem():建立"账本扩展页",记录每个内存页的额外信息mem_init():正式启动内存管理,"银行开门营业"memblock_discard():丢弃早期的临时内存管理器,"旧系统下线"kmem_cache_init():初始化SLAB分配器,"建立小额快速取款机"percpu_init_late():为每个CPU准备专用内存区域,"每个柜台都有自己的钱箱"vmalloc_init():初始化虚拟内存分配器,"建立信用卡系统"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() - 建立"呼叫中心": 这个函数就像建立一个大型呼叫中心:
arch_probe_nr_irqs():先调查"需要多少个电话线"- 分配中断描述符:为每个中断号准备一个"接线员工位",每个工位都有编号和处理规则
init_IRQ() - 安装"电话交换机": 这个函数负责具体的"电话线路连接":
x86_init.irqs.intr_init():执行硬件相关的初始化,"安装交换机硬件"- 设置中断门:为每个中断向量设置处理入口,就像"给每个电话号码分配接线员"
set_system_intr_gate(SYSCALL_VECTOR, ...):专门为系统调用设置特殊通道,"VIP专线"- 设置特殊中断:为重启、清理等特殊操作设置专用中断,"紧急热线"
整个过程就像建立现代化的客服中心:
- 每个硬件设备(键盘、鼠标、网卡等)都有自己的"专线电话"
- 当设备需要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()就像"工厂建成后的开业仪式",内核要把控制权交给用户空间:
最后的准备工作:
kernel_init_freeable():完成最后的初始化,"做最后的检查"async_synchronize_full():等待所有异步任务完成,"确保所有工人都到位"free_initmem():释放初始化内存,"拆除施工脚手架"mark_readonly():标记系统为只读,"锁定重要设施"system_state = SYSTEM_RUNNING:宣布系统正式运行,"挂上'开业大吉'的牌子"
寻找第一个用户程序: 内核像"招聘经理"一样,按优先级寻找合适的init程序:
- 先看ramdisk:如果有ramdisk指定的程序,优先使用
- 再看命令行:如果启动时指定了init程序,就用它
- 最后试默认位置:按顺序尝试
/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函数就像"工厂的人事部门",负责招聘和管理所有的内核工作人员:
kernel_thread(kthreadd, ...):创建kthreadd线程,就像"招聘一个人事经理"find_task_by_pid_ns():找到刚创建的线程,"确认人事经理已经到岗"complete(&kthreadd_done):通知其他部门"人事经理已就位"async_synchronize_full():等待所有异步任务完成,"确保所有准备工作都做完了"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就像内核世界的"人事经理",专门负责创建和管理所有内核线程:
初始化阶段:
set_task_comm(tsk, "kthreadd"):给自己起名叫"kthreadd",就像挂上"人事经理"的名牌ignore_signals(tsk):忽略所有信号,"专心工作,不受外界干扰"set_cpus_allowed_ptr():可以在任何CPU上运行,"哪里需要就去哪里"current->flags |= PF_NOFREEZE:设置为不可冻结,"关键岗位,不能停工"
工作循环: 这是一个永不停止的循环,就像人事经理的日常工作:
set_current_state(TASK_INTERRUPTIBLE):进入可中断睡眠,"没事的时候就休息"if (list_empty(&kthread_create_list)):检查是否有创建线程的请求,"看看有没有招聘需求"schedule():如果没有工作就让出CPU,"没事就让别人先干"处理创建请求:当有请求时,从列表中取出并处理,"有招聘需求就立即处理"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详细讲解:
这个结构包含了进程的"全部家当",主要分为几大类:
身份信息:
pid、tgid:进程ID,就像"身份证号码"comm[TASK_COMM_LEN]:进程名称,就像"姓名"state:当前状态(运行、睡眠、停止等),就像"当前在做什么"
调度相关:
prio、static_prio、normal_prio:各种优先级,就像"工作等级"se、rt、dl:不同的调度实体,就像"不同类型的工作合同"policy:调度策略,就像"工作方式"(全职、兼职、临时工等)
内存管理:
mm、active_mm:内存描述符,就像"住址和房产证"stack:内核栈,就像"个人办公桌"
家庭关系:
parent、real_parent:父进程,就像"父母"children、sibling:子进程和兄弟进程链表,就像"家庭成员名单"
资源管理:
files:打开的文件,就像"借阅的图书清单"fs:文件系统信息,就像"工作目录"signal、sighand:信号处理,就像"通信方式"
这就像一个超级详细的员工档案,记录了员工的所有信息,让系统能够完美地管理每个进程!
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_code、end_code:代码段范围,就像"办公区域"start_data、end_data:数据段范围,就像"仓库区域"start_brk、brk:堆的范围,就像"可扩展的工业区"start_stack:栈的起始地址,就像"临时存储区"
统计信息:
total_vm:总虚拟内存,就像"城市总面积"locked_vm:锁定内存,就像"不能出售的保护区"rss_stat:物理内存使用统计,就像"实际占用的土地"
保护机制:
page_table_lock:页表锁,就像"房产证的保险柜"mmap_sem:内存映射信号量,就像"房产交易的许可证"
特殊区域:
exe_file:可执行文件,就像"主要建筑物的蓝图"context:内存管理上下文,就像"城市管理的具体规则"
整个结构就像一个完整的城市规划档案,记录了进程这个"虚拟城市"的所有细节!
总结
Linux启动过程是一个复杂而精密的过程,涉及从硬件初始化到用户空间服务启动的多个阶段。通过深入理解源码实现,我们可以:
- 掌握系统启动流程: 从BIOS/UEFI到init进程的完整过程
- 理解内核架构: 内核各子系统的初始化顺序和依赖关系
- 优化启动性能: 识别启动瓶颈并进行针对性优化
- 排查启动故障: 快速定位和解决启动问题
- 定制启动过程: 根据需求修改启动流程和参数
关键要点
- 分阶段初始化: 内核采用分阶段初始化策略,确保依赖关系正确
- 架构抽象: 通过架构抽象层支持多种硬件平台
- 模块化设计: 各子系统相对独立,便于维护和扩展
- 错误处理: 完善的错误处理机制确保系统稳定性
- 性能优化: 多种优化技术提高启动速度和运行效率