当前位置:首页>Linux>从零构建 Linux|03 内核启动

从零构建 Linux|03 内核启动

  • 2026-09-18 21:27:29
从零构建 Linux|03 内核启动

系列第 3 篇|Bootloader 交权之后,内核在忙什么用关键函数串主线;讲清挂载、块设备与文件系统;对照 Ubuntu 目录树

Bootloader 把内核映像放进内存并跳转之后,舞台交给了 Linux 内核(kernel)。本篇说明内核启动时在忙什么、挂载根文件系统时发生了什么,以及它怎样把系统交给用户空间。

读完后你应能回答:

  1. 内核启动大概分哪几段?关键入口函数是什么?
  2. 块设备、文件系统、挂载分别是什么关系?
  3. 挂载 rootfs 之后,目录树长什么样(以 Ubuntu 为例)?
  4. root=、init= 在这里起什么作用?

先澄清:内核启动 ≠ 你看到桌面

很多人说「系统启动了」,指的是已经能登录、能开浏览器。那其实已经走到用户空间很后面了。

内核启动只覆盖到这一步附近:

内核自己初始化完成 → 挂上根文件系统 → 成功运行第一个用户进程 init。

桌面、网络服务、你的应用,都是 init 之后的故事(见系列第 4 篇)。

主线一:早期启动——从入口到 start_kernel

刚被 Bootloader 跳进来时,内核需要先把自己安顿好。对 C 开发者,可以把它想成:先跑一小段「与 CPU 架构强相关」的启动代码,再进入统一的 C 入口。

架构相关入口(不必背汇编)

不同 CPU 架构入口不同。以常见的 x86_64 为例,概念上是:

  1. 若镜像是压缩的(如 bzImage),先解压、摆放到正确的内存位置;
  2. 进入架构相关的早期入口(如 startup_64);
  3. 建立最基础的页表、处理 Bootloader 传来的启动信息(cmdline,嵌入式上还有 DTB);
  4. 转入与架构无关的 C 入口:start_kernel()。

ARM、RISC-V 同理:入口函数名不同,但目标一样——尽快进入 start_kernel()。

start_kernel():内核的「main」

可以把 start_kernel() 粗浅理解成内核世界的 main()。它不会直接跑你的 App,但会按顺序把操作系统的骨架搭起来,例如:

阶段(概念)
典型动作
你需要记住的点
架构与启动参数
setup_arch()
 等
确认 CPU、内存布局、cmdline、设备描述信息
内存与核心机制
页分配、slab、调度、中断等初始化
没有它们,后面谈不上进程与驱动
子系统就绪
VFS、块层、网络等按 initcall 排队初始化
「文件系统」「磁盘」能力在此具备
收尾并拉起 init 线程
rest_init()
内核把自己的启动线程创建出来,去挂 root、跑 init

补充一个常听到的词:initcall。你可以把它理解成「内核把自己的初始化函数排了个队,按阶段依次调用」。驱动和子系统大量靠它就位。

此时还没有完整的用户程序世界,但内核已经开始运转。

主线二:子系统与驱动——让硬件真正可用

内核里有大量子系统:进程、文件、网络、块设备(磁盘)等。它们会按一定顺序初始化。驱动(device driver)也在这个阶段陆续就位。

对 C 开发者,可以用这个类比:

驱动 ≈ 内核里的硬件适配层。你的应用一般不直接操作寄存器,而是通过文件或系统调用,最终由驱动和内核子系统完成对硬件的访问。

为什么这一点对启动很关键?

  1. 若存储相关驱动没就绪,内核可能读不到根分区 → 挂不上 rootfs。
  2. 若串口 / 显示相关驱动或 console 配置不对,你可能看不到任何日志,误以为「没启动」。

后面用 Yocto 为某台机器定制系统时,「选对 MACHINE / 内核配置 / 模块」本质上就是在保证:启动关键路径上的驱动是齐的。

主线三:挂载 rootfs 之前——先分清三件事

挂载 rootfs 是启动里最「看得见」的一步。要真正理解它,需要先分清:块设备、文件系统、挂载。

1. 块设备(block device):一串可按块读写的存储

硬盘、SSD、eMMC、SD 卡、U 盘,在内核里通常表现为块设备:以固定大小的「块」(常见 512B 或 4KB)为单位读写,而不是像串口那样按字符流读写。

用户空间常通过 /dev 下的设备节点看到它们,例如:

