rk3588/kernel-6.1 init 目录作用与内核启动流程分析
扩展文档集:见 README.md(总览 + 01–07 专题,原理与实现分篇)。
1. init 目录的作用
init/ 是内核从“刚进入 C 代码”到“拉起用户态 init 进程”这段路径的核心目录,主要承担:
- 系统早期初始化总控(
main.c) - initramfs/initrd 解包与早期根文件系统准备(
initramfs.c、do_mounts*.c) - 内核命令行参数解析与 init 进程参数传递(
main.c) - initcall 驱动初始化框架调度(
do_initcalls) - 版本/初始任务等基础对象(
version.c、init_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
- 无 initrd:
- 根挂载相关:
do_mounts.odo_mounts_rd.o(ramdisk)do_mounts_initrd.o(initrd)
- 其他:
calibrate.o(延迟校准)
关键文件职责:
main.c:start_kernel、rest_init、kernel_init_freeable、kernel_initdo_mounts.c:root=解析、prepare_namespace、根设备挂载initramfs.c:内置 cpio 的解包与文件恢复
3. 启动流程总览(主干调用链)
主链路可抽象为:
start_kernel()arch_call_rest_init()->rest_init()rest_init()创建:- PID1 内核线程入口:
kernel_init kthreadd
- PID1 内核线程入口:
kernel_init():kernel_init_freeable()- 释放 init 内存、切系统状态为
SYSTEM_RUNNING run_init_process(...)执行用户态/sbin/init等
4. 分阶段启动过程详解
4.1 早期阶段:start_kernel()
start_kernel 做的是“把最小可运行内核环境搭起来”:
- CPU/体系结构早期初始化
smp_setup_processor_id、setup_arch - 命令行处理
setup_command_line、parse_early_param、parse_args - 内存子系统初始化
mm_init(页分配、slab、vmalloc 等) - 调度与中断基础能力
sched_init、early_irq_init、init_IRQ、softirq_init - 时间子系统
timekeeping_init、time_init、hrtimers_init - 控制台与锁调试
console_init、lockdep_init - 关键核心子系统
fork_init、cred_init、vfs_caches_init、cgroup_init、security_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_smpworkqueue_initdo_pre_smp_initcalls(early initcall)do_basic_setup():driver_initinit_irq_procdo_ctorsdo_initcalls(按 level 执行各类__initcall)
wait_for_initramfsconsole_on_rootfs- 若无可执行的
rdinit,走prepare_namespace()挂载最终根文件系统 integrity_load_keys
4.4 收尾并进入用户态:kernel_init()
kernel_init 在 freeable 阶段后继续:
async_synchronize_fullfree_initmem释放__init*内存mark_readonly打开只读保护(rodata 等)system_state = SYSTEM_RUNNING- 根据优先级尝试执行 init:
ramdisk_execute_command(rdinit=)execute_command(init=)CONFIG_DEFAULT_INIT/sbin/init->/etc/init->/bin/init->/bin/sh
- 全失败则 panic:
No working init found
5. 内核如何拉起用户态 init 进程
Linux 的用户态 init 不是由某个已经存在的用户进程 fork 出来的,而是由内核在启动末期把 PID1 从“内核态启动线程”转换成“用户态 init 程序”。
整体链路:
1 | start_kernel() |
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 | if (init_eaccess(ramdisk_execute_command) != 0) { |
默认 ramdisk_execute_command 是 "/init"。如果 initramfs 中存在可执行 /init,内核优先执行它;否则挂载真实根文件系统,再寻找 /sbin/init 等。
5.3 kernel_init() 选择哪个 init 程序
kernel_init_freeable() 结束后,kernel_init() 做启动收尾:
1 | async_synchronize_full() |
然后开始选择用户态 init。优先级如下:
rdinit=指定的 initramfs init,默认是/init;init=指定的真实根文件系统 init;CONFIG_DEFAULT_INIT;- 内核硬编码 fallback:
/sbin/init/etc/init/bin/init/bin/sh
源码逻辑可抽象为:
1 | if (ramdisk_execute_command) |
如果 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 | static int run_init_process(const char *init_filename) |
这一步和用户态调用 execve() 的效果类似:不新建进程,而是把当前 PID1 的地址空间和执行镜像替换成目标 init 程序。
因此:
- PID 仍然是 1;
task_struct仍是这个 init 任务;- 原来的
kernel_init()内核态执行路径被新程序镜像替换; - 成功后不会再返回
kernel_init(); - CPU 最终通过体系结构返回用户态路径,开始执行
/init或/sbin/init的入口。
kernel_execve() 内部大致做:
1 | getname_kernel(init_filename) |
如果 init 是脚本,例如:
1 |
则 exec 路径会继续寻找并执行解释器 /bin/sh。所以 initramfs 或 rootfs 中不仅要有 init 文件,还要有它依赖的解释器、动态链接器和库。
5.5 init=、rdinit= 和根文件系统的关系
两个参数含义不同:
rdinit=/xxx:优先从 initramfs/rootfs 早期环境中执行,常用于 initramfs 的/init;init=/xxx:挂载真实根文件系统后执行,常见为init=/sbin/init、init=/bin/sh。
典型场景:
1 | 使用 initramfs: |
调试启动失败时,常用:
1 | init=/bin/sh |
如果能进入 shell,说明内核已基本完成启动,问题多半在 rootfs、init 程序、动态库或启动脚本。
6. 命令行参数在启动中的作用
init/main.c 里,命令行参数分几类:
- early_param:超早期生效(如
loglevel、debug、quiet、rodata) - __setup:常规启动参数(如
init=、rdinit=) "--"后参数:作为用户态 init 的参数传递
典型参数:
init=/path/to/init:指定 PID1 可执行文件rdinit=/path/to/init:指定 initramfs 内早期 initro/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 目录在系统稳定性中的关键点
start_kernel顺序极其敏感
中断、调度、内存初始化次序错误会导致早期崩溃。PID1 启动失败直接致命
run_init_process全部失败后系统 panic。根文件系统参数错误常见导致卡启动
root=不正确、设备未就绪、initrd路径错误都会阻塞启动。initcall 卡住会表现为“开机停在某阶段”
借助initcall_debug和 early console 可定位。
10. 调试建议(面向启动问题)
查看早期日志
- 使用
earlycon、提高loglevel - 关注
Kernel command line、Run ... as init process输出
- 使用
验证 PID1 可执行路径
- 检查
init=/rdinit=是否存在并可执行 - 检查 initramfs 是否包含所需解释器/依赖
- 检查
定位根挂载问题
- 核对
root=语法与设备可见性 - 必要时加
rootwait
- 核对
定位驱动初始化卡顿
- 开启
initcall_debug - 根据最后一条 initcall 输出追踪对应驱动
- 开启
11. 一句话总结
init/ 的本质是内核启动编排器:start_kernel 完成核心基础初始化,rest_init 创建 PID1 的 kernel_init 启动任务,kernel_init_freeable 执行驱动与根文件系统准备,最后 run_init_process() 通过 kernel_execve() 把 PID1 替换成用户态 /init 或 /sbin/init。
正在加载留言…