Contents

深入理解Linux内核镜像、模块与启动过程

当谈论 Linux 内核时,我们常常被一堆名词绕得云里雾里:vmlinuxbzImagevmlinuzinitrdinitramfs、内核模块…… 它们名称相似,却在系统启动的宏大交响乐中扮演着各自不可或不可缺的角色。

它们之间究竟是何关系?为什么一个简单的启动过程需要如此多的文件参与?

本文将为你系统性地梳理这些核心概念,从内核编译的产物出发,一步步带你走过从 Bootloader 加载到用户空间程序运行的完整链条,让你彻底厘清 Linux 的启动脉络。


一、内核的心脏:镜像的演化与形态

一切故事的起点,都源于内核编译的最终产物。当我们执行 make 编译内核时,最核心、最原始的文件只有一个:

  • vmlinux:一个未经压缩、包含了调试符号表和地址信息的 ELF 格式可执行文件。

你可以把它理解为内核的“源码编译出的最完整形态”。然而,它体积庞大,且不包含用于引导自身的代码,因此不能被 Bootloader 直接加载启动。它的主要用途是进行内核调试(如使用 GDB 分析内核 crash 后的 vmcore 文件)和地址转换(addr2line)。

既然 vmlinux 无法直接启动,那么我们日常使用的可引导内核又是如何而来的呢?答案是:压缩与封装

为了适应早期引导器有限的加载能力和加快启动速度,vmlinux 需要被“瘦身”和“打包”。

1️⃣ 各类镜像文件辨析

文件名内容形式可否直接启动典型平台主要用途与解读
vmlinux未压缩的 ELF 文件,包含符号表❌ 否通用内核调试、地址分析(GDB, addr2line)
bzImagebig zImage,压缩内核镜像✅ 可启动x86现代 PC/服务器标准引导文件(GRUB、QEMU)
zImage压缩镜像(早期格式,限制大小)✅ 可启动ARM, MIPS早期或资源受限的嵌入式平台
Image未压缩的纯二进制映像✅ 可启动ARM64AArch64 架构标准,如树莓派 4+
uImageU-Boot 封装格式(带头部信息)✅ 可启动嵌入式 ARM专为 U-Boot 引导器设计,头部记录了加载地址等信息
vmlinuz压缩版内核的通用别名✅ 可启动PC 发行版/boot 目录下,通常是 bzImage 的一个链接或拷贝

2️⃣ 文件之间的生成关系

整个演化路径清晰明了:

vmlinux ➡️ (压缩) ➡️ zImage / bzImage ➡️ (封装) ➡️ vmlinuz / uImage

简而言之:

  1. 编译:源代码生成了原始的 vmlinux
  2. 压缩vmlinux 被压缩工具处理,并附加上一段自解压代码,形成了 bzImagezImage。这使得内核在被加载到内存后,能做的第一件事就是“自我解压”。
  3. 封装:为了适配特定的 Bootloader(如 U-Boot)或遵循发行版的命名规范,bzImage 等文件被重命名或加上一个头部,变成了 uImagevmlinuz

3️⃣ 示例命令

# 1. 编译生成最原始的 vmlinux
make vmlinux

# 2. 从 vmlinux 生成可启动的 bzImage (x86 平台)
make bzImage

# 3. (嵌入式) 为 U-Boot 引导器制作 uImage
#    这个命令会给 zImage 加上一个 U-Boot 识别的头部
mkimage -A arm -O linux -T kernel -C none \
        -a 0x80008000 -e 0x80008000 \
        -n "Linux Kernel" \
        -d zImage uImage

二、内核的延伸:内核模块 (Kernel Modules)

如果把内核镜像比作一个操作系统的“主程序”,那么内核模块就是它的“插件 (Plugins)”或“DLC (可下载内容)”。

早期的内核是单体 (Monolithic) 的,所有驱动和功能都必须编译进内核镜像本体。这导致内核镜像异常臃肿,且每次增删硬件驱动都需要重新编译整个内核。

为了解决这个问题,内核模块应运而生。

1️⃣ 模块是什么?

内核模块(文件扩展名为 .ko,Kernel Object)是独立编译的内核组件,可在系统运行时被动态地加载或卸载,以扩展内核功能。常见的模块包括:

  • 硬件驱动:网卡、显卡、USB 控制器等。
  • 文件系统驱动:EXT4, XFS, Btrfs, NFS 等。
  • 网络协议、加密算法等。

这种机制极大地增强了 Linux 的灵活性和可维护性。

2️⃣ 模块的家:目录结构

编译安装内核后,所有模块都会被存放在 /lib/modules/<kernel-version>/ 目录下,结构清晰:

/lib/modules/5.15.0-generic/
├── kernel/
│   ├── drivers/      # 存放所有驱动模块
│   ├── fs/           # 文件系统模块
│   ├── net/          # 网络相关模块
│   └── ...
├── modules.dep       # 模块依赖关系数据库
├── modules.alias     # 模块别名,用于设备热插拔
├── modules.symbols   # 模块导出的符号表
└── build -> /usr/src/linux-headers-5.15.0-generic/

3️⃣ 常用命令

命令功能
lsmod列出当前已加载的内核模块
modprobe e1000智能加载 e1000 模块及其所有依赖模块
rmmod e1000卸载 e1000 模块
modinfo e1000查看 e1000 模块的详细信息(作者、路径、依赖等)

思考一下:内核和模块的分离带来了巨大的灵活性,但也引出了一个经典的“鸡生蛋,蛋生鸡”问题:

如果我的根文件系统(/)存放在一块 SATA 硬盘上,而 SATA 驱动是一个内核模块,那么内核在启动初期是如何加载这个模块来访问根文件系统的呢?毕竟,模块文件本身就存放在根文件系统里!

