mobile wallpaper 1mobile wallpaper 2mobile wallpaper 3mobile wallpaper 4
4922 字
13 分钟
Linux 启动流程
2024-11-11

当你按下电源键的那一刻,一场精密编排的启动仪式就开始了。从 BIOS/UEFI 固件自检,到引导加载器将内核映像载入内存,再到内核逐个唤醒 50 多个子系统、最终将控制权交给 systemd——整个过程不过几秒钟,却涉及硬件初始化、内存管理、进程创建、文件系统挂载等几乎所有核心子系统的协同工作。

如果你读过姊妹系列「从零开始的操作系统」第 1 章和第 5 章,应该已经了解 BIOS 如何加载 MBR 引导扇区、以及如何用汇编代码从实模式切换到保护模式。本章将从内核获得控制权的那一刻开始,沿着 startup_64extract_kernelstart_kernelrest_initinitramfssystemd 这条主线,完整追踪 Linux 从”裸机”到”可用系统”的全过程。

理解启动流程的意义远不止满足好奇心——当系统无法启动、卡在某个阶段、或启动耗时异常时,只有掌握了每个阶段在做什么,才能精准定位问题。

一、内核解压与重定位:从 startup_64 到 extract_kernel#

1.1 引导加载器的交付物#

当 GRUB2 或 systemd-boot 等引导加载器完成工作后,它向内核交付了以下”遗产”:

  • bzImage(大内核映像)加载到物理内存的合适位置(通常在 1MB 以上)
  • 进入 64 位保护模式,启用分页
  • 将内核命令行参数(BOOT_CMDLINE)放置在约定的内存位置
  • boot_params 结构(arch/x86/include/uapi/asm/bootparam.h)中填入硬件信息:E820 内存映射、VBE 显示模式、ACPI RSDP 地址等
Note

bzImage 中的 “bz” 并非 “bzip2” 的缩写,而是 “big zImage”——它突破了早期 zImage 对内核大小的限制(实模式下约 512KB),允许内核加载到高位内存。现代 Linux 内核几乎全部使用 bzImage 格式。

1.2 startup_64:内核的第一条指令#

内核映像的入口点是 startup_64,定义在 arch/x86/kernel/head_64.S。此时 CPU 处于一个”半初始化”状态:64 位模式已启用,但内核自身的运行环境尚未建立。startup_64 的核心任务是:

  1. 验证自身加载地址:检查是否被加载到正确的物理地址(CONFIG_PHYSICAL_START),若不是则需重定位
  2. 建立临时页表:使用编译时生成的 init_top_pgt,建立内核映像的恒等映射(Identity Mapping)和内核文本映射
  3. 启用 5 级分页(若硬件支持且内核配置了 CONFIG_X86_5LEVEL):将页表从 4 级(PML4)切换到 5 级(PML5)
  4. 跳转到 C 代码:设置好栈指针后,跳转到 extract_kernel 进行内核解压
// arch/x86/boot/compressed/head_64.S(简化)
SYM_CODE_START(startup_64)
// 1. 检查是否需要重定位
call 1f
1: popq %rbp
subq $1b, %rbp // 计算实际加载偏移
// 2. 设置栈
leaq boot_stack_end(%rbp), %rsp
// 3. 调用内核解压
call extract_kernel
SYM_CODE_END(startup_64)

1.3 extract_kernel:解压与重定位#

extract_kernel 定义在 arch/x86/boot/compressed/misc.c,它负责:

  1. 选择解压算法:根据编译时配置选择解压方法——GZIP、LZ4、LZMA、XZ 或 ZSTD
  2. 计算安全的目标地址:在物理内存中找到一块足够大且不与当前映像重叠的区域
  3. 执行解压:将压缩的内核数据解压到目标地址
  4. 处理重定位:遍历内核映像中的 .reloc 段,修正所有绝对地址引用——因为内核实际加载地址可能与编译时假定的地址不同
  5. 跳转到解压后的内核入口:即 startup_64 的”第二次执行”——这次在正确的地址上运行
Warning

内核解压过程中没有任何控制台输出能力——此时 printk、控制台驱动都尚未初始化。如果解压失败,你只会看到一个无声的挂起或重启。这就是为什么 extract_kernel 中使用 error() 函数时,它只能通过直接操作 VGA 文本缓冲区(0xB8000)来显示错误信息。

