rk3588/kernel-6.1 init 目录作用与内核启动流程分析

rk3588/kernel-6.1 init 目录作用与内核启动流程分析

扩展文档集:见 README.md(总览 + 01–07 专题,原理与实现分篇)。

1. init 目录的作用

init/ 是内核从“刚进入 C 代码”到“拉起用户态 init 进程”这段路径的核心目录,主要承担:

  • 系统早期初始化总控main.c
  • initramfs/initrd 解包与早期根文件系统准备initramfs.cdo_mounts*.c
  • 内核命令行参数解析与 init 进程参数传递main.c
  • initcall 驱动初始化框架调度do_initcalls
  • 版本/初始任务等基础对象version.cinit_task.c

一句话:init/ 负责把系统从“boot CPU 单线程内核态”推进到“多核 + 驱动可用 + 用户态 PID1 运行”。


2. 目录分层与文件职责

init/Makefile 可见构成:

  • 核心:main.o version.o mounts.o init_task.o
  • initramfs 路径:
    • 无 initrd:noinitramfs.o
    • 有 initrd:initramfs.o
  • 根挂载相关:
    • do_mounts.o
    • do_mounts_rd.o(ramdisk)
    • do_mounts_initrd.o(initrd)
  • 其他:calibrate.o(延迟校准)

关键文件职责:

  • main.cstart_kernelrest_initkernel_init_freeablekernel_init
  • do_mounts.croot= 解析、prepare_namespace、根设备挂载
  • initramfs.c:内置 cpio 的解包与文件恢复

3. 启动流程总览(主干调用链)

主链路可抽象为:

  1. start_kernel()
  2. arch_call_rest_init() -> rest_init()
  3. rest_init() 创建:
    • PID1 内核线程入口:kernel_init
    • kthreadd
  4. kernel_init()
    • kernel_init_freeable()
    • 释放 init 内存、切系统状态为 SYSTEM_RUNNING
    • run_init_process(...) 执行用户态 /sbin/init

4. 分阶段启动过程详解

4.1 早期阶段:start_kernel()

start_kernel 做的是“把最小可运行内核环境搭起来”:

  • CPU/体系结构早期初始化
    smp_setup_processor_idsetup_arch
  • 命令行处理
    setup_command_lineparse_early_paramparse_args
  • 内存子系统初始化
    mm_init(页分配、slab、vmalloc 等)
  • 调度与中断基础能力
    sched_initearly_irq_initinit_IRQsoftirq_init
  • 时间子系统
    timekeeping_inittime_inithrtimers_init
  • 控制台与锁调试
    console_initlockdep_init
  • 关键核心子系统
    fork_initcred_initvfs_caches_initcgroup_initsecurity_init

之后调用 arch_call_rest_init(),进入非 __init 的后续阶段。

4.2 任务切换阶段:rest_init()

rest_init 关键动作:

  • 启动 PID1 的内核态线程入口 kernel_init
  • 启动 kthreadd
  • 置系统状态为 SYSTEM_SCHEDULING
  • complete(&kthreadd_done),让 kernel_init 可继续
  • 当前 boot idle 线程进入 cpu_startup_entry

设计重点:先建好 kthreadd,再让后续 init 路径安全创建内核线程。

4.3 可阻塞初始化阶段:kernel_init_freeable()

