目录

Linux-19 从按下电源到 shell 提示符:每一环为什么必须存在

上一篇建立了「理解约束才能理解设计」的框架。这一篇用它分析启动流程 —— 启动是一条环环相扣的链条,每一环都是为了解决前一环留下的问题而存在的。

先看五个问题:

  1. 为什么需要引导器(bootloader)?固件不能直接加载内核吗?
  2. initramfs 为什么被发明?没有它会怎样?
  3. 内核镜像为什么叫 vmlinuzz 表示压缩)?谁来解压它?
  4. PID 1 为什么特殊?它退出会发生什么?
  5. 为什么现代系统能并行启动,SysV 时代却是串行的?

1. 根本难题:自举

1.1 鸡生蛋问题

启动的本质困难是:要运行程序,需要一个程序把它加载进内存;而那个加载程序自己也需要被加载。

   谁把内核读进内存?        -> 引导器
   谁把引导器读进内存?      -> 固件(BIOS/UEFI)
   谁把固件读进内存?        -> 它【本来就在】芯片里(ROM/Flash),CPU 上电后从固定地址开始取指
                              ^^^^^^^^^^^^^^^^^^^^^ 递归的终点

这个链条的终点必须是硬件层面的约定:x86 CPU 上电后进入实模式,从 0xFFFFFFF0 处取第一条指令,那里是固件 ROM 的映射。这是唯一不需要「被加载」的代码。

1.2 为什么必须分阶段

每一阶段的能力都受限于上一阶段留给它的空间,这是分阶段加载的根本原因:

   固件(ROM,几 MB)
      能力:只认最基本的存储访问,不认文件系统(BIOS)
      限制:只能读固定位置的固定大小数据
              |
              v
   第一阶段引导代码(MBR:446 字节!)
      能力:几乎没有 —— 446 字节连"读取一个文件"的逻辑都写不下
      任务:只能做一件事:把下一阶段读进来
              |
              v
   第二阶段引导器(GRUB core,几十 KB ~ 几 MB)
      能力:认识文件系统、能读配置文件、能显示菜单
      任务:加载内核 + initramfs,传递参数
              |
              v
   内核(几 MB ~ 几十 MB,压缩过)
      能力:完整的硬件抽象
      问题:但它还挂不上真正的根文件系统(见第 5 章)
              |
              v
   initramfs(内存中的临时根文件系统)
      任务:加载必要驱动,找到并挂载真正的根文件系统
              |
              v
   真正的根文件系统 + PID 1 + 用户空间

这是典型的「每一环解决上一环的限制」结构。 后面每一章就是逐环分析。

2. 固件阶段:BIOS 与 UEFI

2.1 BIOS 的约束造就了 MBR 的怪异

BIOS(Basic Input/Output System,1975 年设计)给操作系统留下的接口极其原始:

BIOS 的环境:
  ✗ CPU 处于【16 位实模式】(为了兼容 8086)
  ✗ 只能寻址 1MB 内存(20 位地址线)
  ✗ 不认识任何文件系统 —— 只能通过 INT 13h 中断「读第 N 个扇区」
  ✗ 每次只能读 512 字节

BIOS 的启动动作:
  ① 上电自检(POST)
  ② 按配置顺序遍历启动设备
  ③ 读取设备的【第 0 号扇区】(512 字节)到内存 0x7C00
  ④ 检查最后两字节是否为 0x55AA(有效标志)
  ⑤ 跳转到 0x7C00 执行

关键约束:第 0 号扇区只有 512 字节,而且要塞进分区表(11 篇 3.1 讲过):

   MBR(512 字节)
   +--------------------------------------+
   | 引导代码           446 字节           |  <- 能写的代码只有这么多
   +--------------------------------------+
   | 分区表项 1~4       每项 16 字节 = 64  |
   +--------------------------------------+
   | 0x55AA             2 字节             |
   +--------------------------------------+

446 字节能做什么? 几乎什么都做不了 —— 它连「解析 ext4 目录结构找到内核文件」的代码都放不下。所以它唯一能做的事就是:按预先记录的扇区号,把下一阶段的代码读进来

# 亲手看看 MBR 里有什么
sudo dd if=/dev/sda bs=512 count=1 2>/dev/null | xxd | head -5
# 00000000: eb63 9010 8ed0 bc00 b000 0000 ... 前面是引导代码
sudo dd if=/dev/sda bs=512 count=1 2>/dev/null | tail -c 66 | xxd
# 分区表 + 0x55AA

# 备份 MBR(重装/试验前的保命操作)
sudo dd if=/dev/sda of=/root/mbr.bak bs=512 count=1

这就是开篇第一问的答案:不是「不能让固件直接加载内核」,而是 BIOS 时代的固件根本没有能力找到内核文件 —— 它不认识文件系统。引导器存在的第一个理由就是填补「固件只会读扇区」与「内核是文件系统里的一个文件」之间的鸿沟

2.2 UEFI:把文件系统能力放进固件

UEFI(Unified Extensible Firmware Interface)从根本上改变了这个局面:

BIOS UEFI
CPU 模式 16 位实模式 32/64 位保护模式
内存寻址 1MB 全部
认识文件系统 ❌ 只能读扇区 原生支持 FAT32
引导代码位置 MBR 的 446 字节 ESP 分区里的 .efi 文件(大小不限)
分区表 MBR(2TB 上限) GPT
启动项管理 BIOS 设置里选设备 NVRAM 里的启动条目(可用 efibootmgr 管理)
安全启动 Secure Boot(签名验证)
驱动模型 有(UEFI 驱动、网络栈、shell)

因为 UEFI 认识 FAT32 文件系统,「引导代码只有 446 字节」这个约束彻底消失了

# UEFI 系统上,引导器就是 ESP 分区里的一个普通文件
lsblk -f | grep -i vfat
# └─sda1 vfat  FAT32  ESP   1234-ABCD  /boot/efi

sudo ls -R /boot/efi/EFI/
# /boot/efi/EFI/ubuntu/shimx64.efi     <- Secure Boot 的第一跳
# /boot/efi/EFI/ubuntu/grubx64.efi     <- GRUB 本体(几 MB,随便多大都行)
# /boot/efi/EFI/BOOT/BOOTX64.EFI       <- 默认回退路径

# 查看/管理 UEFI 启动条目(存在主板 NVRAM 里)
efibootmgr -v
# BootCurrent: 0000
# BootOrder: 0000,0001,0002
# Boot0000* ubuntu    HD(1,GPT,1234-ABCD,...)/File(\EFI\ubuntu\shimx64.efi)
# Boot0001* UEFI OS   ...
sudo efibootmgr -o 0001,0000          # 改启动顺序
sudo efibootmgr -b 0002 -B            # 删除条目 2