二、start_kernel():50+ 子系统初始化的调度中心#

解压后的内核最终跳转到 start_kernel(),这是整个内核初始化的”总指挥”。它定义在 init/main.c,是一个超过 200 行的函数,按严格顺序调用了 50 多个子系统的初始化函数。这个顺序不是随意的——每个初始化都可能依赖前面已完成的工作。

flowchart TD A["setup_arch()<br/>架构相关初始化"] --> B["mm_core_init()<br/>内存管理核心"] B --> C["trap_init()<br/>中断/异常门"] C --> D["sched_init()<br/>调度器"] D --> E["workqueue_init()<br/>工作队列"] E --> F["vfs_caches_init()<br/>VFS 缓存"] F --> G["driver_init()<br/>设备模型"] G --> H["do_initcalls()<br/>内核模块 initcall"] H --> I["rest_init()<br/>创建 init 进程"] style A fill:#fbb,stroke:#333 style D fill:#bbf,stroke:#333 style F fill:#bfb,stroke:#333 style I fill:#f9f,stroke:#333

2.1 关键初始化阶段详解#

以下是 start_kernel() 中最关键的几个初始化调用,按照它们在源码中的出现顺序排列:

阶段一:架构与内存(让内核”站稳脚跟”)

函数作用关键依赖
setup_arch()解析内核命令行、初始化内存映射(E820)、建立内核页表、探测 CPU 特性无(最早执行)
mm_core_init()初始化伙伴系统、slab 分配器、vmalloc 机制setup_arch() 提供的物理内存信息
trap_init()设置 IDT(中断描述符表),注册异常处理程序内存分配器可用
early_irq_init()初始化中断描述符基础结构内存分配器可用

setup_arch() 是架构相关的初始化入口,在 x86 上它做了大量工作:解析 ACPI 表、建立 E820 内存映射、初始化 struct page 数组(memmap)、设置内核的 .text / .data / .bss 段映射。这些工作为后续所有子系统提供了内存分配和地址转换的基础——参见第 6 章:物理内存管理第 7 章:虚拟内存与 VMA

阶段二:调度与并发(让内核”能做事”)

函数作用关键依赖
sched_init()初始化 CFS/RT/Deadline 调度类、创建 init_task 的调度实体内存分配器
workqueue_init()创建系统默认工作队列(system_wq 等)调度器可用
rcu_init()初始化 RCU 机制,启动 grace period 检测调度器可用

sched_init() 标志着内核从”单线程执行”向”可调度”状态转变。在此之前,内核代码以同步方式顺序执行;在此之后,内核具备了创建和管理多个执行上下文的能力。但此时只有 init_task(PID 0)一个任务在运行——参见第 4 章:进程调度

阶段三:文件系统与设备(让内核”能访问资源”)

函数作用关键依赖
vfs_caches_init()初始化 dentry cache、inode cache、mount 哈希表内存分配器
driver_init()初始化设备模型(kobject、sysfs、设备树)VFS 缓存
do_initcalls()执行所有编译时注册的 initcall 函数所有基础子系统

vfs_caches_init() 创建了 VFS 的核心缓存结构——dentry 和 inode 的 slab 缓存。没有这些缓存,后续的文件系统挂载将无法进行。driver_init() 则建立了设备驱动模型的基础设施,包括 sysfs 的根目录、设备类的注册机制等——参见第 9 章:VFS 与文件系统第 11 章:设备驱动模型

2.2 do_initcalls:内核模块的延迟初始化#

do_initcalls()start_kernel() 中最”重量级”的调用之一。它按优先级顺序执行所有通过 module_init()fs_initcall()device_initcall() 等宏注册的初始化函数。内核定义了 8 个 initcall 级别:

include/linux/init.h
#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) // 最晚执行

链接器将同一级别的 initcall 函数指针收集到 .init.data 段的特定区域,do_initcalls() 依次遍历这些区域并调用每个函数。这就是为什么大多数内核驱动只需声明 module_init(my_init) 就能自动在启动时被调用。

Note

