系列第 3 篇|Bootloader 交权之后,内核在忙什么用关键函数串主线;讲清挂载、块设备与文件系统;对照 Ubuntu 目录树
Bootloader 把内核映像放进内存并跳转之后,舞台交给了 Linux 内核(kernel)。本篇说明内核启动时在忙什么、挂载根文件系统时发生了什么,以及它怎样把系统交给用户空间。
读完后你应能回答:
root=、init= 在这里起什么作用?很多人说「系统启动了」,指的是已经能登录、能开浏览器。那其实已经走到用户空间很后面了。
内核启动只覆盖到这一步附近:
内核自己初始化完成 → 挂上根文件系统 → 成功运行第一个用户进程 init。
桌面、网络服务、你的应用,都是 init 之后的故事(见系列第 4 篇)。
start_kernel刚被 Bootloader 跳进来时,内核需要先把自己安顿好。对 C 开发者,可以把它想成:先跑一小段「与 CPU 架构强相关」的启动代码,再进入统一的 C 入口。
不同 CPU 架构入口不同。以常见的 x86_64 为例,概念上是:
bzImage),先解压、摆放到正确的内存位置;startup_64);start_kernel()。ARM、RISC-V 同理:入口函数名不同,但目标一样——尽快进入 start_kernel()。
start_kernel():内核的「main」可以把 start_kernel() 粗浅理解成内核世界的 main()。它不会直接跑你的 App,但会按顺序把操作系统的骨架搭起来,例如:
setup_arch() | ||
rest_init() |
补充一个常听到的词:initcall。你可以把它理解成「内核把自己的初始化函数排了个队,按阶段依次调用」。驱动和子系统大量靠它就位。
此时还没有完整的用户程序世界,但内核已经开始运转。
内核里有大量子系统:进程、文件、网络、块设备(磁盘)等。它们会按一定顺序初始化。驱动(device driver)也在这个阶段陆续就位。
对 C 开发者,可以用这个类比:
驱动 ≈ 内核里的硬件适配层。你的应用一般不直接操作寄存器,而是通过文件或系统调用,最终由驱动和内核子系统完成对硬件的访问。
为什么这一点对启动很关键?
后面用 Yocto 为某台机器定制系统时,「选对 MACHINE / 内核配置 / 模块」本质上就是在保证:启动关键路径上的驱动是齐的。
挂载 rootfs 是启动里最「看得见」的一步。要真正理解它,需要先分清:块设备、文件系统、挂载。
硬盘、SSD、eMMC、SD 卡、U 盘,在内核里通常表现为块设备:以固定大小的「块」(常见 512B 或 4KB)为单位读写,而不是像串口那样按字符流读写。
用户空间常通过 /dev 下的设备节点看到它们,例如:
/dev/sda | |
/dev/sda1/dev/sda2 | |
/dev/nvme0n1/dev/nvme0n1p2 | |
/dev/mmcblk0/dev/mmcblk0p2 | |
/dev/vda/dev/vda1 |
可以记:
整盘 ≈ 一整块物理/虚拟存储;分区 ≈ 在盘上划出的一段连续空间,启动参数里的
root=通常指向某个分区。
分区表(MBR / GPT)告诉内核:这块盘从哪一扇区到哪一扇区算第几个分区。Bootloader 和内核都靠它找到「根分区」在哪。
分区里存的不只是原始字节,还要按某种格式组织成目录、文件、权限、时间戳等。这种格式就是文件系统类型,例如:
没有正确的文件系统驱动,内核即使找到了分区,也「读不懂」上面的目录树。
对 C 开发者的类比:
块设备 ≈ 一块很大的
char buf[](按块访问);文件系统 ≈ 规定如何把数组里的字节解释成「路径 → inode → 数据块」的一套规则与代码;VFS(虚拟文件系统)≈ 内核里统一的「打开 / 读 / 写 / 遍历目录」接口,底下再接到具体文件系统实现。
「挂载」的意思是:让某个存储上的某个文件系统,成为目录树上的一部分。
块设备/分区 + 文件系统类型 + 挂载点 ↓ ↓ ↓ /dev/sda2 + ext4 + /效果直白地说:
/bin、/etc 对用户空间几乎没有真实内容可访问(还没有可用的根);/ 下的内容,/bin、/etc、/usr 等路径才指向真实文件;/home,把 EFI 分区挂到 /boot/efi。命令行里你熟悉的:
mount /dev/sda2 /mnt含义就是:把 /dev/sda2 上的文件系统,接到当前目录树的 /mnt 这个挂载点。启动时内核做的「挂根」,本质上是同一类动作,只是挂载点是 /,而且发生得更早。
硬件和内核基础设施就绪后,内核要做两件对用户可见的大事。对应到源码主线上,大致是:
rest_init() └─ 创建内核线程 → kernel_init() ├─ 准备命名空间 / 挂载根(do_mounts 一带) └─ run_init_process() → 用户空间 PID 1不必死记函数名,但建议认识这条链:start_kernel → rest_init → kernel_init → 挂根 → 执行 init。
内核根据 cmdline 里的 root=(以及 rootfstype= 等)找到根设备,调用挂载相关逻辑(历史上集中在 do_mounts 一带),把该分区上的文件系统挂到 /。
常见两种走法:
root= | ||
switch_root / pivot_root 切到真正的根分区 |
挂载成功后的直接效果:
/sbin/init、/etc/... 等路径;根文件系统可用后,内核会在其中启动第一个用户空间进程。这个进程的进程号(PID)是 1,所以常叫 PID 1,程序名习惯上叫 init。
内核如何找到它?
init=/某路径,就用你指定的;/sbin/init、/etc/init、/bin/init、/bin/sh 等常见路径。源码概念上对应 run_init_process():用内核的「执行用户程序」路径加载该 ELF,把它变成 PID 1。
可以这样判断边界:当 init 已经成功跑起来,就认为内核完成了「把系统运行交给用户空间」这一棒。再往后的服务、登录界面、应用程序,属于第 4 篇。
挂载完成后,用户空间看到的就是一棵以 / 为根的目录树。下面以一台典型 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 → 会变化的数据:日志、缓存、包数据库等结合「块设备 / 文件系统 / 挂载」再读一遍这张表,会更清楚:
/ | /dev/nvme0n1p2)上的 ext4 |
/boot/efi | |
/home | |
/proc/sys、/dev | |
/mnt/media/... |
对启动而言,内核至少要保证:根分区挂上后,/sbin/init(或你指定的 init=)这条路径真实存在且可执行。Ubuntu 上 PID 1 通常是 systemd(/sbin/init 往往是指向 systemd 的符号链接)——细节见第 4 篇。
无论用 Yocto 还是别的构建系统,产出里通常都有:
这几样必须「对得上」。典型错配会表现为:
root= | |
所以后面改 Yocto 配置时,不要只关心「我想装哪个 App」,也要意识到:你其实在组装「能被 Bootloader 加载的内核」+「能被内核挂上的 rootfs」。
嵌入式串口场景:
console=ttyS0,115200root=/dev/mmcblk0p2 rwrootfstype=ext4console=... | |
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 从何而来。