# 判断当前系统是 UEFI 还是 BIOS 启动
[[ -d /sys/firmware/efi ]] && echo "UEFI 启动" || echo "BIOS/Legacy 启动"
sudo dmidecode -t bios | head          # 固件版本信息

2.3 Secure Boot 的信任链

Secure Boot 的设计目标是阻止启动被篡改的引导器或内核(防 bootkit)。它建立了一条签名验证链:

   固件里内置的公钥(厂商的 PK/KEK/db)
        |  验证签名
        v
   shim(发行版签名的小程序,被微软的密钥签名)
        |  验证签名(用发行版自己的密钥)
        v
   GRUB(发行版签名)
        |  验证签名
        v
   内核(发行版签名)
        |  验证签名
        v
   内核模块(发行版签名;自编译模块需要自己签名或注册 MOK)
# 查看 Secure Boot 状态
mokutil --sb-state
# SecureBoot enabled
sudo bootctl status | head -20         # systemd 的工具,信息更全

# ⚠️ 这解释了一个常见现象:Secure Boot 开启时自编译的内核模块无法加载
sudo modprobe mymodule
# modprobe: ERROR: could not insert 'mymodule': Key was rejected by service
dmesg | tail -2
# Lockdown: modprobe: unsigned module loading is restricted; see man kernel_lockdown.7

# 解法(三选一)
# ① 用 MOK 注册自己的签名密钥(推荐)
sudo mokutil --import MOK.der
# ② 给模块签名(DKMS 可以自动做)
# ③ 关闭 Secure Boot(牺牲安全性)

3. 引导器阶段:GRUB

3.1 引导器存在的四个理由

即使在 UEFI 时代(固件已经能读文件),引导器仍然有价值:

① 选择内核 —— 升级后保留旧内核,新内核起不来时可以回退(这是救命功能)
② 传递参数 —— 内核命令行(root=、init=、单用户模式、调试参数)
③ 多系统引导 —— Linux/Windows 共存
④ 加载 initramfs —— 内核和 initramfs 是两个文件,需要有人同时加载并告知内核
# 看看当前内核是用什么参数启动的
cat /proc/cmdline
# BOOT_IMAGE=/vmlinuz-5.15.0-91 root=UUID=abc123 ro quiet splash
#                                ^^^^^^^^^^^^^^^ 根文件系统在哪(11 篇讲的 UUID)
#                                                ^^ 先以只读挂载(fsck 需要)

# 可用的内核(升级后会保留几个旧的)
ls -l /boot/vmlinuz-*
ls -l /boot/initrd.img-*

3.2 GRUB 的分阶段

在 BIOS 模式下,GRUB 自己也必须分阶段(因为 446 字节的限制):

   MBR 里的 boot.img(446 字节)
      任务:读取 core.img 的第一个扇区(扇区号被写死在自己里面)
              |
              v
   core.img(存在 MBR 后的「引导间隙」,通常是第 1~2047 扇区,约 1MB)
      内容:文件系统驱动、终端驱动、必要模块
      任务:现在能读文件系统了 -> 加载 /boot/grub/ 下的模块和配置
              |
              v
   /boot/grub/grub.cfg + 各种 .mod 模块
      任务:显示菜单,加载内核和 initramfs
# 「引导间隙」的由来:MBR 之后到第一个分区之前的空白
sudo fdisk -l /dev/sda | grep -A2 Device
# /dev/sda1  2048  ...
#            ^^^^ 第一个分区从第 2048 扇区开始(1MB 对齐),
#                 前面的 2047 个扇区(约 1MB)留给 core.img
# ⚠️ 这是为什么老式分区工具从 63 扇区开始分区会导致 GRUB2 装不下

UEFI 模式下不需要这些技巧:grubx64.efi 就是 ESP 里的一个完整文件。

3.3 配置与救援

# ⚠️ 不要直接编辑 grub.cfg(它是【生成】的,升级内核会被覆盖)
sudo vim /etc/default/grub              # ✅ 改这里
# GRUB_TIMEOUT=5
# GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"
# GRUB_CMDLINE_LINUX=""
# GRUB_DEFAULT=0                        # 默认启动第几项(或 "saved")
sudo ls /etc/grub.d/                    # 自定义菜单项放这里(40_custom)

sudo update-grub                         # Debian/Ubuntu:重新生成 grub.cfg
sudo grub2-mkconfig -o /boot/grub2/grub.cfg    # RHEL 系
sudo grub-install /dev/sda               # 重装引导器到 MBR(修复引导时用)
sudo grub-install --efi-directory=/boot/efi   # UEFI 模式

# 查看/设置默认启动项
sudo grub-set-default 0
sudo grub-reboot 1                       # ✅ 只有【下一次】启动用第 1 项(测试新内核的安全做法)

GRUB 是最重要的救援入口(11 篇 6.4 用过):

开机时按住 Shift(BIOS)或 Esc(UEFI)进入菜单
   |
   ├─ e 键:编辑当前启动项(临时,不写盘)
   |     在 linux 那一行末尾追加参数:
   |       single                    进单用户模式
   |       systemd.unit=rescue.target       救援模式
   |       systemd.unit=emergency.target    紧急模式(只挂根分区)
   |       init=/bin/bash            ✅ 直接给一个 shell(终极救援)
   |       rd.break                  在 initramfs 阶段停下(RHEL)
   |       ro / rw                   根分区读写模式
   |       nomodeset                 显卡驱动有问题时
   |       systemd.mask=xxx.service  屏蔽某个起不来的服务
   |     然后 Ctrl-X 或 F10 启动
   |
   ├─ Advanced options:选择旧内核(新内核起不来时的救命选项)
   |
   └─ c 键:进入 GRUB 命令行(连菜单都坏了时手动引导)
         grub> ls                      # 列出分区
         grub> set root=(hd0,gpt2)
         grub> linux /vmlinuz-5.15 root=/dev/sda2 ro
         grub> initrd /initrd.img-5.15
         grub> boot
# 用 init=/bin/bash 救援后的标准操作
mount -o remount,rw /                   # 根分区默认只读
vim /etc/fstab                          # 修复配置
passwd root                             # 重设密码
sync && reboot -f                       # 强制重启(正常的 reboot 需要 init)

3.4 替代方案

# systemd-boot(原 gummiboot):只支持 UEFI,极简
bootctl status
sudo bootctl install
ls /boot/loader/entries/                # 每个内核一个 .conf 文件(比 grub.cfg 简单得多)
# 优势:配置极简、启动快;劣势:不支持 BIOS、功能少

