Linux 启动流程学习笔记:start_kernel、根文件系统与 init

本文记录一次围绕 Linux 6.1 内核启动流程的学习过程,重点关注:

  • start_kernel() 的整体职责
  • 根文件系统如何挂载
  • PID 1 / init 进程如何创建
  • initramfs 中的 /init 与真实根文件系统中的 /sbin/init 的区别
  • 根据实际串口启动日志判断本机启动路径

1. start_kernel 的整体位置

start_kernel() 位于:

1
init/main.c

它可以理解为 Linux 内核进入 C 世界后的总启动入口。架构相关的汇编和早期初始化完成后,会进入 start_kernel()

简化后的主线如下:

1
2
3
4
5
6
7
8
arch 入口汇编/早期 C
-> start_kernel()
-> 初始化 CPU、内存、调度、中断、时间、RCU、VFS、cgroup 等核心子系统
-> arch_call_rest_init()
-> rest_init()
-> 创建 kernel_init 线程,成为 PID 1
-> 创建 kthreadd 线程,通常为 PID 2
-> boot CPU 进入 idle

start_kernel() 本身不会直接启动用户态程序。它的核心职责是把内核运行环境搭好,然后通过 rest_init() 创建后续负责完成启动的线程。

2. PID 1 是如何创建的

rest_init() 中有关键代码:

1
pid = user_mode_thread(kernel_init, NULL, CLONE_FS);

这会创建一个运行 kernel_init() 的线程。因为它是第一个将来进入用户态的线程,所以会成为 PID 1。

随后又创建:

1
pid = kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES);

这个线程通常是 PID 2,即 kthreadd,负责后续内核线程的创建和管理。

因此,PID 1 一开始并不是 /sbin/init,而是运行在内核态的 kernel_init()。后面它会通过 kernel_execve() 替换成真正的用户态 init 程序。

3. kernel_init 的主要工作

kernel_init() 的简化逻辑如下:

1
2
3
4
5
6
kernel_init()
-> wait_for_completion(&kthreadd_done)
-> kernel_init_freeable()
-> 释放 initmem
-> 设置 system_state = SYSTEM_RUNNING
-> 尝试执行用户态 init

其中 kernel_init_freeable() 会完成很多后期初始化:

1
2
3
4
5
6
7
8
9
10
11
12
kernel_init_freeable()
-> smp_prepare_cpus()
-> workqueue_init()
-> do_pre_smp_initcalls()
-> smp_init()
-> sched_init_smp()
-> do_basic_setup()
-> driver_init()
-> do_initcalls()
-> wait_for_initramfs()
-> console_on_rootfs()
-> 根据 /init 是否存在决定是否 prepare_namespace()

4. /init 是什么

/init 指的是 initramfs 解包后的内存根文件系统里的第一个用户态程序。

内核里默认配置:

1
static char *ramdisk_execute_command = "/init";

也就是说,内核默认会优先尝试执行当前 rootfs 中的 /init

这里的 rootfs 通常是内核早期挂载的内存文件系统,可以是 ramfs 或 tmpfs。initramfs 会被解包到这个 rootfs 中。

/init 可以是:

1
2
3
4
shell 脚本
busybox
systemd
自定义 ELF 程序

典型 initramfs /init 脚本可能会做:

1
2
3
4
5
6
7
1. 挂载 /proc、/sys、/dev
2. 加载必要驱动模块
3. 等待根设备出现
4. 解密磁盘、组装 RAID/LVM
5. 挂载真正根文件系统
6. switch_root 到真实根
7. 执行 /sbin/init

5. 内核如何判断有没有 initramfs /init

关键代码在 kernel_init_freeable()

1
2
3
4
if (init_eaccess(ramdisk_execute_command) != 0) {
ramdisk_execute_command = NULL;
prepare_namespace();
}

默认情况下,ramdisk_execute_command 是:

1
/init

因此判断逻辑是:

1
2
3
4
5
6
7
8
如果 /init 存在且可执行:
不调用 prepare_namespace()
后面 kernel_init() 执行 /init

如果 /init 不存在或不可执行:
ramdisk_execute_command = NULL
调用 prepare_namespace()
挂载 root= 指定的真实根文件系统

如果启动参数指定了:

1
rdinit=/xxx

ramdisk_execute_command 会变成 /xxx,内核会优先检查并执行这个路径,而不是默认的 /init

6. prepare_namespace 如何挂载真实根文件系统

当 initramfs 里没有可执行的 /init 时,内核会进入:

1
prepare_namespace();

它位于:

1
init/do_mounts.c

简化流程:

1
2
3
4
5
6
7
8
9
10
11
12
prepare_namespace()
-> 如果有 rootdelay=,先等待指定秒数
-> wait_for_device_probe()
-> md_run_setup()
-> 解析 root= 参数
-> name_to_dev_t() 将 root= 转换成设备号或特殊 root 类型
-> 如有必要处理旧式 initrd
-> 如果 rootwait,等待根设备出现
-> mount_root()
-> devtmpfs_mount()
-> init_mount(".", "/", NULL, MS_MOVE, NULL)
-> init_chroot(".")