do_initcalls() 执行的所有函数都标记为 __init,它们所在的 .init.text 段在启动完成后会被释放——内核启动后你可以在 dmesg 中看到 Freeing unused kernel memory: ... 的消息。这就是为什么你不能在启动完成后调用 __init 函数——那段内存已经不存在了。

三、rest_init():从内核线程到用户空间#

start_kernel() 的最后一行调用 rest_init(),它完成了从”内核初始化”到”用户空间启动”的关键过渡:

// init/main.c(简化)
noinline void __init __noreturn rest_init(void)
{
// 1. 创建内核线程 init(PID 1)
struct task_struct *tsk = kernel_thread(kernel_init, NULL, CLONE_FS);
// 2. 创建内核线程 kthreadd(PID 2)
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);
// 3. 当前 init_task(PID 0)变为 idle 进程
schedule_preempt_disabled();
cpu_startup_entry(CPUHP_ONLINE); // 永不返回
}

3.1 init 进程(PID 1)#

kernel_init 是 init 进程的入口函数,它完成以下工作:

  1. 等待 kthreadd 就绪wait_for_completion(&kthreadd_done)——确保内核线程管理器已启动
  2. 执行 async_init 探测async_synchronize_full() 等待所有异步探测完成
  3. 尝试挂载根文件系统:调用 kernel_init_freeable(),其中包含 initramfs 的处理和真实根文件系统的挂载
  4. 执行 /sbin/init:如果根文件系统挂载成功,则通过 run_init_process() 执行 /sbin/init(或内核命令行指定的 init= 参数)
init/main.c
static int __ref kernel_init(void *unused)
{
wait_for_completion(&kthreadd_done);
kernel_init_freeable();
// 尝试执行 init 程序,按优先级依次尝试
if (ramdisk_execute_command) {
if (run_init_process(ramdisk_execute_command))
pr_err("Failed to execute %s\n", ramdisk_execute_command);
}
if (execute_command) {
if (run_init_process(execute_command))
pr_err("Failed to execute %s\n", execute_command);
}
// 默认路径
if (!run_init_process("/sbin/init") ||
!run_init_process("/etc/init") ||
!run_init_process("/bin/init") ||
!run_init_process("/bin/sh"))
return 0;
panic("No working init found.");
}
Warning

如果所有 init 程序路径都失败,内核会 panic——这就是为什么删除 /sbin/init 会导致系统无法启动。通过内核命令行参数 init=/bin/bash 可以指定替代的 init 程序,这是系统救援时的常用手段。

3.2 kthreadd 进程(PID 2)#

kthreadd 是所有内核线程的”父进程”。它的职责很简单:在一个循环中等待请求,然后创建新的内核线程。

// kernel/kthread.c(简化)
int kthreadd(void *unused)
{
for (;;) {
// 等待创建内核线程的请求
while (list_empty(&kthread_create_list))
schedule();
// 从请求列表中取出任务并创建内核线程
create_kthread(create);
}
}

当内核其他部分需要创建内核线程时,它们通过 kthread_create() 将请求挂入 kthread_create_list,然后唤醒 kthreadd。这种设计确保所有内核线程都有一个统一的父进程,便于管理和追踪——你可以在 ps 输出中看到所有内核线程的 PPID 都是 2。

3.3 idle 进程(PID 0)#

rest_init() 的调用者——init_task——在创建完 init 和 kthreadd 后,自身变为 idle 进程。它进入 cpu_startup_entry()do_idle(),在一个无限循环中等待中断唤醒。当 CPU 没有可运行的任务时,调度器就会切换到 idle 进程——它是每个 CPU 上优先级最低、永远存在的”兜底”进程。

四、initramfs:早期用户空间#

4.1 为什么需要 initramfs?#

内核启动后面临的”鸡生蛋”问题:要挂载根文件系统,需要文件系统驱动和存储驱动;但这些驱动存储在根文件系统上的 /lib/modules/ 中。initramfs 就是解决这个”先有鸡还是先有蛋”问题的方案——它是一个打包在内核映像中(或由引导加载器单独加载)的微型根文件系统,包含了挂载真实根文件系统所需的驱动和工具。

