1. 启动流程总览:从按下电源键到Shell提示符的完整旅程

按下电源键,到屏幕上出现 $ 或 # 提示符,这中间到底发生了什么?

很多人用了好几年 Linux,却说不清这几十秒里系统干了什么。我刚开始接触内核时也一样,总觉得启动是个黑盒——按一下键,等一会儿,就能用了。直到有一次我在嵌入式项目里调试一个启动卡死的问题,才被迫把整个流程从头到尾捋了一遍。嗯,从那以后,我对启动的理解就完全不一样了。

这一章,我们先搭个框架。把从电源键到 Shell 的完整路径走一遍,后面再逐个环节深入。

1.1 启动的五个核心阶段

我把整个启动过程拆成五个阶段。你记一下,后面每一章都会对应到其中某个阶段:

阶段 关键动作 输出产物
① 固件阶段 CPU 复位、POST、引导设备枚举 Boot Device 选定
② Bootloader 阶段 加载内核映像、设备树、initramfs 内核解压入口
③ 内核初始化 架构初始化、中断、调度器、驱动 根文件系统挂载
④ init 进程与用户态 PID 1 启动、服务编排、挂载点准备 控制台就绪
⑤ 登录与 Shell getty、login、Shell 启动 用户交互提示符

说白了,这五个阶段就是一条流水线。每个阶段只负责一件事,然后把接力棒交给下一棒。

1.2 固件阶段:CPU 的第一口呼吸

按下电源键,主板通电。CPU 的复位逻辑会把程序计数器(PC)指向一个固定地址——对 x86 来说是 0xFFFFFFF0,对 ARM 来说可能是 ROM 里的 BootROM 代码。

这个地址里放的是什么?固件。要么是 BIOS(Legacy),要么是 UEFI。

固件干三件事:

  • POST(上电自检)——检查内存、CPU、基本外设。如果内存条没插好,你会听到蜂鸣器报警。
  • 引导设备枚举——按优先级扫描硬盘、U盘、网络、光驱,找到可启动设备。
  • 加载 Bootloader——从引导设备读取第一个扇区(MBR 或 GPT 的引导分区),把控制权交给它。
一个小经验: 我调试过一块 ARM 开发板,启动卡在固件阶段,串口没有任何输出。最后发现是 DDR 时序参数没配对,CPU 跑到了 BootROM 但内存初始化失败。固件阶段的问题最难排查,因为它连日志都出不来。

1.3 Bootloader 阶段:内核的搬运工

Bootloader 的任务很纯粹:把内核从磁盘搬到内存,然后跳过去执行。

常见的 Bootloader 有 GRUB(x86)、U-Boot(嵌入式)、systemd-boot(UEFI 环境)。它们的工作流程大同小异:

  1. 读取配置文件(如 grub.cfg 或 extlinux.conf)
  2. 加载内核映像(vmlinuz 或 Image)到内存
  3. 加载设备树(DTB)或 ACPI 表
  4. 加载 initramfs(初始内存文件系统)
  5. 设置启动参数(如 root=、console=)
  6. 跳转到内核入口点

你想想看,Bootloader 其实是个"小操作系统"。它要有文件系统驱动(ext4、FAT、btrfs),要有磁盘驱动,甚至要有网络协议栈(PXE 启动时)。

关键点: 内核本身不能直接挂载根文件系统,因为根文件系统可能在加密分区、LVM 逻辑卷、或网络存储上。所以需要 initramfs 作为"中间人",先加载必要的驱动,再挂载真正的根。

1.4 内核初始化:从汇编到 C 的世界

内核入口点通常是汇编代码。以 x86 为例,入口是 arch/x86/boot/header.S 里的 start_of_setup。这段汇编代码很短,主要做:

  • 设置段寄存器
  • 切换到保护模式(或长模式)
  • 解压内核(内核映像通常是压缩的)
  • 跳转到 C 语言的入口 start_kernel()

一旦进入 start_kernel(),内核就开始"造世界"了:

