当你按下电源键的那一刻,一场精密编排的启动仪式就开始了。从 BIOS/UEFI 固件自检,到引导加载器将内核映像载入内存,再到内核逐个唤醒 50 多个子系统、最终将控制权交给 systemd——整个过程不过几秒钟,却涉及硬件初始化、内存管理、进程创建、文件系统挂载等几乎所有核心子系统的协同工作。
如果你读过姊妹系列「从零开始的操作系统」第 1 章和第 5 章,应该已经了解 BIOS 如何加载 MBR 引导扇区、以及如何用汇编代码从实模式切换到保护模式。本章将从内核获得控制权的那一刻开始,沿着 startup_64 → extract_kernel → start_kernel → rest_init → initramfs → systemd 这条主线,完整追踪 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 地址等
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 的核心任务是:
- 验证自身加载地址:检查是否被加载到正确的物理地址(
CONFIG_PHYSICAL_START),若不是则需重定位 - 建立临时页表:使用编译时生成的
init_top_pgt,建立内核映像的恒等映射(Identity Mapping)和内核文本映射 - 启用 5 级分页(若硬件支持且内核配置了
CONFIG_X86_5LEVEL):将页表从 4 级(PML4)切换到 5 级(PML5) - 跳转到 C 代码:设置好栈指针后,跳转到
extract_kernel进行内核解压
// arch/x86/boot/compressed/head_64.S(简化)SYM_CODE_START(startup_64) // 1. 检查是否需要重定位 call 1f1: popq %rbp subq $1b, %rbp // 计算实际加载偏移
// 2. 设置栈 leaq boot_stack_end(%rbp), %rsp
// 3. 调用内核解压 call extract_kernelSYM_CODE_END(startup_64)1.3 extract_kernel:解压与重定位
extract_kernel 定义在 arch/x86/boot/compressed/misc.c,它负责:
- 选择解压算法:根据编译时配置选择解压方法——GZIP、LZ4、LZMA、XZ 或 ZSTD
- 计算安全的目标地址:在物理内存中找到一块足够大且不与当前映像重叠的区域
- 执行解压:将压缩的内核数据解压到目标地址
- 处理重定位:遍历内核映像中的
.reloc段,修正所有绝对地址引用——因为内核实际加载地址可能与编译时假定的地址不同 - 跳转到解压后的内核入口:即
startup_64的”第二次执行”——这次在正确的地址上运行
内核解压过程中没有任何控制台输出能力——此时 printk、控制台驱动都尚未初始化。如果解压失败,你只会看到一个无声的挂起或重启。这就是为什么 extract_kernel 中使用 error() 函数时,它只能通过直接操作 VGA 文本缓冲区(0xB8000)来显示错误信息。
二、start_kernel():50+ 子系统初始化的调度中心
解压后的内核最终跳转到 start_kernel(),这是整个内核初始化的”总指挥”。它定义在 init/main.c,是一个超过 200 行的函数,按严格顺序调用了 50 多个子系统的初始化函数。这个顺序不是随意的——每个初始化都可能依赖前面已完成的工作。
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 级别:
#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) 就能自动在启动时被调用。
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 进程的入口函数,它完成以下工作:
- 等待 kthreadd 就绪:
wait_for_completion(&kthreadd_done)——确保内核线程管理器已启动 - 执行 async_init 探测:
async_synchronize_full()等待所有异步探测完成 - 尝试挂载根文件系统:调用
kernel_init_freeable(),其中包含 initramfs 的处理和真实根文件系统的挂载 - 执行 /sbin/init:如果根文件系统挂载成功,则通过
run_init_process()执行/sbin/init(或内核命令行指定的init=参数)
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.");}如果所有 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() 调用链中。
initramfs 和 initrd 是两个不同的概念。initrd(Initial RAM Disk)是旧方案,它是一个块设备映像,内核需要用 ext2 等文件系统驱动来挂载它;initramfs 是新方案,它直接解压到 rootfs 中,不需要任何文件系统驱动。现代 Linux 发行版几乎全部使用 initramfs,但为了向后兼容,内核仍然支持 initrd。
4.3 /init:initramfs 的入口
initramfs 解压完成后,内核会尝试执行 /init 程序。这个程序通常是 shell 脚本或 ELF 二进制,负责:
- 加载必要的内核模块:通过
modprobe加载存储控制器驱动、文件系统驱动等 - 创建设备节点:在
/dev下创建必要的设备文件(或依赖 devtmpfs 自动创建) - 挂载临时文件系统:
/proc、/sys、/dev等 - 定位并挂载真实根文件系统:这是最关键的一步
- 切换到真实根文件系统:通过
pivot_root或switch_root - 执行真实根文件系统上的 init:
exec /sbin/init
典型的 /init 脚本结构如下:
#!/bin/sh# /init - initramfs 入口脚本(简化版)
# 1. 挂载虚拟文件系统mount -t proc proc /procmount -t sysfs sysfs /sysmount -t devtmpfs devtmpfs /dev
# 2. 加载必要的驱动模块modprobe ahci # SATA 控制器modprobe ext4 # ext4 文件系统modprobe dm-crypt # 磁盘加密(如需要)
# 3. 挂载真实根文件系统mount -t ext4 /dev/sda2 /mnt/root
# 4. 切换根文件系统并执行 initexec 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);工作原理:
- 将当前根文件系统挂载到
put_old目录下 - 将
new_root设为新的根文件系统 - 进程的根目录切换到
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.c 的 prepare_namespace() 和 mount_root() 中有对应实现。内核自身的 init 进程在挂载真实根文件系统后,通过 run_init_process() 执行新根上的 init 程序,这实际上完成了 exec + 根切换的组合操作。
switch_root 会不可逆地删除 initramfs 中的所有内容。如果在切换前没有正确挂载真实根文件系统,系统将无法恢复。这就是为什么 /init 脚本在 switch_root 之前通常会进行大量的错误检查。
5.3 内核中的根文件系统挂载流程
内核挂载根文件系统的完整流程如下:
内核命令行参数 root= 指定真实根文件系统的设备,rootfstype= 指定文件系统类型,rootflags= 指定挂载选项。如果使用了 initramfs,这些参数通常由 /init 脚本读取并处理。
六、systemd:作为 PID 1 的服务管理器
6.1 systemd 的架构
当 /sbin/init(通常是 /lib/systemd/systemd 的符号链接)以 PID 1 身份启动后,它承担了两个核心角色:
- 系统初始化:按照依赖关系启动所有系统服务
- 服务管理:监控运行中的服务,按需重启、处理崩溃
systemd 的核心设计理念是基于依赖的服务启动——每个服务声明自己需要什么前置条件,systemd 根据这些声明构建一个有向无环图(DAG),然后尽可能并行地启动服务。
6.2 systemd Unit 类型
systemd 使用”Unit”作为配置和管理的统一抽象。以下是核心的 Unit 类型:
| Unit 类型 | 扩展名 | 用途 | 示例 |
|---|---|---|---|
| Service | .service | 系统服务(守护进程) | sshd.service、nginx.service |
| Target | .target | 服务组/状态里程碑 | multi-user.target、graphical.target |
| Socket | .socket | IPC 套接字(按需激活) | sshd.socket |
| Timer | .timer | 定时器(替代 cron) | logrotate.timer |
| Mount | .mount | 文件系统挂载点 | home.mount |
| Path | .path | 文件/目录监控 | acpid.path |
| Slice | .slice | cgroup 资源分组 | system.slice、user.slice |
| Scope | .scope | 外部创建的进程组 | session-1.scope |
Service Unit 是最常用的类型,其配置文件结构如下:
[Unit]Description=OpenSSH DaemonAfter=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:
sysinit.target:系统初始化——挂载文件系统、启动日志、设置主机名basic.target:基本系统就绪——内核模块加载、设备节点创建完成multi-user.target:多用户模式——网络、SSH、cron 等服务启动graphical.target:图形界面——显示管理器(GDM/SDDM)启动
你可以通过 systemd-analyze blame 查看每个服务的启动耗时,通过 systemd-analyze critical-chain 查看关键路径。
七、内核命令行参数
内核命令行参数是控制启动行为的重要接口。它们由引导加载器传递给内核,在 setup_arch() 和 start_kernel() 中被解析。以下是最常用的参数分类:
7.1 根文件系统参数
| 参数 | 作用 | 示例 |
|---|---|---|
root= | 指定根文件系统设备 | root=/dev/sda2、root=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 |
内核命令行参数的解析代码位于 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/initramfszcat /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
# 查看当前启动到哪个 targetsystemctl 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/Ubuntusudo 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.c | start_kernel()、rest_init()、kernel_init() — 内核初始化主流程 |
init/do_mounts.c | prepare_namespace()、mount_root() — 根文件系统挂载 |
init/initramfs.c | unpack_to_rootfs()、populate_rootfs() — initramfs 解压 |
arch/x86/kernel/head_64.S | startup_64 — 内核入口点 |
arch/x86/boot/compressed/misc.c | extract_kernel() — 内核自解压 |
arch/x86/boot/compressed/head_64.S | 解压阶段的汇编入口 |
kernel/kthread.c | kthreadd() — 内核线程管理器 |
include/linux/init.h | initcall 级别定义 |
init/do_mounts_initrd.c | initrd 相关支持代码 |
权威文档与书籍
- 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》 — 对启动流程的详细源码级分析
在线资源
- Bootlin Elixir Cross Referencer — 在线阅读
start_kernel()源码 - kernel.org x86 boot protocol — 引导加载器与内核的接口规范
- systemd.io — systemd 项目官方站点
- ArchWiki: Arch boot process — 对启动流程的优秀实践文档
支持与分享
如果这篇文章对你有帮助,欢迎支持作者或分享给更多人
部分信息可能已经过时