具体来说,initramfs 在以下场景中不可或缺:

  • 根文件系统在特殊设备上:LVM、软件 RAID、NVMe、USB 存储等需要先加载驱动
  • 根文件系统是加密的:需要先运行 cryptsetup 解密
  • 根文件系统是网络挂载的:NFS/iSCSI 需要先配置网络
  • 使用 overlayfs 的 Live CD:需要先组装叠加层

4.2 initramfs 的 cpio 格式#

initramfs 使用 cpio 归档格式(newc 变体),这是一种比 tar 更简单、更适合内核解析的格式。cpio 归档由一系列”条目”组成,每个条目包含一个文件头和文件数据:

┌─────────────────────────────┐
│ cpio header (110 bytes) │ ← 魔数 "070701"、文件名、权限、大小等
├─────────────────────────────┤
│ filename (NUL terminated) │
├─────────────────────────────┤
│ file data │
├─────────────────────────────┤
│ padding (align to 4 bytes) │
├─────────────────────────────┤
│ next entry... │
└─────────────────────────────┘

内核通过 unpack_to_rootfs()init/initramfs.c)在启动时将 cpio 归档解压到 rootfs(一个基于 tmpfs 的内存文件系统)中。这个过程发生在 start_kernel()vfs_caches_init()populate_rootfs() 调用链中。

Note

initramfs 和 initrd 是两个不同的概念。initrd(Initial RAM Disk)是旧方案,它是一个块设备映像,内核需要用 ext2 等文件系统驱动来挂载它;initramfs 是新方案,它直接解压到 rootfs 中,不需要任何文件系统驱动。现代 Linux 发行版几乎全部使用 initramfs,但为了向后兼容,内核仍然支持 initrd。

4.3 /init:initramfs 的入口#

initramfs 解压完成后,内核会尝试执行 /init 程序。这个程序通常是 shell 脚本或 ELF 二进制,负责:

  1. 加载必要的内核模块:通过 modprobe 加载存储控制器驱动、文件系统驱动等
  2. 创建设备节点:在 /dev 下创建必要的设备文件(或依赖 devtmpfs 自动创建)
  3. 挂载临时文件系统/proc/sys/dev
  4. 定位并挂载真实根文件系统:这是最关键的一步
  5. 切换到真实根文件系统:通过 pivot_rootswitch_root
  6. 执行真实根文件系统上的 initexec /sbin/init

典型的 /init 脚本结构如下:

#!/bin/sh
# /init - initramfs 入口脚本(简化版)
# 1. 挂载虚拟文件系统
mount -t proc proc /proc
mount -t sysfs sysfs /sys
mount -t devtmpfs devtmpfs /dev
# 2. 加载必要的驱动模块
modprobe ahci # SATA 控制器
modprobe ext4 # ext4 文件系统
modprobe dm-crypt # 磁盘加密(如需要)
# 3. 挂载真实根文件系统
mount -t ext4 /dev/sda2 /mnt/root
# 4. 切换根文件系统并执行 init
exec switch_root /mnt/root /sbin/init

五、pivot_root 与 switch_root:切换根文件系统#

5.1 pivot_root 系统调用#

pivot_root 是 Linux 提供的系统调用,用于将当前进程的根文件系统切换到新的位置。它的原型为:

int pivot_root(const char *new_root, const char *put_old);

工作原理:

  1. 将当前根文件系统挂载到 put_old 目录下
  2. new_root 设为新的根文件系统
  3. 进程的根目录切换到 new_root

pivot_root 的关键约束是:调用进程不能与其他进程共享根文件系统(即不能在共享 mount namespace 中使用),这确保了切换操作的安全性。

5.2 switch_root 工具#

switch_root 是 util-linux 提供的用户态工具,专为 initramfs 场景设计。与 pivot_root 不同,它:

  • 删除 initramfs 中的所有内容:释放 initramfs 占用的内存
  • 不需要 put_old 参数:因为它直接删除旧根,而非保留
  • 只能由 PID 1 执行:确保切换操作的安全性

源码层面,switch_root 的核心逻辑在 init/do_mounts.cprepare_namespace()mount_root() 中有对应实现。内核自身的 init 进程在挂载真实根文件系统后,通过 run_init_process() 执行新根上的 init 程序,这实际上完成了 exec + 根切换的组合操作。

Warning

