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 的引导分区),把控制权交给它。
1.3 Bootloader 阶段:内核的搬运工
Bootloader 的任务很纯粹:把内核从磁盘搬到内存,然后跳过去执行。
常见的 Bootloader 有 GRUB(x86)、U-Boot(嵌入式)、systemd-boot(UEFI 环境)。它们的工作流程大同小异:
- 读取配置文件(如
grub.cfg或extlinux.conf) - 加载内核映像(
vmlinuz或Image)到内存 - 加载设备树(DTB)或 ACPI 表
- 加载 initramfs(初始内存文件系统)
- 设置启动参数(如
root=、console=) - 跳转到内核入口点
你想想看,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 的启动顺序:
- login 读取
/etc/passwd,找到用户的 Shell 路径(通常是/bin/bash) - Shell 执行系统级配置文件(
/etc/profile、/etc/bash.bashrc) - Shell 执行用户级配置文件(
~/.bash_profile、~/.bashrc) - 显示提示符
$或#
到这里,启动流程就走完了。你看到的那个 $,背后是几十万行代码的接力跑。
1.7 全流程可视化
下面这张图把整个流程串起来了。我建议你把它存下来,后面每一章都可以回来对照着看。
1.8 本章小结
启动流程说白了就是五级流水线:
- 固件——让 CPU 找到可启动设备
- Bootloader——把内核从磁盘搬到内存
- 内核——初始化硬件和核心子系统
- init——拉起用户空间的服务
- Shell——给你一个交互入口
后面每一章,我们都会深入其中一个环节。下一章先讲固件——BIOS 和 UEFI 到底有什么区别?为什么 UEFI 是趋势?到时候我会结合我踩过的一个坑来聊。
一个提醒: 如果你现在用的是虚拟机或云服务器,启动流程可能被虚拟化层或云平台简化了。但底层逻辑不变。我建议你找一台物理机,用 dmesg 和 systemd-analyze 实际看一下启动日志,比看书管用得多。