Linux 内核启动流程详解
Linux 内核启动流程详解:init/main.c 文件分析
概述
Linux 内核的启动过程是计算机系统中最复杂和关键的初始化序列之一。init/main.c 是内核初始化的核心文件,负责从内核解压完成后的第一条指令开始,直到第一个用户空间程序(init)启动的整个过程。本文将深入分析这个文件的功能、关键数据结构和主要函数,帮助读者理解 Linux 内核是如何从无到有构建起整个操作系统的基础设施。
文件历史背景
这个文件最初由 Linus Torvalds 于 1991 年编写,是 Linux 内核最早的文件之一。经过三十多年的发展,该文件经历了无数次的重构和扩展,但核心的设计理念始终保持不变:提供一个清晰、可追溯的初始化序列,确保内核的各个子系统能够按照正确的依赖顺序被初始化。从最初的简单代码到如今包含数千行复杂逻辑的现代化实现,这个文件见证了 Linux 从一个个人项目成长为全球最重要的开源操作系统的全过程。
核心变量与数据结构
系统状态标识
内核使用一个全局状态变量来跟踪系统的启动进度,这在调试和同步各个子系统时至关重要。system_state 变量定义了系统的当前状态,包括 SYSTEM_BOOTING(启动中)、SYSTEM_SCHEDULING(调度开始)、SYSTEM_FREEING_INITMEM(释放初始化内存)和 SYSTEM_RUNNING(运行中)等状态。这些状态的转换由内核的各个阶段控制,确保在正确的时刻启用或禁用某些功能。例如,system_state = SYSTEM_RUNNING 表示系统已经完全启动,此时可以进行各种运行时操作。
启动参数管理
Linux 内核支持通过命令行传递大量参数来控制启动行为。在 main.c 中,这些参数被保存和处理:boot_command_line 保存了原始的启动命令行参数,saved_command_line 保存了处理后的命令行副本供后续使用,static_command_line 和 extra_command_line 则用于解析过程中需要保留的额外参数。这种多层存储的设计允许内核在不丢失原始信息的情况下,对命令行进行多次解析和修改,以满足不同初始化阶段的需求。
初始化函数级别
内核的初始化函数被分为不同的级别,每个级别对应系统启动的不同阶段。这种分层设计确保了依赖关系得到正确处理,较早的初始化级别只依赖更基础的系统组件,而较晚的级别则可以使用更多的系统资源。七个初始化级别分别是:pure(纯初始化函数)、core(核心子系统)、postcore(后核心初始化)、arch(架构特定初始化)、subsys(子系统初始化)、fs(文件系统初始化)、device(设备初始化)和 late(后期初始化)。每个级别的函数都使用特定的宏标记,如 pure_initcall()、arch_initcall() 等。
启动序列分析
start_kernel 函数详解
start_kernel() 是内核启动的真正入口点,这是一个用 asmlinkage 标记的函数,意味着它期望所有参数通过堆栈传递而非寄存器。这个函数的执行标志着内核从架构特定的汇编代码转向通用的 C 代码执行。函数的开头部分设置了一些基本的安全措施,例如 set_task_stack_end_magic(&init_task) 用于检测栈溢出。接下来依次调用各个子系统的初始化函数,这些调用按照精心设计的顺序排列,以确保后续的初始化能够依赖前面的结果。
在早期的初始化阶段,系统首先处理启动命令行参数:smp_setup_processor_id() 设置当前处理器的 ID,boot_cpu_init() 初始化启动 CPU 的状态位图,page_address_init() 初始化页面地址相关的全局变量。随后,setup_arch() 函数执行架构特定的硬件设置,这可能包括内存探测、设备树解析、中断控制器初始化等。在内存分配器初始化之前,系统会进行大量的准备工作,包括 setup_per_cpu_areas() 设置每个 CPU 的独立内存区域、early_numa_node_init() 初始化 NUMA 节点信息。
调度器初始化
调度器的初始化是 start_kernel 中最关键的步骤之一。sched_init() 函数在早期阶段被调用,此时中断仍然可能处于禁用状态。这个函数建立了调度器的基本数据结构,包括每个 CPU 的运行队列、调度类链表等。虽然在这个阶段调度器还不能真正进行进程切换,但它已经准备好了接收新的任务。调度器初始化的时机非常重要:它必须在中断系统完全初始化之前运行,因为中断处理可能需要调度功能;同时它又必须在系统真正需要多任务处理之前完成。
中断和定时器系统
中断系统是操作系统响应外部事件的基础。在 main.c 中,中断相关的初始化分散在多个函数中:init_IRQ() 初始化中断控制器,early_irq_init() 进行早期的中断相关设置,tick_init() 初始化时钟滴答设备。这些初始化必须在内核启用中断之前完成,否则任何中断都可能导致系统崩溃。定时器系统同样重要:timers_init() 初始化软件定时器子系统,hrtimers_init() 初始化高精度定时器,timekeeping_init() 初始化时间keeping相关的变量,time_init() 执行架构特定的时间初始化。
内存管理初始化
内存管理是内核最复杂的子系统之一,其初始化跨越多个函数调用序列。page_address_init() 首先处理高端内存的页面地址映射,随后 setup_per_cpu_pageset() 初始化每个 CPU 的页面集缓存。当内核的 slab 分配器准备好后,kmem_cache_init_late() 完成了内存分配器的最后初始化步骤。在内存管理完全就绪之前,内核使用 bootmem 或 memblock 分配器来获取内存,这些临时分配器在后期会被释放。mem_cgroup_init() 初始化内存控制组,为 cgroup 内存限制提供支持。
VFS 缓存初始化
虚拟文件系统(VFS)缓存的初始化为后续的文件系统操作提供了基础结构。vfs_caches_init_early() 在早期阶段创建 slab 缓存,用于 VFS 相关的对象,如 dentry 和 inode。完整的 VFS 缓存初始化在 vfs_caches_init() 中完成,此时会创建更多的缓存并初始化文件描述符表等数据结构。VFS 的初始化必须在任何文件系统被挂载之前完成,因为根文件系统的挂载需要 VFS 基础设施的支持。
用户空间初始化
rest_init 函数与多核启动
当 start_kernel 完成所有核心子系统的初始化后,它调用 rest_init() 来启动用户空间初始化过程。这个函数的核心任务是创建两个关键的内核线程:init 线程(PID 1)和 kthreadd 线程(PID 2)。init 线程将负责最终执行用户空间的第一个程序,而 kthreadd 线程是所有其他内核线程的祖先。函数首先通过 user_mode_thread() 创建 init 进程,这个进程将运行 kernel_init() 函数。为了确保 init 进程首先获得 PID 1,内核会等待 kthreadd 完成创建后才让 init 进程继续执行。
在 rest_init 中,系统状态被设置为 SYSTEM_SCHEDULING,这是一个重要的里程碑,表示调度器已经准备好进行进程切换。从这个时刻开始,内核代码可以在需要时调用 schedule() 来让出 CPU 控制权。函数最后让初始 CPU 进入空闲状态,这通过 cpu_startup_entry(CPUHP_ONLINE) 实现。空闲线程是系统中优先级最低的线程,当没有其他工作需要处理时,CPU 就会执行这个线程。
kernel_init_freeable 函数
kernel_init_freeable() 函数执行 init 进程开始运行后需要进行的一些"可延迟"初始化工作。这个函数名的含义是"可以释放初始化内存之后执行的初始化",这暗示了它与初始化内存释放之间的时序关系。在函数内部,系统完成了大量的最后初始化步骤:SMP 准备(smp_prepare_cpus())、工作队列初始化(workqueue_init())、完整的 SMP 初始化(smp_init() 和 sched_init_smp())以及设备驱动初始化(do_basic_setup())。
init 进程的启动过程
kernel_init() 是 init 进程的入口函数,它负责找到并执行用户空间的第一个程序。函数首先等待 kthreadd 完成初始化,然后调用 kernel_init_freeable() 进行最后的准备工作。之后,它依次尝试执行多个可能的 init 程序:首先检查 ramdisk_execute_command(如果指定了 rdinit= 参数),然后检查内核命令行中的 execute_command(init= 参数),最后按顺序尝试 /sbin/init、/etc/init、/bin/init 和 /bin/sh。如果所有这些尝试都失败,系统会触发内核恐慌(panic),因为没有有效的 init 进程意味着系统无法正常运行。
根文件系统挂载
在用户空间 init 程序执行之前,系统必须首先挂载根文件系统。这个过程由 prepare_namespace() 函数完成,它会根据内核命令行参数选择合适的根文件系统设备,并按照指定的选项进行挂载。常见的根文件系统位置包括本地磁盘分区、网络文件系统(NFS)或 initramfs。如果内核配置了 initrd 或 initramfs,那么 init 程序可能已经在其中运行,此时 prepare_namespace() 的行为会有所不同。根文件系统的成功挂载是执行用户空间程序的前提条件。
初始化调用机制
initcall 级别与顺序
Linux 内核使用一种精巧的机制来管理数千个初始化函数。内核镜像中定义了多个 section,分别存放不同级别的初始化函数:__initcall0_start 到 __initcall7_start 分别对应七个初始化级别,而 __initcall_start 用于存放早期的纯初始化函数。这些 section 由链接器自动生成,开发者只需要在函数前加上相应的宏即可。do_initcall_level() 函数负责执行每个级别的所有初始化调用,它首先解析该级别相关的命令行参数,然后依次调用每个函数。
初始化调试支持
内核提供了丰富的初始化调试支持。当内核命令行包含 initcall_debug 参数时,每个初始化调用的执行时间和结果都会被记录到内核日志中。这对于诊断启动缓慢或初始化失败的问题非常有帮助。do_one_initcall() 函数是执行单个初始化调用的核心,它处理函数调用的参数传递、错误捕获和性能测量。如果初始化函数返回非零值,内核会记录警告信息,但通常不会中止启动过程,因为这可能只是某个可选功能初始化失败。
初始化内存释放
在内核初始化完成后,初始化阶段使用的临时内存会被释放。这个过程通过 free_initmem() 函数完成,它会遍历所有初始化代码和数据段,将它们标记为可回收。释放这些内存后,系统会调用 mark_readonly() 将内核的代码段和数据段设置为只读,以提供额外的安全保护。这一步骤是可选的,取决于内核配置选项。如果内核启用了 CONFIG_DEBUG_RODATA,那么 mark_readonly() 还会执行额外的验证,确保没有意外的写操作破坏了只读保护。
关键子系统初始化顺序
早期初始化序列
从 start_kernel 的开头到 rest_init() 调用之前,内核执行了大量的早期初始化工作。这个序列的设计原则是:首先初始化最基础的依赖,然后逐步建立更复杂的功能。最初的几步包括设置栈保护魔法数字、获取处理器 ID、初始化调试对象等。随后,内存子系统的早期部分被初始化,允许内核开始使用更高级的内存分配函数。页分配器的早期准备和 vfs 缓存的早期初始化也在这个阶段完成,它们为后续的文件系统操作提供了基础。
cgroup 和命名空间初始化
Linux 容器的兴起使得 cgroup 和命名空间的初始化变得尤为重要。在 main.c 中,cgroup_init_early() 在非常早期的阶段就被调用,这允许内核在初始化过程中就开始建立 cgroup 层次结构。完整的 cgroup 初始化在 cgroup_init() 中完成,包括各种控制器的注册和默认层次结构的创建。命名空间相关的初始化包括 uts_ns_init()(UTS 命名空间)、time_ns_init()(时间命名空间)和 pid_ns_init()(PID 命名空间),它们共同支持了容器技术的实现。
网络命名空间初始化
虽然 TCP/IP 协议栈的初始化发生在更晚的阶段,但网络子系统的基础设施在 main.c 中就已经开始建立。net_ns_init() 函数创建了初始的网络命名空间,设置了网络相关的全局变量和缓存。网络子系统的初始化必须在网络设备驱动初始化之前完成,以确保设备驱动能够正确注册到网络子系统中。这个依赖关系是通过 initcall 的级别顺序来保证的:网络命名空间在 subsys 级别初始化,而设备驱动在 device 级别初始化。
安全与调试支持
内核调试基础设施
main.c 提供了多种调试工具来帮助开发者诊断问题。lockdep_init() 初始化了锁依赖验证器,它可以检测死锁和其他锁使用错误。debug_lates_init() 在所有初始化完成后执行额外的自检。kgdb_free_init_mem() 和 kfence_init() 等函数设置了各种内存调试工具的内部状态。如果内核编译时启用了 KASAN 或其他内存错误检测器,这些工具会在初始化过程中进行自我验证。
KUnit 测试框架
在内核 5.5 版本引入的 KUnit 测试框架集成在 main.c 的初始化流程中。kunit_run_all_tests() 在 do_basic_setup() 完成后执行所有编译到内核中的 KUnit 测试。这些测试覆盖了内核的各个子系统,提供了回归测试和单元测试的能力。KUnit 的集成使得内核开发者能够在每次启动时验证关键代码的正确性,这在内核这样的复杂系统中非常宝贵。
总结
[init/main.c](file:///home/debian0/workspace/linuxproject/linux-6.18.35/init/main.c) 文件是 Linux 内核启动过程的核心,它定义了一个从无到有构建操作系统的完整序列。通过精心设计的初始化顺序、灵活的分级机制和完善的错误处理,内核能够可靠地在各种硬件平台上启动并提供服务。理解这个文件的结构和逻辑,不仅对于内核开发者有重要意义,也能帮助系统程序员更好地理解操作系统的底层工作原理。
从 start_kernel() 的第一条指令到用户空间 init 程序的执行,内核经历了数百个初始化步骤。每个步骤都为后续的步骤创造了必要的条件,最终建立起一个功能完整的操作系统。这个过程中体现的设计哲学——分层、依赖管理、优雅降级——也值得在其他复杂的软件系统中学习和借鉴。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐
所有评论(0)