switch_root不可逆地删除 initramfs 中的所有内容。如果在切换前没有正确挂载真实根文件系统,系统将无法恢复。这就是为什么 /init 脚本在 switch_root 之前通常会进行大量的错误检查。

5.3 内核中的根文件系统挂载流程#

内核挂载根文件系统的完整流程如下:

flowchart TD A["populate_rootfs()<br/>解压 initramfs 到 rootfs"] --> B{"存在 /init?"} B -->|是| C["执行 /init<br/>(initramfs 入口)"] B -->|否| D["prepare_namespace()<br/>传统根挂载路径"] C --> E["/init 加载驱动<br/>挂载真实根文件系统"] E --> F["switch_root / pivot_root<br/>切换根文件系统"] F --> G["exec /sbin/init<br/>启动用户空间 init"] D --> H["mount_root()<br/>直接挂载 root= 设备"] H --> G style A fill:#bbf,stroke:#333 style G fill:#fbb,stroke:#333

内核命令行参数 root= 指定真实根文件系统的设备,rootfstype= 指定文件系统类型,rootflags= 指定挂载选项。如果使用了 initramfs,这些参数通常由 /init 脚本读取并处理。

六、systemd:作为 PID 1 的服务管理器#

6.1 systemd 的架构#

/sbin/init(通常是 /lib/systemd/systemd 的符号链接)以 PID 1 身份启动后,它承担了两个核心角色:

  1. 系统初始化:按照依赖关系启动所有系统服务
  2. 服务管理:监控运行中的服务,按需重启、处理崩溃

systemd 的核心设计理念是基于依赖的服务启动——每个服务声明自己需要什么前置条件,systemd 根据这些声明构建一个有向无环图(DAG),然后尽可能并行地启动服务。

flowchart TD subgraph "systemd 启动依赖图" A["sysinit.target<br/>基础系统初始化"] --> B["basic.target<br/>基本系统就绪"] B --> C["multi-user.target<br/>多用户模式"] C --> D["graphical.target<br/>图形界面"] B --> E["network.target<br/>网络就绪"] E --> F["sshd.service<br/>SSH 服务"] E --> G["nginx.service<br/>Web 服务"] B --> H["dbus.service<br/>D-Bus 通信"] H --> F H --> G C --> I["cronie.service<br/>定时任务"] C --> J["rsyslog.service<br/>系统日志"] end style A fill:#fbb,stroke:#333 style D fill:#bbf,stroke:#333

6.2 systemd Unit 类型#

systemd 使用”Unit”作为配置和管理的统一抽象。以下是核心的 Unit 类型:

Unit 类型扩展名用途示例
Service.service系统服务(守护进程)sshd.servicenginx.service
Target.target服务组/状态里程碑multi-user.targetgraphical.target
Socket.socketIPC 套接字(按需激活)sshd.socket
Timer.timer定时器(替代 cron)logrotate.timer
Mount.mount文件系统挂载点home.mount
Path.path文件/目录监控acpid.path
Slice.slicecgroup 资源分组system.sliceuser.slice
Scope.scope外部创建的进程组session-1.scope

Service Unit 是最常用的类型,其配置文件结构如下:

/lib/systemd/system/sshd.service
[Unit]
Description=OpenSSH Daemon
After=network.target # 在网络就绪后启动
Wants=sshdgenkeys.service # 弱依赖:密钥生成
[Service]
Type=notify # 服务通过 sd_notify() 通知就绪
ExecStart=/usr/bin/sshd -D # 启动命令
ExecReload=/bin/kill -HUP $MAINPID # 重载命令
Restart=on-failure # 失败时自动重启
[Install]
WantedBy=multi-user.target # 启用时的目标

6.3 依赖关系:Wants vs Requires#

systemd 定义了多种依赖关系,理解它们的区别至关重要:

  • Requires:强依赖。如果被依赖的 Unit 失败,当前 Unit 也会失败
  • Wants:弱依赖。如果被依赖的 Unit 失败,当前 Unit 不受影响
  • Requisite:前置依赖。如果被依赖的 Unit 尚未启动,当前 Unit 立即失败(不等待)
  • Conflicts:互斥。不能与指定 Unit 同时运行
  • Before/After:顺序依赖。仅控制启动顺序,不产生依赖关系

6.4 systemd 的启动阶段#