设备节点示例
通常表示
/dev/sda
第一块 SATA/SCSI 风格磁盘整盘
/dev/sda1
/dev/sda2
该盘的第 1、第 2 分区
/dev/nvme0n1
/dev/nvme0n1p2
NVMe SSD 整盘 / 第 2 分区
/dev/mmcblk0
/dev/mmcblk0p2
eMMC/SD 整盘 / 第 2 分区
/dev/vda
/dev/vda1
虚拟机里常见的 virtio 磁盘

可以记:

整盘 ≈ 一整块物理/虚拟存储;分区 ≈ 在盘上划出的一段连续空间,启动参数里的 root= 通常指向某个分区。

分区表(MBR / GPT)告诉内核:这块盘从哪一扇区到哪一扇区算第几个分区。Bootloader 和内核都靠它找到「根分区」在哪。

2. 文件系统(filesystem):如何把「块」组织成「文件和目录」

分区里存的不只是原始字节,还要按某种格式组织成目录、文件、权限、时间戳等。这种格式就是文件系统类型,例如:

类型
常见场景
ext4
Ubuntu / Debian 等 Linux 根分区很常见
xfs、btrfs
服务器或特定发行版选择
vfat / fat32
U 盘、EFI 系统分区(ESP)常见
squashfs
只读压缩根(不少嵌入式 / Live 镜像)

没有正确的文件系统驱动,内核即使找到了分区,也「读不懂」上面的目录树。

对 C 开发者的类比:

块设备 ≈ 一块很大的 char buf[](按块访问);文件系统 ≈ 规定如何把数组里的字节解释成「路径 → inode → 数据块」的一套规则与代码;VFS(虚拟文件系统)≈ 内核里统一的「打开 / 读 / 写 / 遍历目录」接口,底下再接到具体文件系统实现。

3. 挂载(mount):把「某分区上的文件系统」接到目录树上的某个点

「挂载」的意思是:让某个存储上的某个文件系统,成为目录树上的一部分。

块设备/分区  +  文件系统类型  +  挂载点     ↓              ↓              ↓ /dev/sda2    +    ext4      +     /

效果直白地说:

  1. 挂载前:路径 /bin、/etc 对用户空间几乎没有真实内容可访问(还没有可用的根);
  2. 挂载后:该分区上的目录树成为 / 下的内容,/bin、/etc、/usr 等路径才指向真实文件;
  3. 还可以把别的分区挂到子目录,例如把数据盘挂到 /home,把 EFI 分区挂到 /boot/efi。

命令行里你熟悉的:

mount /dev/sda2 /mnt

含义就是:把 /dev/sda2 上的文件系统,接到当前目录树的 /mnt 这个挂载点。启动时内核做的「挂根」,本质上是同一类动作,只是挂载点是 /,而且发生得更早。

主线四:挂载 rootfs,启动 init

硬件和内核基础设施就绪后,内核要做两件对用户可见的大事。对应到源码主线上,大致是:

rest_init()  └─ 创建内核线程 → kernel_init()                    ├─ 准备命名空间 / 挂载根(do_mounts 一带)                    └─ run_init_process()  →  用户空间 PID 1

不必死记函数名,但建议认识这条链:start_kernel → rest_init → kernel_init → 挂根 → 执行 init。

第一件:挂载根文件系统(rootfs)

内核根据 cmdline 里的 root=(以及 rootfstype= 等)找到根设备,调用挂载相关逻辑(历史上集中在 do_mounts 一带),把该分区上的文件系统挂到 /。

常见两种走法:

走法
做法
常见场景
直接挂真实 root
root=
 已指向最终分区,内核直接挂它
精简嵌入式系统
先走 initramfs,再切换
先用内存里的小型临时根(initramfs / initrd)跑一小段早期用户空间,加载额外驱动、解锁磁盘、搜索根设备,再 switch_root / pivot_root 切到真正的根分区
Ubuntu 等 PC 发行版

挂载成功后的直接效果:

  1. VFS 的根挂载点指向真实 rootfs;
  2. 可以打开 /sbin/init、/etc/... 等路径;
  3. 若挂载失败,内核会报无法挂载 root,并常常停在这里——这是定制镜像时最常见的启动失败点之一。

第二件:执行 init(PID 1)

根文件系统可用后,内核会在其中启动第一个用户空间进程。这个进程的进程号(PID)是 1,所以常叫 PID 1,程序名习惯上叫 init。

内核如何找到它?

  1. 若 cmdline 写了 init=/某路径,就用你指定的;
  2. 否则按惯例尝试 /sbin/init、/etc/init、/bin/init、/bin/sh 等常见路径。

源码概念上对应 run_init_process():用内核的「执行用户程序」路径加载该 ELF,把它变成 PID 1。