这个问题的答案,正是下一节的主角。


三、启动的桥梁:initrd 与 initramfs

为了解决上一节提出的“鸡生蛋”问题,Linux 需要一个临时的根文件系统 (Temporary Root Filesystem)。这个临时环境存在于内存中,包含了挂载真实根文件系统所必需的驱动模块和工具。

这个临时环境,就是 initrdinitramfs

1️⃣ initrd (Initial RAM Disk) - 昔日之星

  • 形式:一个块设备镜像,通常采用 ext2cramfs 文件系统格式。
  • 工作流程
    1. Bootloader 将内核和 initrd.img 文件加载到内存。
    2. 内核启动后,将这块内存区域虚拟成一个磁盘设备(如 /dev/ram0)。
    3. 内核将 /dev/ram0 挂载为临时根目录。
    4. 执行临时根目录中的 /linuxrc 脚本,该脚本负责加载真实硬盘的驱动模块(如 ahci.ko)。
    5. 驱动加载后,挂载真实的根文件系统,并切换过去。

2️⃣ initramfs (Initial RAM Filesystem) - 当代标准

  • 形式:一个 cpio 归档文件,经过 gziplz4 压缩。
  • 工作流程
    1. Bootloader 将内核和 initramfs.cpio.gz 文件加载到内存。
    2. 内核启动后,在内存中创建一个特殊的 tmpfs 文件系统。
    3. 内核将 initramfs.cpio.gz 的内容直接解压到这个 tmpfs 中作为临时根。
    4. 执行临时根目录中的 /init 脚本,其功能与 /linuxrc 类似。
    5. 挂载真实根文件系统并切换。

3️⃣ initrd vs initramfs 对比

项目initrdinitramfs
形式块设备镜像内存文件系统 (cpio 归档)
格式ext2, cramfs 等cpio 压缩包
挂载方式内核挂载到 /dev/ram0内核直接解压到 tmpfs
启动脚本/linuxrc/init
效率较复杂,多一次块设备抽象更简洁、高效,已完全取代 initrd
引入版本Linux 2.4 及以前Linux 2.6+
状态已淘汰现代 Linux 标准

📦 特别注意: 现代 Linux 发行版(如 Ubuntu, CentOS)在 /boot 目录下依然使用 initrd.img-<version> 这样的文件名。这只是一个历史遗留的命名习惯,其内容实际上是 initramfs 格式的! 你可以使用 lsinitramfs 命令来验证。


四、启动流程图:一场从硬件到桌面的接力赛

现在,我们已经集齐了所有关键组件。让我们将它们串联起来,绘制一幅完整的 Linux 启动流程图。

启动过程概述:

  1. 阶段 1:Bootloader

    • BIOS/UEFI 完成硬件自检后,加载并执行硬盘主引导记录 (MBR) 或 EFI 分区中的 Bootloader(如 GRUB)。
    • GRUB 读取其配置文件(grub.cfg),将内核镜像 (vmlinuz-*) 和 initramfs 文件 (initrd.img-*) 加载到指定内存地址。
    • 将控制权移交给内核。
  2. 阶段 2:内核初始化

    • 内核首先执行自解压代码,将压缩的自身还原到内存中。
    • 执行 start_kernel() 函数,进行一系列底层初始化(内存管理、进程调度、中断处理等)。
    • 将 Bootloader 加载的 initramfs 数据解压到内存 tmpfs 中,并将其挂载为临时根文件系统
  3. 阶段 3:Initramfs 阶段

    • 内核执行临时根中的 /init 脚本。
    • 该脚本通过 modprobe 加载访问真实根文件系统所需的关键驱动模块(例如 ahci 用于 SATA 盘,nvme 用于 NVMe SSD)。
    • 挂载真实的根文件系统到一个临时挂载点(如 /mnt)。
    • 通过 switch_root 命令,将系统的根目录从内存中的 initramfs 切换到真实的硬盘分区上,并执行新的 /sbin/init 程序。
  4. 阶段 4:用户空间初始化

    • 系统的控制权现在完全交给了用户空间的第一个进程 /sbin/init(在现代系统中通常是 systemd)。
    • systemd 根据其配置单元(unit files)按部就班地启动所有系统服务,如网络管理、日志服务、登录管理器等。
    • 最终,系统进入预设的运行级别,呈现出用户登录界面或命令行提示符。

五、全景图:一张表看懂所有组件

类别文件 / 组件核心作用
内核镜像bzImage, zImage, Image, vmlinuz系统启动和运行的核心程序
调试文件vmlinuxELF 格式,含符号信息,用于内核调试和分析
内核模块/lib/modules/.../*.ko提供驱动和功能,作为内核的动态“插件”
临时根文件系统initramfs / initrd在启动早期提供环境,用于加载访问真实根的驱动
启动管理器GRUB / U-Boot负责初始化硬件、加载内核与 initramfs 到内存
用户空间init / systemd内核启动后接管控制权,负责启动所有系统服务

六、一句话总结

Linux 内核镜像是启动的核心,initramfs 是它踏入真实世界前的“序章”,而内核模块则是它在运行时不断扩展能力的“军火库”。它们共同构成了从 Bootloader 到用户空间完整而精妙的启动生态。


✨ 七、延伸阅读

  • 书籍:《Linux 内核设计与实现》 — Robert Love
  • 源码init/main.c -> start_kernel() 函数,这是内核初始化的起点。
  • 实用命令
    • file /boot/vmlinuz-*:查看内核镜像的真实类型。
    • lsinitramfs /boot/initrd.img-*:列出 initramfs 文件中的内容。
    • modinfo <module_name>:深入了解一个内核模块。