# kexec:不经过固件和引导器直接换内核(重启快 10 倍以上)
sudo kexec -l /boot/vmlinuz-5.15 --initrd=/boot/initrd.img-5.15 \
           --command-line="$(cat /proc/cmdline)"
sudo kexec -e                            # 立即切换(会跳过全部硬件自检)
# 用途:大规模集群快速重启、kdump 崩溃转储(崩溃时用备用内核收集现场)
systemctl status kdump

4. 内核阶段

4.1 开篇第三问:vmlinuz 谁来解压

ls -l /boot/vmlinuz-$(uname -r)
# -rw-r--r-- 1 root root 11.5M ... /boot/vmlinuz-5.15.0-91-generic
#                                        ^ 这个 z 表示 compressed(压缩)

file /boot/vmlinuz-$(uname -r)
# Linux kernel x86 boot executable bzImage, version 5.15.0-91-generic ...
#                                           ^^^^^^^ big zImage

几个名字的来历

名字 含义
vmlinux 未压缩的内核 ELF 文件(vm 指虚拟内存支持,Unix 传统命名)
vmlinuz 压缩后的可引导镜像(z = compressed,类比 gzip
zImage 早期的压缩内核格式,上限 512KB(放在低端内存)
bzImage big zImage —— 突破 512KB 限制(b 是 big,与 bzip2 无关!)

为什么要压缩? 历史原因是软盘容量(1.44MB 要装下内核);现在的理由是减少从磁盘读取的时间 —— 解压的 CPU 开销小于多读几 MB 的 I/O 开销。

谁来解压?内核镜像自己。 bzImage 的结构是「一小段未压缩的自解压代码 + 压缩的内核本体」:

   bzImage 的结构
   +--------------------------------------------------+
   | 实模式引导代码(setup.bin,未压缩)                |  <- 固件/引导器跳到这里
   |   任务:切换到保护模式,准备解压环境                 |
   +--------------------------------------------------+
   | 解压器(未压缩的一小段代码)                        |
   |   任务:把后面的内核本体解压到内存                   |
   +--------------------------------------------------+
   | 压缩的 vmlinux(gzip/lz4/zstd/xz 之一)            |
   +--------------------------------------------------+

# 所以启动时你会看到这一行(dmesg 的第一条)
dmesg | head -3
# [    0.000000] Linux version 5.15.0-91-generic ...
# 更早的 "Decompressing Linux... Parsing ELF... done." 只在屏幕上闪现,不进 dmesg
# 查看内核用什么压缩算法(CONFIG_KERNEL_*)
grep -E 'CONFIG_KERNEL_(GZIP|LZ4|ZSTD|XZ|LZO)=y' /boot/config-$(uname -r)
# CONFIG_KERNEL_ZSTD=y                  <- 现代内核偏向 zstd(压缩比好且解压快,12 篇讲过)

# 从 vmlinuz 里提取未压缩的 vmlinux(调试用)
/usr/src/linux/scripts/extract-vmlinux /boot/vmlinuz-$(uname -r) > /tmp/vmlinux
file /tmp/vmlinux
# ELF 64-bit LSB executable, x86-64, statically linked, not stripped

4.2 模式切换与 start_kernel

   实模式(16 位,1MB 寻址)           <- 固件交给我们时的状态
        | 设置 GDT、开启 A20、切换 CR0 的 PE 位
        v
   保护模式(32 位)
        | 设置页表,开启 PAE/长模式(CR4/EFER)
        v
   长模式(64 位)                     <- 现代内核真正运行的模式
        |
        v
   解压内核本体到内存
        |
        v
   start_kernel()                     <- C 代码的入口(init/main.c)

start_kernel() 里发生的事(顺序很有信息量)

setup_arch()            架构相关初始化(解析 e820 内存图、设置 CPU 特性)
                        ↓
mm_init()               内存管理子系统上线(buddy 分配器可用,25 篇讲)
                        ↓
sched_init()            调度器初始化(此时才有"进程"的概念,27 篇讲)
                        ↓
early_irq_init()        中断子系统
trap_init()             异常处理
time_init()             时钟源
                        ↓
rest_init()             创建两个内核线程:
                          ├─ PID 1:kernel_init  -> 后面会 exec 用户态的 /sbin/init
                          └─ PID 2:kthreadd     -> 所有内核线程的父进程
                        ↓
                        原来的启动流程变成 PID 0(swapper/idle 进程)
# 验证这三个特殊进程
ps -p 1,2 -o pid,ppid,comm
#   PID  PPID COMMAND
#     1     0 systemd            <- 用户态的 init(由 kernel_init exec 而来)
#     2     0 kthreadd           <- 内核线程的祖先
# PID 0 是 idle 进程,ps 看不到(它不在进程链表里,每个 CPU 一个)

# 内核线程都在方括号里(13 篇 2.2 提过)
ps -eo pid,ppid,comm | awk '$2==2' | head -8
#     3     2 rcu_gp
#     4     2 rcu_par_gp
#    11     2 migration/0
#    13     2 ksoftirqd/0        <- 软中断处理线程(15 篇的 si 就是它在跑)
#    14     2 kworker/0:1
# 看内核启动各阶段的耗时
dmesg -T | head -40
sudo dmesg | grep -E 'Booting|Command line|Memory:|Freeing unused'
# [    0.000000] Command line: BOOT_IMAGE=/vmlinuz-5.15 root=UUID=... ro quiet
# [    0.500000] Memory: 16265816K/16777216K available
# [    2.100000] Freeing unused kernel image (initmem) memory: 2696K
#                ^^^^^^^^^^^^^^^^^^^^^^^^^^^ 只在启动时用的代码被释放(__init 段)
systemd-analyze                          # 内核阶段与用户态阶段各花了多久

5. initramfs:为什么被发明

5.1 开篇第二问:一个鸡生蛋问题

内核解压完成、子系统初始化完毕之后,它要做一件事:挂载根文件系统/),然后执行里面的 /sbin/init

问题来了:

   要挂载根文件系统(假设在 /dev/nvme0n1p2,xfs 格式,还在 LVM 上)
        需要:NVMe 驱动 + LVM 支持 + xfs 驱动
              |
              v
   这些驱动在哪?
        答:作为内核模块(.ko 文件)存在 /lib/modules/... 里
              |
              v
   /lib/modules 在哪?
        答:在【根文件系统】里
              |
              v
   ❌ 死锁:要挂根文件系统需要驱动,驱动在根文件系统里

