Linux-19 从按下电源到 shell 提示符:每一环为什么必须存在
上一篇建立了「理解约束才能理解设计」的框架。这一篇用它分析启动流程 —— 启动是一条环环相扣的链条,每一环都是为了解决前一环留下的问题而存在的。
先看五个问题:
- 为什么需要引导器(bootloader)?固件不能直接加载内核吗?
- initramfs 为什么被发明?没有它会怎样?
- 内核镜像为什么叫
vmlinuz(z表示压缩)?谁来解压它? - PID 1 为什么特殊?它退出会发生什么?
- 为什么现代系统能并行启动,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、解锁加密盘、等待设备就绪 → 挂载真实根到 /root → switch_root 释放 initramfs 内存并 exec /sbin/init(PID 仍是 1)。
顺带解释了 03 篇的 usrmerge:传统上 /bin、/lib 放在根分区是因为「启动早期必需」,而 /usr 挂载晚。initramfs 出现后,挂载所有分区的工作全部由它完成,等真实根就位时 /usr 早就挂好了 —— 这个划分失去了技术依据,于是 /bin 变成了指向 /usr/bin 的软链接。
Q:initrd 和 initramfs 有什么区别?
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 是早期格式,上限 512KB;bzImage 是 big zImage(突破了这个限制,b 是 big,与 bzip2 无关)。
解压是内核镜像自己做的。 bzImage 的结构是「一小段未压缩的实模式引导代码 + 未压缩的解压器 + 压缩的内核本体」。引导器跳到那段未压缩代码,它切换 CPU 模式、把后面的内核本体解压到内存、再跳进 start_kernel()。
压缩的历史原因是软盘容量(1.44MB 要装下内核),现在的理由是解压的 CPU 开销小于多读几 MB 的 I/O 开销。现代内核偏向 zstd(压缩比好且解压快)。
Q:PID 1 为什么特殊?在容器里这意味着什么?
内核对 PID 1 有三条特殊规定:
- PID 1 退出 = 内核 panic(内核认为 init 死了系统就没救了)
- PID 1 收养所有孤儿进程(父进程先死的进程被 reparent 到它,由它
wait()回收) - 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 的依赖关系没有被显式表达,只能靠文件名编号隐含(S10network 在 S20nginx 之前)。init 完全不知道任何依赖关系,所以唯一安全的策略就是按编号一个一个来,前一个完全结束才启动下一个 —— 即使两个服务毫无关系。后果是慢、脆弱(插入新服务要小心选编号)、且无法表达「A 依赖 B 但 B 失败时 A 仍可启动」这类语义。
systemd 靠三个机制并行:
- 声明式依赖(
Wants=/Requires=/After=)→ 能构建依赖图,无依赖关系的服务同时启动 - socket 激活(最关键的突破)→ systemd 先创建好所有监听 socket 再并行启动服务,即使 B 没就绪,A 连接 B 的请求也会被缓冲在 socket 里 —— 彻底消除了「必须等 B 完全就绪」的时序依赖
- 按需启动(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/cmdline、blkid、modprobe nvme、vgchange -ay逐步排查- 进 emergency mode → 头号原因是
/etc/fstab写错(11 篇 6.4,所以非关键分区一定要加nofail) - 能启动但登不进去 → PAM 配置、shell 不存在、家目录权限、磁盘满(
df -h和df -i都要看)
通用手段:journalctl -b -1 看上一次启动的日志、systemctl --failed、systemd-analyze blame、systemctl list-jobs 看卡住的任务。终极救援是在 GRUB 里加 init=/bin/bash,然后 mount -o remount,rw / 修配置。
Q:为什么 /etc/security/limits.conf 对 systemd 服务无效?
因为它是通过 PAM 的 pam_limits.so 模块生效的,而 PAM 只在登录会话建立时被调用(login、sshd、su、cron 都会走 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.target→init=/bin/bash
下一篇讲 VFS:一切皆文件的实现代价 —— 18 篇讲了这个抽象的取舍,20 篇讲内核到底怎么实现它:file_operations 如何把磁盘、管道、socket、设备统一成同一组接口,struct file / dentry / inode 三层对象各自解决什么问题,以及为什么 open() 一个深路径的开销值得内核专门做 dentry cache。
xingliuhua