systemd 将启动过程划分为明确的阶段,每个阶段对应一个 Target:

  1. sysinit.target:系统初始化——挂载文件系统、启动日志、设置主机名
  2. basic.target:基本系统就绪——内核模块加载、设备节点创建完成
  3. multi-user.target:多用户模式——网络、SSH、cron 等服务启动
  4. graphical.target:图形界面——显示管理器(GDM/SDDM)启动

你可以通过 systemd-analyze blame 查看每个服务的启动耗时,通过 systemd-analyze critical-chain 查看关键路径。

七、内核命令行参数#

内核命令行参数是控制启动行为的重要接口。它们由引导加载器传递给内核,在 setup_arch()start_kernel() 中被解析。以下是最常用的参数分类:

7.1 根文件系统参数#

参数作用示例
root=指定根文件系统设备root=/dev/sda2root=UUID=xxx
rootfstype=指定根文件系统类型rootfstype=ext4
rootflags=根文件系统挂载选项rootflags=noatime
rootdelay=挂载前等待秒数(等待 USB 设备就绪)rootdelay=5
rootwait=无限等待根设备出现rootwait

7.2 初始化参数#

参数作用示例
init=指定 init 程序路径init=/bin/bash(救援模式)
rdinit=指定 initramfs 中的 init 路径rdinit=/bin/sh
S / single单用户模式single
emergency紧急模式(仅挂载根文件系统)emergency
rescue救援模式(挂载根文件系统 + 基本服务)rescue

7.3 调试参数#

参数作用示例
console=指定控制台设备console=ttyS0,115200
loglevel=设置内核日志级别(0-7)loglevel=7
debug启用内核调试输出debug
quiet减少启动信息输出quiet
panic=内核 panic 后自动重启的秒数panic=10
initcall_debug打印每个 initcall 的执行时间initcall_debug

7.4 内存与性能参数#

参数作用示例
mem=限制内核使用的最大内存mem=4G
maxcpus=限制启动时激活的 CPU 数量maxcpus=2
nr_cpus=内核支持的最大 CPU 数量(硬限制)nr_cpus=8
hugepages=预分配的大页数量hugepages=1024
Note

内核命令行参数的解析代码位于 init/main.c 中的 cmdline_parse() 和各子系统的 __setup() / early_param() 注册函数。__setup() 注册的参数在 start_kernel() 末尾的 parse_args() 中被处理;early_param() 注册的参数则在 setup_arch() 阶段就被处理——后者用于那些需要在早期就生效的参数(如 mem=)。

八、完整启动流程总览#

将以上所有阶段串联起来,Linux 的完整启动流程如下:

┌──────────────────────────────────────────────────────────────────┐
│ 1. 固件阶段(BIOS/UEFI) │
│ POST 自检 → 查找引导设备 → 加载并执行引导加载器 │
├──────────────────────────────────────────────────────────────────┤
│ 2. 引导加载器阶段(GRUB2/systemd-boot) │
│ 读取配置 → 加载 bzImage + initramfs 到内存 → 跳转到 startup_64 │
├──────────────────────────────────────────────────────────────────┤
│ 3. 内核自解压阶段 │
│ startup_64 → extract_kernel → 解压 + 重定位 → 跳转到内核入口 │
├──────────────────────────────────────────────────────────────────┤
│ 4. 内核初始化阶段(start_kernel) │
│ setup_arch → mm_init → trap_init → sched_init → │
│ vfs_caches_init → driver_init → do_initcalls │
├──────────────────────────────────────────────────────────────────┤
│ 5. 内核线程阶段(rest_init) │
│ 创建 init(PID1) + kthreadd(PID2) → idle(PID0) 进入循环 │
├──────────────────────────────────────────────────────────────────┤
│ 6. 早期用户空间(initramfs) │
│ 解压 cpio → 执行 /init → 加载驱动 → 挂载真实根文件系统 │
├──────────────────────────────────────────────────────────────────┤
│ 7. 根文件系统切换 │
│ pivot_root / switch_root → exec /sbin/init │
├──────────────────────────────────────────────────────────────────┤
│ 8. 系统服务启动(systemd) │
│ sysinit.target → basic.target → multi-user.target → │
│ graphical.target → 登录界面 │
└──────────────────────────────────────────────────────────────────┘