两种解法

   解法 A:把所有可能用到的驱动【编译进内核】(built-in,不做模块)
      ✗ 内核镜像会极其庞大(要包含所有存储控制器、所有文件系统、LVM、加密、RAID…)
      ✗ 每次加新硬件支持都要重编内核
      ✗ 发行版无法做一个「通用内核」适配所有机器

   解法 B:先挂一个【临时的、内存里的】根文件系统,它自带必要驱动,
           用它去找到并挂载真正的根文件系统,然后「切换过去」
      ✅ 内核可以保持精简,驱动按需加载
      ✅ 发行版能做通用内核 + 按机器生成的 initramfs
      ✅ 这就是 initramfs

5.2 initrd 与 initramfs 的区别

这两个名字经常混用,但机制不同:

initrd(旧,2.4 时代) initramfs(新,2.6+)
本质 一个块设备镜像(ext2 等格式) 一个 cpio 归档(gzip/zstd 压缩)
内核如何使用 挂成 /dev/ram0 这个 ramdisk,再挂载它 直接解开到 tmpfs(rootfs)
需要文件系统驱动 ✅ 需要(要能读 ext2) ❌ 不需要(cpio 解压是内核内置能力)
大小固定 ✅ 预分配固定大小 ❌ 按需增长
双重缓存 ⚠️ 有(ramdisk + page cache) ✅ 无
切换根的方式 pivot_root switch_root(直接换掉 rootfs 内容)
# 现在的文件虽然还叫 initrd.img(历史命名),但内容其实是 initramfs
file /boot/initrd.img-$(uname -r)
# Zstandard compressed data  或  gzip compressed data
lsinitramfs /boot/initrd.img-$(uname -r) | head    # Debian
lsinitrd /boot/initramfs-$(uname -r).img | head    # RHEL

5.3 拆开看看里面有什么

mkdir -p /tmp/initramfs && cd /tmp/initramfs
# Debian/Ubuntu
sudo unmkinitramfs /boot/initrd.img-$(uname -r) .
# 通用方式(手动解)
zstdcat /boot/initrd.img-$(uname -r) | cpio -idmv 2>/dev/null
# 或 gzip 的:  zcat /boot/initrd.img-... | cpio -idmv

ls
# bin  conf  cryptroot  etc  init  lib  run  sbin  scripts  usr  var

ls bin/ | head -20
# busybox(或 一堆命令的软链接)、blkid、cat、mount、modprobe、sh …
#  ^^ 一个极简的用户空间,通常靠 busybox 提供所有命令(01 篇提过它)

cat init | head -20                       # ✅ initramfs 的入口脚本(PID 1 最初执行的就是它)
find lib/modules -name '*.ko*' | head     # 里面带的驱动
# lib/modules/5.15.0/kernel/drivers/nvme/host/nvme.ko.zst
# lib/modules/5.15.0/kernel/fs/xfs/xfs.ko.zst
# lib/modules/5.15.0/kernel/drivers/md/dm-crypt.ko.zst
#  ^^ 正是「挂载根文件系统所必需」的那些

它的完整工作流程

   ① 内核把 initramfs 的 cpio 解开到 tmpfs,作为初始的 /
   ② 内核执行 /init(这时它就是 PID 1)
   ③ /init 脚本做的事:
        - 挂载 /proc /sys /dev(后续操作需要)
        - 根据 /proc/cmdline 里的 root= 参数确定目标
        - modprobe 必要的驱动(存储控制器、文件系统、LVM、dm-crypt)
        - 如果是加密盘:提示输入密码,解锁(cryptsetup)
        - 如果是 LVM:扫描并激活卷组(vgchange -ay)
        - 如果是网络存储:配置网络,连接 iSCSI/NBD/NFS
        - 等待设备出现(udev 事件)
        - fsck(如果需要)
        - 把真正的根文件系统挂到 /root(临时挂载点)
   ④ switch_root /root /sbin/init
        - 把 /root 变成新的 /
        - 释放 initramfs 占用的内存
        - exec /sbin/init(PID 仍然是 1!)
   ⑤ 真正的 init(systemd)开始接管
# 观察这个切换过程
sudo journalctl -b -o short-monotonic | head -40
# 早期的日志来自 initramfs 阶段
sudo journalctl -b | grep -iE 'switch_root|switching root'
# systemd[1]: Switching root.

# 内核参数里可以让它在切换前停下(RHEL/Fedora 的 dracut)
# 在 GRUB 里加 rd.break -> 进入 initramfs 的 shell,此时真实根挂在 /sysroot

5.4 什么时候真的需要 initramfs

必须要:
  ✓ 根文件系统在 LVM / RAID / 加密卷(dm-crypt)上
  ✓ 根文件系统在需要额外驱动的设备上(NVMe、某些 RAID 卡、iSCSI、NBD)
  ✓ 根文件系统类型的驱动是模块(如 btrfs、xfs 在某些发行版里是模块)
  ✓ 需要在挂载根之前做特殊操作(网络启动、resume from hibernate)

可以不要:
  ✓ 内核把所有必要驱动都编译进去了(built-in)
  ✓ 根文件系统是简单的 ext4 在直连的 SATA 盘上
  → 嵌入式系统、精简的容器宿主机、自编译内核常常不用 initramfs
# 重建 initramfs(换了硬件、加了驱动、改了配置后必做)
sudo update-initramfs -u                       # 当前内核(Debian/Ubuntu)
sudo update-initramfs -u -k all                # 所有内核
sudo dracut -f                                  # RHEL/Fedora/SUSE
sudo dracut -f --kver 5.15.0-91
sudo mkinitcpio -P                              # Arch

# 控制内容多少
# /etc/initramfs-tools/initramfs.conf
# MODULES=most        <- 包含大量驱动(通用,体积大)
# MODULES=dep         <- ✅ 只包含当前硬件需要的(体积小,但换硬件可能起不来)

5.5 这解释了 03 篇的 usrmerge

03 篇讲过:/bin/sbin/lib 现在都是指向 /usr/ 的软链接(usrmerge)。当时说「理由是 initramfs 让『启动必需』的划分失效了」,现在可以说清楚了:

   传统划分的逻辑(1970s ~ 2000s):
      /bin /sbin /lib   放在根分区,【启动早期必需】
      /usr              可能是独立分区甚至 NFS,挂载时机晚
      → 所以「启动必需的东西」必须放在 /bin 而不是 /usr/bin

   initramfs 出现后:
      挂载所有分区(包括 /usr)的工作【全部由 initramfs 完成】
      等真正的根文件系统就位时,/usr 早就挂好了
      → 「启动必需」的划分失去了技术依据
      → 于是 usrmerge:把 /bin /sbin /lib 合并进 /usr,只留软链接