// 伪代码,展示关键初始化顺序
start_kernel()
  ├── setup_arch()          // 架构相关:页表、中断向量、时钟
  ├── trap_init()           // 中断门初始化
  ├── mm_init()             // 内存管理:伙伴系统、slab
  ├── sched_init()          // 调度器:进程 0(idle)和进程 1(init)
  ├── init_IRQ()            // 中断控制器初始化
  ├── console_init()        // 早期控制台(printk 可用)
  ├── vfs_caches_init()     // VFS 缓存:dentry、inode
  ├── rest_init()           // 创建 kernel_init 线程(即 PID 1)

这里有个细节我特别想提一下:console_init() 之后,printk 才能输出到串口或屏幕。在此之前,内核如果崩了,你连日志都看不到。我在项目中遇到过内核在 setup_arch() 里挂掉,只能靠 JTAG 调试器抓 PC 指针来定位问题。

1.5 init 进程与用户态:PID 1 的诞生

rest_init() 会创建 kernel_init 线程,这个线程最终会变成用户空间的 1 号进程。

1 号进程是什么?取决于你用的 init 系统:

Init 系统 可执行文件 特点
SysV init /sbin/init 串行启动,依赖 rc 脚本
systemd /lib/systemd/systemd 并行启动,单元化管理
BusyBox init /sbin/init 轻量,嵌入式常用

我个人习惯用 systemd,不是因为它"先进",而是因为它的依赖管理和日志系统(journald)在排查启动问题时太方便了。曾经有一台服务器启动到一半卡住,我直接用 systemd-analyze blame 就找到了是某个 NFS 挂载超时导致的。

init 进程的核心工作:

  • 挂载 /proc、/sys、/dev、/run 等虚拟文件系统
  • 启动系统服务(网络、SSH、cron、docker 等)
  • 启动 getty 进程,等待用户登录

1.6 登录与 Shell:最后的临门一脚

init 启动 getty(或 agetty),getty 在终端上显示登录提示符。用户输入用户名密码后,login 程序验证身份,然后执行 Shell。

Shell 的启动顺序:

  1. login 读取 /etc/passwd,找到用户的 Shell 路径(通常是 /bin/bash)
  2. Shell 执行系统级配置文件(/etc/profile、/etc/bash.bashrc)
  3. Shell 执行用户级配置文件(~/.bash_profile、~/.bashrc)
  4. 显示提示符 $ 或 #

到这里,启动流程就走完了。你看到的那个 $,背后是几十万行代码的接力跑。

1.7 全流程可视化

下面这张图把整个流程串起来了。我建议你把它存下来,后面每一章都可以回来对照着看。

系统启动全流程总览 ① 固件阶段 BIOS / UEFI ② Bootloader GRUB / U-Boot ③ 内核初始化 start_kernel() ④ init 进程 PID 1 ⑤ 登录与 Shell getty → login → bash 固件子步骤 CPU 复位 POST 自检 引导设备枚举 加载 Bootloader Bootloader 子步骤 读取配置文件 加载内核映像 加载 initramfs 跳转到内核入口 内核初始化子步骤 setup_arch() mm_init() sched_init() rest_init() → PID 1 init 子步骤 挂载虚拟文件系统 启动系统服务 启动 getty 等待用户登录 💡 关键理解 每个阶段都是"接力棒"模式:前一个阶段准备好环境,后一个阶段在此基础上继续 启动卡死时,先判断卡在哪个阶段,再针对性排查

1.8 本章小结

启动流程说白了就是五级流水线:

  • 固件——让 CPU 找到可启动设备
  • Bootloader——把内核从磁盘搬到内存
  • 内核——初始化硬件和核心子系统
  • init——拉起用户空间的服务
  • Shell——给你一个交互入口

后面每一章,我们都会深入其中一个环节。下一章先讲固件——BIOS 和 UEFI 到底有什么区别?为什么 UEFI 是趋势?到时候我会结合我踩过的一个坑来聊。

一个提醒: 如果你现在用的是虚拟机或云服务器,启动流程可能被虚拟化层或云平台简化了。但底层逻辑不变。我建议你找一台物理机,用 dmesg 和 systemd-analyze 实际看一下启动日志,比看书管用得多。