九、动手实践#

实践一:使用 dmesg 追踪内核启动过程#

# 查看完整的内核启动日志
dmesg
# 带时间戳查看(精确到微秒)
dmesg -T
# 只看启动阶段的初始化信息
dmesg | grep -E "initcall|Freeing|Mounted|systemd"
# 查看内核命令行参数
cat /proc/cmdline
# 统计各 initcall 级别的耗时(需启用 initcall_debug)
dmesg | grep "initcall" | awk '{print $NF}' | sort -n | tail -20

实践二:探索 initramfs 内容#

# 查看 initramfs 中包含的文件(需安装 lsinitramfs)
lsinitramfs /boot/initrd.img-$(uname -r)
# 手动解压 initramfs 查看内容
mkdir /tmp/initramfs && cd /tmp/initramfs
zcat /boot/initrd.img-$(uname -r) | cpio -idmv
# 查看 /init 入口脚本
cat /tmp/initramfs/init
# 查看包含的内核模块
find /tmp/initramfs -name "*.ko*" | head -20

实践三:分析 systemd 启动耗时#

# 查看总体启动耗时
systemd-analyze
# 查看各服务启动耗时(按时间降序)
systemd-analyze blame
# 查看关键路径(启动链上的瓶颈)
systemd-analyze critical-chain
# 生成启动耗时 SVG 图
systemd-analyze plot > boot_analysis.svg
# 查看当前启动到哪个 target
systemctl get-default

实践四:查看和修改内核命令行参数#

# 查看当前内核命令行
cat /proc/cmdline
# 临时修改(下次启动生效,GRUB2)
# 在 GRUB 启动菜单按 'e' 编辑,在 linux 行末尾添加参数
# 例如添加: init=/bin/bash quiet loglevel=3
# 永久修改(GRUB2)
sudo vim /etc/default/grub
# 修改 GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"
sudo update-grub # Debian/Ubuntu
sudo grub2-mkconfig -o /boot/grub2/grub.cfg # RHEL/Fedora

实践五:观察 PID 0/1/2#

# 查看 init 进程(PID 1)
ps -p 1 -o pid,comm,cmd
# 查看所有内核线程(它们的 PPID 都是 2)
ps -eo pid,ppid,comm | awk '$2 == 2 {print}'
# 查看 idle 进程(PID 0,通常显示为 [swaper/...]
# 注意:idle 进程在 ps 中通常不可见,但可以通过 /proc 查看
ls /proc/*/comm 2>/dev/null | head
# 查看 kthreadd(PID 2)
ps -p 2 -o pid,comm,cmd

参考资料#

内核源码#

文件内容
init/main.cstart_kernel()rest_init()kernel_init() — 内核初始化主流程
init/do_mounts.cprepare_namespace()mount_root() — 根文件系统挂载
init/initramfs.cunpack_to_rootfs()populate_rootfs() — initramfs 解压
arch/x86/kernel/head_64.Sstartup_64 — 内核入口点
arch/x86/boot/compressed/misc.cextract_kernel() — 内核自解压
arch/x86/boot/compressed/head_64.S解压阶段的汇编入口
kernel/kthread.ckthreadd() — 内核线程管理器
include/linux/init.hinitcall 级别定义
init/do_mounts_initrd.cinitrd 相关支持代码

权威文档与书籍#

  • Linux 内核官方文档Boot Process — x86 启动协议规范
  • Linux 内核官方文档initramfs buffer format — cpio 格式规范
  • systemd 官方文档Bootup — systemd 启动流程规范
  • 《Linux 内核设计与实现》(Robert Love)— 第 5 章系统调用、第 7 章中断、附录内核启动
  • 《深入理解 Linux 内核》(Bovet & Cesati)— 附录 A 系统启动
  • 《Understanding the Linux Kernel》 — 对启动流程的详细源码级分析

在线资源#

支持与分享

如果这篇文章对你有帮助,欢迎支持作者或分享给更多人

Linux 启动流程
https://blog.souloss.com/posts/linux-internals/linux-boot-process/
作者
Souloss
发布于
2024-11-11
许可协议
CC BY-NC-SA 4.0

部分信息可能已经过时