这是一个很好的例子:一个底层机制(initramfs)的出现,让一个延续了三十年的目录布局约定失去了存在理由。 后面讲 systemd(37 篇)时会看到同样的模式。

5.6 故障:initramfs 里缺东西

这是「机器起不来」的常见原因之一:

   现象:内核加载完成后卡住,或者进入类似这样的提示
      Gave up waiting for root device.  Common problems:
       - Boot args (cat /proc/cmdline)
       - Check rootdelay= (did the system wait long enough?)
      ALERT!  UUID=xxx does not exist.  Dropping to a shell!
      (initramfs) _
                  ^^ 你现在在 initramfs 的 busybox shell 里

   原因:initramfs 里没有能访问根设备的驱动
      - 换了存储控制器(SATA -> NVMe / 加了 RAID 卡)
      - 把根文件系统迁到了 LVM/加密卷但没重建 initramfs
      - MODULES=dep 生成的 initramfs 被拷到了别的机器
      - 内核升级后 initramfs 生成失败(磁盘满是常见原因!)
# 在 (initramfs) 提示符下的排查
(initramfs) cat /proc/cmdline              # 确认 root= 参数
(initramfs) ls /dev/                        # 根设备的节点存在吗?
(initramfs) blkid                           # 能识别到分区吗?
(initramfs) lsmod                           # 加载了哪些驱动
(initramfs) modprobe nvme                   # 手动加载试试
(initramfs) vgchange -ay                    # 手动激活 LVM
(initramfs) cryptsetup open /dev/sda2 root  # 手动解锁加密盘
(initramfs) mount /dev/mapper/vg-root /root && exit    # 挂上后 exit 会继续启动

# 事后修复(用旧内核或 live CD 启动后)
sudo update-initramfs -u -k 5.15.0-91
# 确认 /boot 有足够空间(initramfs 生成失败的头号原因)
df -h /boot

6. PID 1 与 init 系统

6.1 开篇第四问:PID 1 为什么特殊

内核在 rest_init() 里创建的第一个进程就是 PID 1,它随后 exec 用户态的 /sbin/init内核对 PID 1 有三条特殊规定

① PID 1 退出 = 内核 panic
     内核认为「init 死了系统就没救了」,直接崩溃
② PID 1 收养所有孤儿进程
     父进程先死的进程会被 reparent 到 PID 1,由它负责 wait() 回收(13 篇 1.3)
③ PID 1 【忽略】所有它没有显式注册处理函数的信号
     普通进程收到 SIGTERM 会被默认终止;PID 1 不会 —— 这是为了防止误杀 init
# ① 验证「PID 1 退出会 panic」(只在虚拟机/容器里试!)
sudo kill -9 1
# 什么都不会发生 —— 因为规定 ③:PID 1 忽略未注册的信号,包括 SIGKILL?
# 注意:SIGKILL/SIGSTOP 对普通进程无法被忽略,但对 PID 1 内核也会跳过默认动作

# 在容器里可以安全验证
docker run --rm -it alpine sh -c 'kill -9 1; echo "还活着"'
# 容器会退出(因为 PID 1 就是那个 sh,它自己退出了)

# ② 验证收养孤儿(13 篇做过)
bash -c 'sleep 300 & exit'
ps -o pid,ppid,comm -C sleep
#   PID  PPID COMMAND
#  9001     1 sleep            <- PPID 变成 1

# ③ 验证信号行为
# 普通进程:默认终止
sleep 300 & kill -TERM $!; wait   # 立即被杀
# PID 1(systemd):注册了处理函数,SIGTERM 的含义被重定义
sudo kill -TERM 1                  # systemd 把它解释为「重新执行自己」而不是退出
sudo systemctl daemon-reexec       # 等价的正规做法

容器里的推论(13 篇 3.2 讲过现象,这里是设计原因):

# 容器里 PID 1 是你的应用,于是它继承了 PID 1 的全部特殊性:
#   ① 它必须负责回收孤儿进程 —— 不做就会积累僵尸
#   ② 它【忽略】未注册的信号 —— 没有注册 SIGTERM 处理函数的程序
#      收到 docker stop 的 SIGTERM 时【什么都不会发生】,
#      10 秒后被 SIGKILL 强杀(这就是「容器 stop 很慢」的原因)

# ✅ 两个对策
docker run --init myimage          # 注入 tini 作为 PID 1(负责转发信号 + 回收僵尸)
# 或者应用自己处理(13 篇 5.3 的 Go 优雅退出代码)

6.2 开篇第五问:SysV 为什么串行

SysV init(1983,System V)的模型

# 一切基于 runlevel(运行级别)
/etc/inittab                      # 定义默认 runlevel 和各级别要做什么
/etc/rc.d/rc3.d/                  # runlevel 3 要启动的服务
ls /etc/rc3.d/                    # (现代系统上多是兼容层)
# S01foo  S20nginx  S30mysql  K10bar
# ^^^^^^^ S=start K=kill,数字决定【执行顺序】

# 启动过程:
#   init 读 /etc/inittab -> 确定 runlevel -> 执行 /etc/rc.d/rc 3
#     -> 按【文件名的数字顺序】依次执行 S* 脚本
#        S01 执行完,才执行 S20,然后 S30 …… 严格串行

为什么必须串行? 因为依赖关系没有被显式表达,只能靠编号隐含

   S20nginx 需要网络就绪,S10network 提供网络
        ↓
   靠「10 < 20」这个约定来保证顺序
        ↓
   但脚本无法声明「我依赖谁」,init 也不知道任何依赖关系
        ↓
   ❌ 唯一安全的策略是:按编号一个一个来,前一个完全结束再启动下一个
        ↓
   后果:① 慢(每个脚本要等前一个跑完,即使它们毫无关系)
        ② 脆弱(插入新服务要小心选编号,两个服务编号相同顺序不确定)
        ③ 无法表达「A 需要 B,但 B 失败时 A 仍可启动」这类语义

systemd 的并行化靠三个机制

① 声明式依赖(14 篇 1.4)
     Wants= / Requires= / After= / Before=
     -> systemd 能构建【依赖图】,没有依赖关系的服务可以【同时启动】

② socket 激活(14 篇第 9 章)
     systemd 先创建好所有监听 socket,再并行启动所有服务。
     即使 B 还没启动,A 连接 B 的请求也会被缓冲在 socket 里。
     -> 消除了「必须等 B 完全就绪」的时序依赖,这是并行化的关键突破