其中 mount_root() 会根据不同根文件系统类型分支处理:

1
2
3
4
root=/dev/nfs      -> NFS root
root=/dev/cifs -> CIFS root
普通块设备 -> mount_block_root()
nodev 文件系统 -> mount_nodev_root()

对普通块设备或 UBI/UBIFS 根文件系统而言,最终会把真实根挂载到 /root,再通过 MS_MOVE 移动成真正的 /

7. 用户态 init 的选择顺序

根文件系统准备好后,kernel_init() 会尝试执行 init 程序。

顺序大致是:

1
2
3
4
5
6
7
ramdisk_execute_command    默认 /init,可由 rdinit= 指定
execute_command 由 init= 指定
CONFIG_DEFAULT_INIT
/sbin/init
/etc/init
/bin/init
/bin/sh

如果都失败,内核会 panic:

1
No working init found

执行用户态程序的函数是:

1
kernel_execve(init_filename, argv_init, envp_init);

一旦成功,PID 1 就从内核态的 kernel_init() 变成真正的用户态 init 程序。

8. initramfs /init 与 /sbin/init 的区别

两者容易混淆,但位置和角色不同。

1
/init

通常位于 initramfs 解包出来的内存 rootfs 中。它是内核优先尝试执行的早期用户态程序。

1
/sbin/init

通常位于真实根文件系统中,例如 eMMC、SD、UBIFS、NFS root 等。它是在真实根挂载完成后执行的系统 init。

判断依据:

1
2
3
4
5
6
日志出现 Run /init as init process
-> 使用了 initramfs 里的 /init

日志出现 VFS: Mounted root (...)
日志出现 Run /sbin/init as init process
-> 挂载了真实根文件系统,然后执行 /sbin/init

9. 本机启动日志分析

本次串口日志文件:

1
MobaXterm_COM9USB-Enhanced-SERIALCH343COM91_20260417_210416.txt

关键启动参数:

1
2
rw rootwait earlycon=uart8250,mmio32,0xff0a0000 console=ttyFIQ0
ubi.mtd=5 root=ubi0:rootfs rootfstype=ubifs

这说明根文件系统来自:

1
2
3
4
MTD 分区 5
-> attach 为 ubi0
-> volume 名称 rootfs
-> 文件系统类型 ubifs

关键日志:

1
2
3
4
5
6
7
ubi0: attaching mtd5
ubi0: attached mtd5 (name "rootfs", size 170 MiB)
UBIFS (ubi0:0): Mounting in unauthenticated mode
UBIFS (ubi0:0): UBIFS: mounted UBI device 0, volume 0, name "rootfs"
VFS: Mounted root (ubifs filesystem) on device 0:14.
devtmpfs: mounted
Run /sbin/init as init process

因此可以确定本机实际启动路径是:

1
2
3
4
5
6
7
8
9
10
kernel_init()
-> kernel_init_freeable()
-> wait_for_initramfs()
-> 没有找到可执行的 /init
-> prepare_namespace()
-> 解析 root=ubi0:rootfs
-> 等待/附加 ubi.mtd=5
-> 挂载 UBIFS 根文件系统
-> run_init_process("/sbin/init")
-> kernel_execve("/sbin/init", ...)

结论:

1
2
3
本次启动没有使用 initramfs /init。
内核挂载了 SPI NAND 上 mtd5 对应的 UBI/UBIFS 根文件系统。
随后执行真实根文件系统中的 /sbin/init。

10. /sbin/init 实际是 BusyBox

在 rootfs 构建目录中查看:

1
realpath output/rootfs/target/sbin/init

结果:

1
/home/dky/workspaces_rk/buildroot/output/atk_dlrk3506/target/usr/bin/busybox

这说明真实根文件系统中:

1
/sbin/init -> /usr/bin/busybox

内核实际执行的是:

1
/sbin/init

但由于它是指向 BusyBox 的符号链接,所以最终运行的是 BusyBox 的 init applet。

路径可以理解为:

1
2
3
4
5
内核执行 /sbin/init
-> /sbin/init 是符号链接
-> 指向 /usr/bin/busybox
-> busybox 根据 argv[0] == "init"
-> 运行 busybox init 逻辑

后续系统服务启动,例如:

1
2
3
4
5
Starting syslogd
Starting klogd
Starting network
starting weston...
Starting sshd

通常来自 BusyBox init 根据 /etc/inittab 和相关启动脚本执行的结果。

11. 后续继续追踪方向

如果继续分析用户态启动流程,可以从真实 rootfs 中这些文件入手:

1
2
3
4
/etc/inittab
/etc/init.d/rcS
/etc/init.d/*
/etc/fstab

对当前系统而言,建议重点关注:

1
2
3
output/rootfs/target/etc/inittab
output/rootfs/target/etc/init.d/rcS
output/rootfs/target/etc/fstab

它们会解释 BusyBox init 如何启动 syslog、klog、udev、网络、weston、sshd 以及 userdata 等挂载逻辑。