这一段已经允许阻塞分配,开始完成“大规模子系统与驱动初始化”:

  • smp_prepare_cpus / smp_init / sched_init_smp
  • workqueue_init
  • do_pre_smp_initcalls(early initcall)
  • do_basic_setup()
    • driver_init
    • init_irq_proc
    • do_ctors
    • do_initcalls(按 level 执行各类 __initcall
  • wait_for_initramfs
  • console_on_rootfs
  • 若无可执行的 rdinit,走 prepare_namespace() 挂载最终根文件系统
  • integrity_load_keys

4.4 收尾并进入用户态:kernel_init()

kernel_init 在 freeable 阶段后继续:

  • async_synchronize_full
  • free_initmem 释放 __init* 内存
  • mark_readonly 打开只读保护(rodata 等)
  • system_state = SYSTEM_RUNNING
  • 根据优先级尝试执行 init:
    1. ramdisk_execute_commandrdinit=
    2. execute_commandinit=
    3. CONFIG_DEFAULT_INIT
    4. /sbin/init -> /etc/init -> /bin/init -> /bin/sh
  • 全失败则 panic:No working init found

5. 内核如何拉起用户态 init 进程

Linux 的用户态 init 不是由某个已经存在的用户进程 fork 出来的,而是由内核在启动末期把 PID1 从“内核态启动线程”转换成“用户态 init 程序”。

整体链路:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
start_kernel()
-> arch_call_rest_init()
-> rest_init()
-> user_mode_thread(kernel_init, NULL, CLONE_FS) // 创建 PID1
-> kernel_thread(kthreadd, NULL, CLONE_FS | CLONE_FILES)
-> complete(&kthreadd_done)

PID1: kernel_init()
-> wait_for_completion(&kthreadd_done)
-> kernel_init_freeable()
-> do_basic_setup()
-> wait_for_initramfs()
-> console_on_rootfs()
-> prepare_namespace()
-> free_initmem()
-> system_state = SYSTEM_RUNNING
-> run_init_process(...)
-> kernel_execve(...)
-> bprm_execve()
-> 装载 ELF / 脚本解释器
-> 进入用户态 init

5.1 rest_init() 创建 PID1

rest_init() 中最关键的一句是:

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

它的作用是创建一个将来进入用户态的任务,并让这个任务先执行内核函数 kernel_init()
因为这是系统中第一个正式创建的任务,所以它获得 PID 1。

随后 rest_init() 再创建 kthreadd

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

这里顺序很重要:

  • 先创建 kernel_init,保证 init 获得 PID1;
  • 再创建 kthreadd,后续内核线程都依赖它;
  • kernel_init() 会等待 kthreadd_done,避免在 kthreadd 尚未准备好时创建内核线程。

也就是说,PID1 最开始并不是 /sbin/init,而是一个执行 kernel_init() 的内核启动任务。

5.2 kernel_init_freeable() 准备用户态运行环境

kernel_init() 先等待 kthreadd 完成:

1
wait_for_completion(&kthreadd_done);

然后执行:

1
kernel_init_freeable();

这一阶段完成用户态 init 启动前必须具备的环境:

  • 调度器 SMP 初始化完成;
  • workqueue 初始化完成;
  • 各级 initcall 执行完成,驱动和子系统基本就绪;
  • initramfs 解包完成;
  • /dev/console 在 rootfs 上打开;
  • 如果没有可用的 initramfs /init,则 prepare_namespace() 挂载真实根文件系统;
  • rootfs、设备节点、控制台、基础驱动准备好后,用户态 init 才可能执行成功。

其中这段逻辑决定是否走 initramfs 的 /init

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

默认 ramdisk_execute_command"/init"。如果 initramfs 中存在可执行 /init,内核优先执行它;否则挂载真实根文件系统,再寻找 /sbin/init 等。

5.3 kernel_init() 选择哪个 init 程序

kernel_init_freeable() 结束后,kernel_init() 做启动收尾:

1
2
3
4
5
6
7
async_synchronize_full()
free_initmem()
mark_readonly()
pti_finalize()
system_state = SYSTEM_RUNNING
rcu_end_inkernel_boot()
do_sysctl_args()

然后开始选择用户态 init。优先级如下:

  1. rdinit= 指定的 initramfs init,默认是 /init
  2. init= 指定的真实根文件系统 init;
  3. CONFIG_DEFAULT_INIT
  4. 内核硬编码 fallback:
    • /sbin/init
    • /etc/init
    • /bin/init
    • /bin/sh

源码逻辑可抽象为:

1
2
3
4
5
6
7
8
9
10
11
12
13
if (ramdisk_execute_command)
run_init_process(ramdisk_execute_command);

if (execute_command)
run_init_process(execute_command);

if (CONFIG_DEFAULT_INIT[0] != '\0')
run_init_process(CONFIG_DEFAULT_INIT);

try_to_run_init_process("/sbin/init");
try_to_run_init_process("/etc/init");
try_to_run_init_process("/bin/init");
try_to_run_init_process("/bin/sh");

如果 init= 指定的程序存在但执行失败,内核会直接 panic;如果 fallback 路径一个都无法执行,也会 panic:

1
No working init found. Try passing init= option to kernel.

5.4 run_init_process() 如何把 PID1 变成用户态 init

run_init_process() 会设置 argv/envp,然后调用 kernel_execve()

1
2
3
4
5
6
static int run_init_process(const char *init_filename)
{
argv_init[0] = init_filename;
pr_info("Run %s as init process\n", init_filename);
return kernel_execve(init_filename, argv_init, envp_init);
}

这一步和用户态调用 execve() 的效果类似:不新建进程,而是把当前 PID1 的地址空间和执行镜像替换成目标 init 程序

因此:

  • PID 仍然是 1;
  • task_struct 仍是这个 init 任务;
  • 原来的 kernel_init() 内核态执行路径被新程序镜像替换;
  • 成功后不会再返回 kernel_init()
  • CPU 最终通过体系结构返回用户态路径,开始执行 /init/sbin/init 的入口。

kernel_execve() 内部大致做:

1
2
3
4
5
6
7
8
getname_kernel(init_filename)
-> alloc_bprm()
-> 复制 argv/envp
-> bprm_execve()
-> search_binary_handler()
-> load_elf_binary() / load_script()
-> 建立用户态 mm、栈、入口地址
-> 返回用户态执行

如果 init 是脚本,例如:

1
#!/bin/sh

则 exec 路径会继续寻找并执行解释器 /bin/sh。所以 initramfs 或 rootfs 中不仅要有 init 文件,还要有它依赖的解释器、动态链接器和库。

5.5 init=rdinit= 和根文件系统的关系

两个参数含义不同:

  • rdinit=/xxx:优先从 initramfs/rootfs 早期环境中执行,常用于 initramfs 的 /init
  • init=/xxx:挂载真实根文件系统后执行,常见为 init=/sbin/initinit=/bin/sh

典型场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
使用 initramfs:
kernel_init_freeable()
-> wait_for_initramfs()
-> 发现 /init 可执行
kernel_init()
-> run_init_process("/init")

使用真实 rootfs:
kernel_init_freeable()
-> initramfs 无 /init
-> prepare_namespace() 挂载 root=
kernel_init()
-> run_init_process("/sbin/init")

调试启动失败时,常用:

1
2
init=/bin/sh
rdinit=/bin/sh

如果能进入 shell,说明内核已基本完成启动,问题多半在 rootfs、init 程序、动态库或启动脚本。


6. 命令行参数在启动中的作用

init/main.c 里,命令行参数分几类:

  • early_param:超早期生效(如 logleveldebugquietrodata
  • __setup:常规启动参数(如 init=rdinit=
  • "--" 后参数:作为用户态 init 的参数传递

典型参数:

  • init=/path/to/init:指定 PID1 可执行文件
  • rdinit=/path/to/init:指定 initramfs 内早期 init
  • ro/rw:根文件系统挂载只读/读写(在 do_mounts.c

7. initramfs/initrd 与根文件系统切换

7.1 initramfs 解包

initramfs.c 负责解析 cpio 流并在 rootfs 中创建文件/目录/设备节点。
这一步让系统在没有真实块设备根分区前也能获得最小用户空间。

7.2 根文件系统准备

do_mounts.c 负责:

  • 解析 root=(支持设备名、PARTUUID=PARTLABEL= 等)
  • 处理 ro/rw/rootwait 等策略
  • prepare_namespace() 完成真实根挂载与切换准备

若使用 initramfs,通常会先跑早期用户空间脚本再切换到真实根。


8. initcall 机制在启动中的位置

do_initcalls() 是内核驱动/子系统初始化总调度器:

  • 按 initcall level 依次执行(early/core/postcore/arch/subsys/fs/device/late)
  • 每个 level 会重新解析命令行片段
  • 可配 initcall_debug 观测耗时与失败

这套机制决定了绝大多数驱动何时“上线”。


9. init 目录在系统稳定性中的关键点

  1. start_kernel 顺序极其敏感
    中断、调度、内存初始化次序错误会导致早期崩溃。

  2. PID1 启动失败直接致命
    run_init_process 全部失败后系统 panic。

  3. 根文件系统参数错误常见导致卡启动
    root= 不正确、设备未就绪、initrd 路径错误都会阻塞启动。

  4. initcall 卡住会表现为“开机停在某阶段”
    借助 initcall_debug 和 early console 可定位。


10. 调试建议(面向启动问题)

  1. 查看早期日志

    • 使用 earlycon、提高 loglevel
    • 关注 Kernel command lineRun ... as init process 输出
  2. 验证 PID1 可执行路径

    • 检查 init= / rdinit= 是否存在并可执行
    • 检查 initramfs 是否包含所需解释器/依赖
  3. 定位根挂载问题

    • 核对 root= 语法与设备可见性
    • 必要时加 rootwait
  4. 定位驱动初始化卡顿

    • 开启 initcall_debug
    • 根据最后一条 initcall 输出追踪对应驱动

11. 一句话总结

init/ 的本质是内核启动编排器:
start_kernel 完成核心基础初始化,rest_init 创建 PID1 的 kernel_init 启动任务,kernel_init_freeable 执行驱动与根文件系统准备,最后 run_init_process() 通过 kernel_execve() 把 PID1 替换成用户态 /init/sbin/init

文章互动

阅读 --

留言

0 条留言

正在加载留言…