③ 按需启动
     不常用的服务等到第一次被访问才启动(socket/path/timer 激活)
# 对比效果
systemd-analyze
# Startup finished in 3.2s (kernel) + 8.5s (userspace) = 11.7s
#                                     ^^^^^^ SysV 时代同样的服务集合通常要 30~60s

systemd-analyze critical-chain           # 真正影响启动时间的【串行链条】
systemd-analyze blame                     # 各服务耗时排名(14 篇 2.5)
systemd-analyze plot > boot.svg            # 可视化时序图(能直观看到并行)

6.3 init 系统的演进

SysV init(1983) Upstart(2006) systemd(2010)
模型 runlevel + 编号脚本 事件驱动 依赖图 + cgroup
并行 ❌ 串行 ⚠️ 部分 ✅ 完全
依赖表达 ❌ 靠编号 事件(started X ✅ 声明式
进程追踪 ❌ 靠 PID 文件(不可靠) 靠 ptrace(脆弱) cgroup(进程无法逃逸)
服务定义 shell 脚本(几百行) 配置文件 ✅ 声明式 unit(十几行)
按需启动 ⚠️ ✅ socket/path/timer 激活
采用 传统 Unix Ubuntu 6.10~14.10 几乎所有主流发行版

「用 cgroup 追踪进程」是 systemd 一个被低估的设计(14 篇提过):

# SysV 时代靠 PID 文件判断服务是否在跑,它有一堆问题:
#   - PID 文件可能过期(进程崩了但文件还在)
#   - PID 可能被复用(新进程恰好拿到同一个 PID)
#   - 服务 fork 出的子进程无人追踪(stop 之后残留进程继续跑)

# systemd 把每个服务放进一个 cgroup —— 进程【无法逃逸】
systemctl status nginx | tail -6
#      CGroup: /system.slice/nginx.service
#              ├─1234 nginx: master process
#              ├─1235 nginx: worker process
#              └─1236 nginx: worker process
#              ^^ 无论 fork 多少层,全部在这个 cgroup 里,stop 时一网打尽

cat /sys/fs/cgroup/system.slice/nginx.service/cgroup.procs
systemd-cgls                              # 整个 cgroup 树

7. 登录与 shell

   systemd(PID 1)
        |
        ├─ 启动 getty@tty1.service          <- 物理终端
        |     └─ agetty:打开 /dev/tty1,显示 login 提示
        |           └─ login:验证身份(走 PAM)
        |                 └─ exec 用户的 shell(bash)
        |
        └─ 启动 sshd.service                 <- 远程登录(16 篇)
              └─ sshd 收到连接 -> fork 子进程 -> 认证(PAM)
                    └─ 分配 pty,exec 用户的 shell

7.1 为什么需要 getty

getty = get tty(获取终端)。它的存在是历史遗产:

   1970 年代:一台主机接几十台电传打字机/哑终端
      每个终端需要一个进程来:
        ① 打开对应的设备文件(/dev/tty1、/dev/ttyS0…)
        ② 设置终端参数(波特率、字符集、回显)—— 不同型号终端参数不同
        ③ 显示登录提示,读取用户名
        ④ 交给 login 处理认证
      -> 这就是 getty,一个终端一个 getty 进程

   今天:
      物理终端只剩虚拟控制台(tty1~tty6)和串口(服务器的带外管理)
      绝大多数登录来自 sshd(它自己处理 pty,不用 getty)
systemctl status getty@tty1.service
ls /dev/tty*                              # tty1-63 虚拟控制台,ttyS0 串口
ps -ef | grep -E 'getty|agetty'
who -a                                    # 哪些终端有人登录

# 串口控制台仍然重要(云主机的救援入口、物理服务器的带外管理)
sudo systemctl enable --now serial-getty@ttyS0.service
# 内核参数加 console=ttyS0,115200 让内核日志也输出到串口

7.2 PAM:可插拔的认证

PAM(Pluggable Authentication Modules)是「机制与策略分离」的又一个例子(18 篇第 4 章):

   问题:login、sshd、sudo、su、cron 都需要「验证用户身份」
        如果每个程序自己实现,那么要支持一种新的认证方式
        (LDAP、Kerberos、双因素、指纹)就得改所有程序

   PAM 的解法:程序只调用 PAM API,具体怎么验证由配置决定
ls /etc/pam.d/                            # 每个服务一个配置文件
# common-auth  common-account  common-password  common-session
# login  sshd  sudo  su  cron  systemd-user

cat /etc/pam.d/sshd | grep -v '^#' | grep -v '^$' | head
# auth       required     pam_env.so
# auth       substack     password-auth
# account    required     pam_nologin.so
# session    required     pam_loginuid.so
# session    optional     pam_motd.so             <- 16 篇提过它可能让登录变慢
# session    required     pam_limits.so           <- ✅ 13 篇的 limits.conf 就靠它生效!
#            ^^^^^^^^     ^^^^^^^^^^^^^^^ 模块
#            控制标志(required/requisite/sufficient/optional)

# 四种管理组(一次登录要过四道关)
# auth      验证身份(密码、密钥、双因素)
# account   检查账号是否可用(是否过期、是否允许此时登录)
# password  修改密码时的策略(复杂度要求)
# session   建立/销毁会话(挂载家目录、设 ulimit、记日志、显示 motd)

这解释了 13 篇的一个坑/etc/security/limits.conf 是通过 pam_limits.so 生效的,而它只在 PAM 的 session 阶段被调用 —— systemd 启动的服务不走 PAM(没有登录会话),所以那个文件对服务无效,必须用 unit 里的 LimitNOFILE=

8. 排查启动问题

8.1 按阶段定位

卡在哪 现象 排查
固件 没有任何输出、不进 BIOS 画面 硬件问题、内存/显卡故障;查主板诊断灯
引导器 黑屏、GRUB rescue>no such partition 分区表损坏、GRUB 配置错、磁盘顺序变了 → live CD 修复
内核加载 卡在 Loading initial ramdisk 之后无输出 nomodeset、去掉 quiet splash 看真实输出
initramfs Gave up waiting for root device / (initramfs) 提示符 驱动缺失、root= 错、LVM/加密未激活(见 5.6)
早期用户态 卡在某个服务、进 emergency mode /etc/fstab(11 篇 6.4)、服务依赖死锁
登录 能启动但登不进去 PAM 配置、shell 不存在、家目录权限、磁盘满

8.2 关键命令

# ── 去掉「安静启动」看真实过程(临时改 GRUB 启动项)──
# 删掉 quiet splash,加上:
#   loglevel=7 systemd.log_level=debug systemd.show_status=true

# ── 事后分析 ──
sudo journalctl -b                        # 本次启动的全部日志
sudo journalctl -b -1                     # ✅ 【上一次】启动(查崩溃原因,14 篇 6.1)
sudo journalctl -b -1 -p err              # 上次启动的错误
sudo journalctl -b -k                     # 只看内核消息(等价 dmesg)
sudo journalctl --list-boots               # 有哪几次启动记录
sudo dmesg -T | grep -iE 'error|fail|warn' | head -20

systemctl --failed                         # ✅ 哪些服务失败了
systemd-analyze blame | head -10           # 谁拖慢了启动
systemd-analyze critical-chain             # 关键路径
systemctl list-jobs                        # ✅ 当前卡住的启动任务

# ── 内核参数救援(GRUB 里临时加)──
systemd.unit=rescue.target                 # 救援模式(单用户,挂载所有分区)
systemd.unit=emergency.target              # 紧急模式(只挂根分区,只有 shell)
init=/bin/bash                             # ✅ 终极手段:直接给 shell
rd.break                                   # 停在 initramfs(RHEL/dracut)
systemd.mask=problematic.service           # 屏蔽某个起不来的服务
systemd.debug-shell=1                      # 在 tty9 开一个 root shell 备用
nomodeset                                  # 显卡驱动问题
fsck.mode=force fsck.repair=yes            # 强制检查修复文件系统

# ── 磁盘满导致起不来(很常见)──
# 表现:能进 emergency shell,但服务全失败
df -h; df -i                               # ✅ 空间和 inode 都要看(11 篇第 10 章)

8.3 加速启动

systemd-analyze blame | head -10
# 4.5s NetworkManager-wait-online.service   <- 最常见的元凶
# 2.1s snapd.service
# 1.2s docker.service

# ① 禁掉不需要的服务
sudo systemctl disable --now snapd.service snapd.socket
sudo systemctl disable NetworkManager-wait-online.service   # ⚠️ 需要联网的服务会受影响
sudo systemctl mask plymouth-quit-wait.service              # 开机动画

# ② 服务器上禁掉图形相关
sudo systemctl set-default multi-user.target

# ③ 缩短 GRUB 等待
sudo sed -i 's/^GRUB_TIMEOUT=.*/GRUB_TIMEOUT=1/' /etc/default/grub && sudo update-grub

# ④ 减小 initramfs(只带当前硬件需要的驱动)
# /etc/initramfs-tools/initramfs.conf: MODULES=dep
# ⚠️ 代价:换硬件或迁移磁盘后可能起不来

# ⑤ 用 kexec 跳过固件自检(大规模重启场景,见 3.4)

# ⑥ 分析内核阶段(如果 kernel 部分很慢)
sudo dmesg -T | awk '{print}' | head -50   # 看哪个阶段有长时间空隙
# 常见:磁盘/网卡探测超时、ACPI 问题、随机数熵不足(现代内核已少见)

9. 面试题

Q:为什么需要引导器?固件不能直接加载内核吗?

不是「不能」,而是 BIOS 时代的固件没有能力找到内核文件 —— 它不认识任何文件系统,只能通过 INT 13h 中断「读第 N 个扇区」,而且一次只读 512 字节。内核是 ext4/xfs 文件系统里的一个文件,固件无从下手。

更要命的约束是:BIOS 只读第 0 号扇区,而那 512 字节里还要塞进分区表,留给引导代码的只有 446 字节 —— 连「解析 ext4 目录结构」的代码都写不下。所以引导必须分阶段:446 字节的第一阶段只做一件事(按写死的扇区号读入下一阶段),第二阶段(GRUB core,约 1MB,放在 MBR 后的「引导间隙」)才有能力读文件系统。

UEFI 改变了这个局面:它原生支持 FAT32,引导器就是 ESP 分区里的一个普通 .efi 文件,大小不限。但引导器仍有四个存在理由:选择内核(新内核起不来时回退,这是救命功能)、传递内核参数多系统引导同时加载 initramfs

Q:initramfs 为什么被发明?它解决的是什么问题?

一个鸡生蛋问题:内核要挂载根文件系统,需要对应的驱动(NVMe 控制器、xfs、LVM、dm-crypt);而这些驱动作为内核模块存在 /lib/modules/ 里,也就是在根文件系统内部

两种解法:把所有可能用到的驱动编译进内核(内核会极其庞大,且发行版无法做通用内核);或者先挂一个内存里的临时根文件系统,它自带必要驱动,用它找到并挂载真正的根,然后 switch_root 切过去 —— 这就是 initramfs。

它的工作流程是:内核把 cpio 归档解开到 tmpfs 作为初始 / → 执行 /init(此时它是 PID 1)→ 挂载 /proc//sys//dev、按 root= 参数 modprobe 驱动、激活 LVM、解锁加密盘、等待设备就绪 → 挂载真实根到 /rootswitch_root 释放 initramfs 内存并 exec /sbin/init(PID 仍是 1)。

顺带解释了 03 篇的 usrmerge:传统上 /bin/lib 放在根分区是因为「启动早期必需」,而 /usr 挂载晚。initramfs 出现后,挂载所有分区的工作全部由它完成,等真实根就位时 /usr 早就挂好了 —— 这个划分失去了技术依据,于是 /bin 变成了指向 /usr/bin 的软链接。

Q:initrdinitramfs 有什么区别?

initrd(2.4 时代)是一个块设备镜像(ext2 格式),内核要把它挂成 /dev/ram0 这个 ramdisk 再挂载 —— 这意味着内核必须内置能读 ext2 的代码,而且是固定大小、存在双重缓存(ramdisk + page cache)。

initramfs(2.6+)是一个 cpio 归档(gzip/zstd 压缩),内核直接解开到 tmpfs。cpio 解压是内核内置能力,不需要任何文件系统驱动;大小按需增长;没有双重缓存;切换根用 switch_root(直接替换 rootfs 内容)而不是 pivot_root

现在的文件虽然还叫 initrd.img(历史命名),但 file 一下会发现内容是压缩的 cpio,即 initramfs。

Q:vmlinuz 里的 z 是什么意思?谁负责解压?

z = compressed(压缩),对应 vmlinux(未压缩的 ELF)。相关命名:zImage 是早期格式,上限 512KBbzImagebig zImage(突破了这个限制,b 是 big,与 bzip2 无关)。

解压是内核镜像自己做的。 bzImage 的结构是「一小段未压缩的实模式引导代码 + 未压缩的解压器 + 压缩的内核本体」。引导器跳到那段未压缩代码,它切换 CPU 模式、把后面的内核本体解压到内存、再跳进 start_kernel()

压缩的历史原因是软盘容量(1.44MB 要装下内核),现在的理由是解压的 CPU 开销小于多读几 MB 的 I/O 开销。现代内核偏向 zstd(压缩比好且解压快)。

Q:PID 1 为什么特殊?在容器里这意味着什么?

内核对 PID 1 有三条特殊规定:

  1. PID 1 退出 = 内核 panic(内核认为 init 死了系统就没救了)
  2. PID 1 收养所有孤儿进程(父进程先死的进程被 reparent 到它,由它 wait() 回收)
  3. PID 1 忽略所有它没有显式注册处理函数的信号(防止误杀 init —— 普通进程收到 SIGTERM 会被默认终止,PID 1 不会)

容器里你的应用就是 PID 1,于是继承了全部特殊性

  • 它必须负责回收孤儿进程,不做就会积累僵尸(13 篇 3.2)
  • 更致命的是规定 ③:没有注册 SIGTERM 处理函数的程序,收到 docker stop 的 SIGTERM 时什么都不会发生,10 秒后被 SIGKILL 强杀 —— 这就是「容器 stop 很慢」和「优雅退出失效」的根因

两个对策:docker run --init(注入 tini 作为 PID 1,负责转发信号 + 回收僵尸),或者应用自己 signal.Notify 处理 SIGTERM。此外 entrypoint 脚本必须用 exec 启动主程序(09 篇 2.4),否则信号发给了 shell。

Q:SysV init 为什么必须串行启动?systemd 靠什么实现并行?

SysV 的依赖关系没有被显式表达,只能靠文件名编号隐含S10networkS20nginx 之前)。init 完全不知道任何依赖关系,所以唯一安全的策略就是按编号一个一个来,前一个完全结束才启动下一个 —— 即使两个服务毫无关系。后果是慢、脆弱(插入新服务要小心选编号)、且无法表达「A 依赖 B 但 B 失败时 A 仍可启动」这类语义。