可以这样判断边界:当 init 已经成功跑起来,就认为内核完成了「把系统运行交给用户空间」这一棒。再往后的服务、登录界面、应用程序,属于第 4 篇。

Ubuntu 根目录结构:挂上之后你看到的世界

挂载完成后,用户空间看到的就是一棵以 / 为根的目录树。下面以一台典型 Ubuntu 系统为例说明(桌面与服务器大体相同;嵌入式精简 rootfs 会少很多目录,但思路一致)。

在 Ubuntu 上可以自己看:

ls -la /

常见顶层目录:

/├── bin        → 基本用户命令(现代 Ubuntu 常是指向 usr/bin 的符号链接)├── boot       → 内核镜像、initrd、GRUB 相关文件├── dev        → 设备节点(块设备、字符设备等,多由 udev 维护)├── etc        → 系统与服务配置├── home       → 普通用户家目录├── lib        → 基本动态库与内核模块路径相关(常链接到 usr/lib)├── media      → 可移动介质的自动挂载点├── mnt        → 临时手工挂载点├── opt        → 可选第三方软件├── proc       → 进程与内核信息的虚拟文件系统(procfs,不是磁盘上的真实目录)├── root       → root 用户的家目录├── run        → 运行时数据(早于 /var/run 的现代位置)├── sbin       → 系统管理命令(常有 init 相关入口;多链接到 usr/sbin)├── srv        → 本机提供的服务数据├── sys        → 设备与驱动拓扑等(sysfs,虚拟文件系统)├── tmp        → 临时文件├── usr        → 用户空间程序与库的主体(bin、lib、share 等)└── var        → 会变化的数据:日志、缓存、包数据库等

结合「块设备 / 文件系统 / 挂载」再读一遍这张表,会更清楚:

你在 Ubuntu 上看到的
背后通常是什么
/
 下大部分目录
根分区(如 /dev/nvme0n1p2)上的 ext4
/boot/efi
另一个分区(ESP,常为 vfat)挂到这里
/home
可以就是根分区的子目录,也可以是单独分区挂上来的
/proc
、/sys、/dev
特殊「文件系统」挂载出来的视图,不是普通磁盘文件夹那么简单
/mnt
、/media/...
额外挂载点:U 盘、数据盘、NFS 等接到树上的位置

对启动而言,内核至少要保证:根分区挂上后,/sbin/init(或你指定的 init=)这条路径真实存在且可执行。Ubuntu 上 PID 1 通常是 systemd(/sbin/init 往往是指向 systemd 的符号链接)——细节见第 4 篇。

和「做镜像」的关系

无论用 Yocto 还是别的构建系统,产出里通常都有:

  1. 内核镜像
  2. (嵌入式上)设备树 DTB
  3. 内核模块(可选,有的驱动编成模块稍后加载)
  4. 与之匹配的 rootfs 或整盘镜像

这几样必须「对得上」。典型错配会表现为:

现象
常见原因
完全没有任何输出
可能还没跳进内核;或 console / early console 配置不对
有内核日志,但最后报无法挂载 root
root=
 写错、分区不对、缺存储驱动、或缺对应文件系统支持(如未启用 ext4)
提示找不到 init,或卡在 starting init
rootfs 里没有可用的 init,或路径不对

所以后面改 Yocto 配置时,不要只关心「我想装哪个 App」,也要意识到:你其实在组装「能被 Bootloader 加载的内核」+「能被内核挂上的 rootfs」。

启动参数示例

嵌入式串口场景:

console=ttyS0,115200root=/dev/mmcblk0p2 rwrootfstype=ext4
参数
含义
console=...
日志与终端走串口
root=...
根在第二分区(块设备节点)
rootfstype=...
根分区上的文件系统类型
rw
可读写挂载

PC 上更常见用 UUID 指分区(避免盘符顺序变化导致挂错盘):

root=UUID=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeeero

指定 init(一般用不着,但要认得):

init=/sbin/init

本篇小结

用一段话串起来:

Bootloader 跳进内核后,经架构入口进入 start_kernel(),再经 rest_init() / kernel_init() 完成子系统与驱动初始化;随后按 root= 找到块设备上的根分区,用对应文件系统类型挂到 /,最后执行 PID 1(init)。挂载成功后,你看到的就是类似 Ubuntu 的那棵目录树——内核把「继续把系统用起来」的任务交给了用户空间。

下一篇我们进入 rootfs 内部,看 init 如何拉起服务,以及 BusyBox、Debian 等不同 rootfs 从何而来。

系列文章列表:

从零构建 Linux 系统
从零构建 Linux|01 启动流程总览
从零构建 Linux|02 Bootloader

最新文章

随机文章