Linux kernel arm64 启动流程
一、进入内核入口点
在 ARM64 平台上,内核镜像通常以 Image 或 Image.gz 形式被 Bootloader(如 U-Boot、BL33)加载到内存,并通过寄存器(如 x0
、x1
、x2
)传递设备树(DTB)的地址。
入口点位于:
arch/arm64/kernel/head.S
主要执行步骤:
设置异常级别(通常内核运行在 EL1)。
初始化栈指针(sp)。
设置内核页表,开启 MMU。
建立异常向量表。
跳转到 C 语言入口:
start_kernel()
。
二、早期汇编阶段(head.S)
在 head.S
中,内核完成最基础的硬件环境搭建:
此阶段结束后,系统具备了最小可运行环境,跳入 C 语言。
primary_entry├─ record_mmu_state // 检查进入时 MMU/Cache 状态├─ preserve_boot_args // 保存 boot args (x0=dtb)├─ init_kernel_el // 切换到 EL1├─ __cpu_setup // CPU 寄存器初始化└─ __primary_switch├─ __enable_mmu // 开启 MMU├─ __pi_early_map_kernel // 建立正式页表└─ __primary_switched├─ init_cpu_task // 初始化 task/栈├─ 设置向量表、保存 dtb/偏移├─ finalise_el2└─ start_kernel // 进入 C 入口
启动参数
注意primary_switch和带ed的
三、C 语言入口:start_kernel()
C 语言初始化入口是:
init/main.c: start_kernel()
这是内核启动的核心函数,几乎所有初始化工作都从这里展开。
函数属性和调用约束
asmlinkage:保证函数参数从寄存器传递,符合汇编调用约定。
__visible:防止编译器优化掉符号,使调试和链接器可见。
__init:该函数只在启动期间使用,启动完成后其所在代码段可回收。
__no_sanitize_address:关闭 AddressSanitizer 检测。
__noreturn:函数不会返回。
__no_stack_protector:关闭栈保护,避免初始化阶段栈保护干扰。
初始化内核最基本的结构
set_task_stack_end_magic():初始化
init_task
栈边界标记,用于栈溢出检测。smp_setup_processor_id():获取当前 CPU 的物理 ID。
debug_objects_early_init():初始化内核调试对象系统。
init_vmlinux_build_id():记录内核 Build ID,用于调试。
cgroup_init_early():初始化控制组 (cgroup) 的基础数据结构。
关闭中断,保证安全初始化
禁用本地 CPU 中断,防止在硬件和内核数据结构尚未初始化时被中断打断。
设置标记
early_boot_irqs_disabled
,方便后续检查。
Boot CPU 初始化和早期架构设置
这些函数主要做 CPU 和架构相关的早期设置:
boot_cpu_init():初始化 boot CPU 的特权寄存器和 CPU 状态。
page_address_init():设置内存页管理相关的基础数据结构。
pr_notice("%s", linux_banner):打印内核启动横幅信息。
setup_arch():架构相关初始化,如物理内存扫描、异常向量表设置、页表准备。
jump_label_init() / static_call_init():静态分支预测优化初始化,用于 LSM、安全模块等。
early_security_init():早期安全模块初始化。
setup_boot_config():读取启动参数和硬件配置。
setup_command_line():保存内核命令行参数。
setup_nr_cpu_ids():计算系统 CPU 数量。
setup_per_cpu_areas():分配每个 CPU 的内核栈和数据结构。
smp_prepare_boot_cpu():arch-specific boot CPU hooks。
early_numa_node_init() / boot_cpu_hotplug_init():NUMA 节点和 CPU 热插拔早期初始化。
解析启动命令行参数
parse_early_param():解析内核启动参数中早期注册的参数(
__setup()
宏注册的参数)。parse_args():处理剩余的参数,解析传递给
init
的命令行选项。set_init_arg:设置
init
进程的启动参数。unknown_bootoption:记录无法识别的参数。
早期随机数生成器和日志
random_init_early():初始化硬件或软件随机数生成器。
setup_log_buf():初始化内核日志缓冲区。
vfs_caches_init_early():早期文件系统缓存初始化。
sort_main_extable():初始化异常表(异常处理)。
trap_init():初始化陷阱向量和异常处理。
mm_core_init():核心内存管理初始化。
poking_init() / ftrace_init() / early_trace_init():调试和跟踪设施初始化。
调度器与内核数据结构初始化
sched_init():初始化进程调度器。
radix_tree_init() / maple_tree_init():内核数据结构初始化。
housekeeping_init():初始化内核维护结构。
workqueue_init_early():允许早期工作队列创建和调度。
RCU 初始化:包括 RCU、kvfree RCU 等。
trace_init():初始化跟踪事件系统。
IRQ 与时钟相关初始化
early_irq_init() / init_IRQ():中断控制器初始化。
tick_init() / timers_init() / hrtimers_init():定时器初始化。
timekeeping_init() / time_init():系统时间初始化。
RCU nohz:无节拍模式初始化。
内核随机数和防护机制
random_init():完整 RNG 初始化。
kfence_init():内存安全检测。
boot_init_stack_canary():栈溢出检测。
性能与调试设施
perf_event_init():性能计数器初始化。
profile_init():系统调用和性能分析初始化。
call_function_init():异步函数调用初始化。
启用中断
启用本地 CPU 中断,允许内核正常调度和中断处理。
内存管理与控制组初始化
kmem_cache_init_late():slab/缓存分配器初始化。
setup_per_cpu_pageset() / numa_policy_init():NUMA 和 per-CPU 内存分配策略。
控制组、命名空间、进程等初始化
内核所有高级结构初始化,包括:
任务/进程管理:
pid_idr_init() / fork_init()
虚拟内存和页缓存:
anon_vma_init() / pagecache_init()
命名空间:
uts_ns_init() / net_ns_init() / nsfs_init()
控制组:
cgroup_init()
安全/密钥:
cred_init() / key_init() / security_init()
性能/统计:
taskstats_init_early() / delayacct_init()
ACPI 和平台相关初始化
ACPI:硬件抽象和电源管理初始化。
arch_post_acpi_subsys_init():架构相关的后 ACPI 初始化。
启动第一个用户空间进程
rest_init();
创建 第一个内核线程
init
,通常执行/sbin/init
或 initramfs。至此内核完成从 汇编 -> C 语言 -> 用户态 的完整启动流程。
四、架构相关初始化:setup_arch()
核心职责
setup_arch()
是 ARM64 Linux 内核 C 语言阶段的早期架构初始化函数。它在 head.S 汇编阶段完成最基本 CPU/内存设置后被调用,作用是设置内核运行所需的硬件环境、内存布局以及平台相关功能。主要包括内存管理初始化、CPU 异常屏蔽、平台硬件初始化以及 SMP 支持等。
内存管理初始化
setup_initial_init_mm(_stext, _etext, _edata, _end); arm64_memblock_init(); paging_init(); bootmem_init();
初始化内核的初始页表和内存段,确保内核代码段、数据段和 BSS 段可用。
初始化
memblock
,管理早期物理内存。初始化分页机制(MMU 页表和内核虚拟映射)。
初始化早期内存分配器(bootmem allocator)。
内核命令行
*cmdline_p = boot_command_line; parse_early_param();
保存 bootloader 传入的内核命令行。
解析早期内核启动参数,如
console=
,root=
,loglevel=
。
KASLR 初始化
kaslr_init();
随机化内核加载地址,提高安全性。
早期固定映射(Fixmap / IO Remap)
early_fixmap_init(); early_ioremap_init();
提供早期固定虚拟地址来访问硬件寄存器或内核关键数据。
在 MMU 开启之前或开启初期使用。
平台硬件初始化
setup_machine_fdt(__fdt_pointer); acpi_table_upgrade(); acpi_boot_table_init(); if (acpi_disabled) unflatten_device_tree(); psci_dt_init(); psci_acpi_init(); arm64_rsi_init();
解析设备树(FDT)或 ACPI 表,获取硬件信息(CPU、内存、设备)。
初始化 PSCI(CPU 热插拔和电源管理接口)。
初始化 ARM64 低级平台接口(RSI)。
CPU 异常屏蔽
local_daif_restore(DAIF_PROCCTX_NOIRQ); cpu_uninstall_idmap();
DAIF 异常屏蔽寄存器控制 IRQ / FIQ / SError / Debug。
这里取消调试和 SError 屏蔽,保证早期错误可报告,但仍保持中断屏蔽。
卸载早期 identity mapping,防止 MMU speculatively fetch 新的页表条目。
EFI / Xen 初始化
xen_early_init(); efi_init();
为 ACPI / boot table 解析做好准备。
静态 key 与动态 SCS 初始化
jump_label_init(); dynamic_scs_init();
初始化静态分支(Static Keys),优化条件分支。
初始化安全关键寄存器(Dynamic SCS)。
SMP 与 CPU 拓扑
init_bootcpu_ops(); smp_init_cpus(); smp_build_mpidr_hash();
初始化 boot CPU 特定操作。
初始化多核 CPU 支持。
构建 MPIDR 哈希表,用于 CPU 拓扑管理。
感觉多核启动想单独讲一下了
启动检测与警告
if (!efi_enabled(EFI_BOOT)) {if ((u64)_text % MIN_KIMG_ALIGN) pr_warn(FW_BUG "Kernel image misaligned at boot..."); WARN_TAINT(mmu_enabled_at_boot, TAINT_FIRMWARE_WORKAROUND, FW_BUG "Booted with MMU enabled!"); }
检查内核对齐情况。
检查启动时 MMU 是否已经开启,不符合规范会打 TAINT 警告。
其他安全与调试初始化
kasan_init(); request_standard_resources(); early_ioremap_reset();
KASAN 早期初始化(内核地址错误检测)。
标准资源请求和早期 IO remap 重置。
TTBR0 与初始化线程信息
init_task.thread_info.ttbr0 = phys_to_ttbr(__pa_symbol(reserved_pg_dir));
在开启 SW_TTBR0_PAN 功能时,确保 init 线程访问用户空间时会触发翻译错误,从而保护内核。
x1-x3 启动参数检查
if (boot_args[1] || boot_args[2] || boot_args[3]) { pr_err("WARNING: x1-x3 nonzero in violation of boot protocol..."); }
根据 ARM64 Boot Protocol,x1~x3 在启动时必须为 0。
非零值表示 bootloader 或内核版本不兼容。
五、内核子系统初始化:initcall 机制
内核采用 initcall 机制 分阶段初始化各个子系统。start_kernel()
中最终会调用:
rest_init() → kernel_init() → kernel_init_freeable() → do_basic_setup() → do_initcalls()
其中 do_initcalls()
会按等级依次执行所有注册的初始化函数。
Initcall 等级如下(位于 include/linux/init.h
):
pure_initcall:最早期的初始化。
core_initcall:核心子系统。
postcore_initcall:核心之后。
arch_initcall:体系结构相关初始化。
subsys_initcall:子系统(如 VFS、PCI、USB)。
fs_initcall:文件系统相关。
device_initcall:设备驱动初始化。
late_initcall:收尾阶段。
通过这种分层机制,内核保证依赖顺序正确,各个子系统逐步上线。
initcall合集
带sync需要等待,不带sync可以多核并行开始
六、用户空间启动
当内核子系统初始化完毕后,最终进入用户空间:
RCU 调度器初始化
rcu_scheduler_starting();
初始化 RCU(Read-Copy Update)调度器,确保内核的读-写锁机制可以正常工作。
这是启动多任务调度前必须做的操作。
创建 init 进程(PID 1)
pid = user_mode_thread(kernel_init, NULL, CLONE_FS);
user_mode_thread()
用来创建一个用户态线程,这里就是 init 进程。kernel_init
是 init 进程实际执行的函数。CLONE_FS
表示线程共享文件系统信息。init 进程最终会成为 PID=1 的用户态进程,是系统第一个启动的用户进程。
固定 init 进程在 Boot CPU 上
rcu_read_lock(); tsk = find_task_by_pid_ns(pid, &init_pid_ns); tsk->flags |= PF_NO_SETAFFINITY; set_cpus_allowed_ptr(tsk, cpumask_of(smp_processor_id())); rcu_read_unlock();
为了确保在多核系统中 init 进程不会被迁移到其他 CPU 上(因为调度器还没完全初始化)。
使用
PF_NO_SETAFFINITY
阻止进程被迁移。set_cpus_allowed_ptr()
只允许它在 Boot CPU 上运行。
初始化 NUMA 默认策略
numa_default_policy();
设置 NUMA(Non-Uniform Memory Access)内存访问策略。
确保后续分配内存时,内核知道首选的节点。
创建 kthreadd(内核线程管理器)
pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES); rcu_read_lock(); kthreadd_task = find_task_by_pid_ns(pid, &init_pid_ns); rcu_read_unlock();
kthreadd
是 Linux 内核的 内核线程管理器。它负责管理内核后台线程(kthreads)的创建和调度。
CLONE_FS | CLONE_FILES
让它共享文件系统和文件描述符。
设置系统状态
system_state = SYSTEM_SCHEDULING; complete(&kthreadd_done);
将
system_state
标记为 可调度,意味着内核进入正式调度阶段。complete(&kthreadd_done)
通知 init 进程和其他线程,kthreadd 已经准备好。
启动 Boot CPU 调度器
schedule_preempt_disabled(); cpu_startup_entry(CPUHP_ONLINE);
schedule_preempt_disabled()
调用一次调度函数,启动调度器循环。cpu_startup_entry()
进入 CPU idle loop,开始执行空闲线程(idle thread)。Boot CPU 现在已经进入可调度状态,系统开始多任务执行。
回到kernel_init看如何进入用户态初始化
调用
kernel_init()
→run_init_process()
。尝试执行:
/sbin/init
/etc/init
/bin/init
/bin/sh
如果成功,系统转交控制权给用户空间的 init 进程(现代系统多为 systemd)。
至此,Linux 内核启动流程完成。
systemd启动后续再介绍