systemd 靠三个机制并行:

  1. 声明式依赖Wants=/Requires=/After=)→ 能构建依赖图,无依赖关系的服务同时启动
  2. socket 激活(最关键的突破)→ systemd 先创建好所有监听 socket 再并行启动服务,即使 B 没就绪,A 连接 B 的请求也会被缓冲在 socket 里 —— 彻底消除了「必须等 B 完全就绪」的时序依赖
  3. 按需启动(socket/path/timer 激活)

还有一个被低估的设计是用 cgroup 追踪进程:SysV 靠 PID 文件判断服务状态,而 PID 文件会过期、PID 会被复用、fork 出的子进程无人追踪。systemd 把每个服务放进一个 cgroup,进程无法逃逸stop 时一网打尽。

Q:机器起不来,怎么按阶段定位?

按现象分六段:

  • 完全无输出、不进 BIOS → 硬件问题
  • 黑屏 / GRUB rescue> / no such partition → 分区表损坏、GRUB 配置错、磁盘顺序变了 → live CD + grub-install 修复
  • 卡在 Loading initial ramdisk 之后 → 去掉 quiet splash、加 nomodeset 看真实输出
  • Gave up waiting for root device / 掉进 (initramfs) 提示符initramfs 里缺驱动(换了存储控制器、迁到 LVM/加密盘但没重建 initramfs、/boot 满导致生成失败)。在那个 shell 里 cat /proc/cmdlineblkidmodprobe nvmevgchange -ay 逐步排查
  • 进 emergency mode → 头号原因是 /etc/fstab 写错(11 篇 6.4,所以非关键分区一定要加 nofail
  • 能启动但登不进去 → PAM 配置、shell 不存在、家目录权限、磁盘满df -hdf -i 都要看)

通用手段:journalctl -b -1 看上一次启动的日志、systemctl --failedsystemd-analyze blamesystemctl list-jobs 看卡住的任务。终极救援是在 GRUB 里加 init=/bin/bash,然后 mount -o remount,rw / 修配置。

Q:为什么 /etc/security/limits.conf 对 systemd 服务无效?

因为它是通过 PAM 的 pam_limits.so 模块生效的,而 PAM 只在登录会话建立时被调用(loginsshdsucron 都会走 PAM 的 session 阶段)。

systemd 启动的服务不经过 PAM —— 它们不是「登录会话」,没有 PAM 上下文,所以 limits.conf 完全不参与。必须在 unit 里写 LimitNOFILE=65535(14 篇 3.2),验证要看 cat /proc/<pid>/limits 而不是 ulimit -n

顺带说 PAM 本身是「机制与策略分离」(18 篇第 4 章)的又一个例子:login/sshd/sudo/cron 都需要验证身份,如果各自实现,那么支持一种新认证方式(LDAP、双因素)就得改所有程序。PAM 让程序只调用统一 API,具体怎么验证由 /etc/pam.d/ 下的配置决定 —— 四个管理组(auth/account/password/session)分别负责验证身份、检查账号可用性、密码策略、会话建立与销毁。


小结

  • 启动的本质是自举,链条的终点必须是硬件约定(CPU 上电从固定地址取指)
  • 分阶段加载源于空间约束:BIOS 只读 512 字节,扣掉分区表只剩 446 字节,只够「读入下一阶段」
  • UEFI 让固件认识 FAT32,引导代码不再受 446 字节限制;但引导器仍因「选内核、传参数、加载 initramfs」而存在
  • bzImage = big zImage(与 bzip2 无关),解压由内核镜像自带的解压器完成
  • initramfs 解决鸡生蛋问题:挂根需要驱动,驱动在根里。它是内存中的临时根,switch_root 后释放
  • initramfs 让 usrmerge 成为可能 —— 一个底层机制的出现,让延续三十年的目录约定失去理由
  • PID 1 的三条特殊规定(退出即 panic、收养孤儿、忽略未注册信号)直接决定了容器的信号处理行为
  • SysV 必须串行是因为依赖没被表达;systemd 靠依赖图 + socket 激活 + 按需启动实现并行,用 cgroup 让进程无法逃逸
  • limits.conf 走 PAM,而服务不走 PAM —— 所以对 systemd 服务无效
  • 救援优先级:旧内核 → systemd.unit=emergency.targetinit=/bin/bash

下一篇讲 VFS:一切皆文件的实现代价 —— 18 篇讲了这个抽象的取舍,20 篇讲内核到底怎么实现它:file_operations 如何把磁盘、管道、socket、设备统一成同一组接口,struct file / dentry / inode 三层对象各自解决什么问题,以及为什么 open() 一个深路径的开销值得内核专门做 dentry cache。