从 start_kernel 到 /init:Linux 3.18.6 内核启动跟踪
课程:《Linux内核原理与分析》
**姓名:**叶丁再
**学号:**20262809
一、实验目标与调试方式
本次实验使用 GDB 跟踪 Linux 内核从start_kernel()到 init 进程启动的过程,并结合源码理解启动早期的初始化、idle 进程和 PID 1 的来历。
实验内核版本为 Linux 3.18.6。普通启动时,我在实验楼虚拟机执行:
cd ~/LinuxKernel/ qemu -kernel linux-3.18.6/arch/x86/boot/bzImage -initrd rootfs.img截图中可以看到内核启动后进入MenuOS>>,说明普通启动路径最终运行了 initrd 中提供的用户态菜单程序。
图1 普通启动后出现 MenuOS 命令提示符。
调试时,QEMU 使用-s -S暂停 CPU 并开放 GDB 远程调试端口;另一个 shell 中加载同版本的vmlinux符号表、连接远程目标并设置断点:
qemu -kernel linux-3.18.6/arch/x86/boot/bzImage \ -initrd rootfs.img -s -S(gdb) file linux-3.18.6/vmlinux (gdb) target remote :1234 (gdb) break start_kernel (gdb) continue-S让模拟 CPU 在启动时保持暂停,-s使用默认的tcp::1234GDB 端口。file加载符号信息,target remote连接 QEMU,continue才让内核继续执行到断点。
图2 GDB 已加载vmlinux符号并连接 QEMU 远程调试端口。
二、start_kernel():建立内核运行所需的基础环境
GDB 在init/main.c:501命中start_kernel()。调用栈显示其上一层是i386_start_kernel(),说明体系结构启动代码把控制权交给通用内核启动入口。
图3start_kernel()断点命中;调用栈回溯到 x86 启动入口,并设置后续跟踪断点。
结合 Linux 3.18.6 的init/main.c,start_kernel()可按职责分成几个阶段:
- **准备早期内核状态。**函数开始执行锁依赖跟踪初始化、初始任务栈边界标记、处理器编号和早期调试对象初始化,并建立启动栈保护值、早期 cgroup 状态。
- **完成体系结构和启动参数设置。**内核先关闭本地中断,再初始化启动 CPU、页地址机制,调用
setup_arch()获取启动命令行;之后建立命令行副本、per-CPU 区域和启动 CPU 状态,并解析早期参数及普通内核参数。 - **建立内存、异常和调度基础。**源码依次初始化日志缓冲区、PID 哈希、早期 VFS 缓存、异常表、陷阱和内存管理。
sched_init()建立调度器基础;随后初始化 RCU、早期中断、IRQ、时钟 tick、定时器、软中断和 timekeeping 等机制。 - **打开中断并准备控制台及其余子系统。**在关键早期结构准备完成后,代码清除“早期中断关闭”状态并打开本地中断,再初始化控制台、检查 initrd 地址和继续初始化进程管理、VFS、proc、cgroup 等设施。
- **转入创建初始内核线程的阶段。**完成架构和通用子系统初始化后,
start_kernel()最后调用rest_init()。GDB 的调用栈显示该调用位于start_kernel()的第 680 行,与所用 Linux 3.18.6 源码一致。
这些初始化并非“把所有服务都启动完”。它们为后续调度、创建内核线程、挂载根文件系统以及启动用户态 init 建立必要条件。具体函数顺序以上游 Linux 3.18.6init/main.c为准。
三、从rest_init()到 PID 1
GDB 在rest_init()断点停下时,回溯栈显示它由start_kernel()调用。源码中的关键步骤是先执行kernel_thread(kernel_init, ...),再创建kthreadd,之后标记并调度启动时的 idle 任务。
图4rest_init()断点和调用栈,调用者是start_kernel()。
源码注释解释了创建顺序:先创建 init,使它取得 PID 1;随后再创建kthreadd,并通过完成量kthreadd_done通知 init 可以继续。因此,kernel_init()对应的内核线程成为 PID 1。GDB 随后在kernel_init()断点命中,调用栈显示它由ret_from_kernel_thread()返回进入。
图5 GDB 命中kernel_init(),并显示其内核线程调用路径。
idle 进程从哪里来
启动早期已经存在一个静态的初始任务init_task。start_kernel()在这个初始任务上下文中运行;它不是由rest_init()新建出来的普通任务。Linux 为这个启动任务保留 PID 0。rest_init()中的init_idle_bootup_task(current)和后续调度调用把当前启动任务作为 boot idle 任务处理;当它不再执行其他初始化工作时,会进入cpu_startup_entry()的 idle 循环。
因此,可以把 idle 进程理解为内核启动所依托的初始任务,PID 0 并不是后来通过创建 PID 1 的同一条路径产生的。
PID 1 怎样变成用户态 init
GDB 在kernel_init_freeable()停下时,源码位置是等待kthreadd_done的完成量。这保证 PID 1 的内核线程等到kthreadd建立好后再继续做可阻塞初始化、SMP 初始化和基本子系统设置。
图6kernel_init_freeable()等待kthreadd_done。
之后kernel_init()检查 initrd 中的 init 路径。GDB 命中run_init_process(init_filename="/init"),调用栈显示该函数由kernel_init()调用;源码将/init放入初始参数,并通过do_execve()发起执行。这是 PID 1 从内核线程转入用户态 init 程序的关键交接点。执行execve会替换进程映像,但保留该进程的 PID。
图7 GDB 停在run_init_process("/init"),可以看到调用栈和do_execve()。
四、证据说明与启动链总结
本次 GDB 截图实际记录了start_kernel()、rest_init()、kernel_init()、kernel_init_freeable()和run_init_process("/init")等节点;普通 QEMU 启动截图记录了MenuOS>>。current在 GDB 中无法直接解析,是因为调试器没有把内核宏current当作普通符号识别;这不代表启动失败。分析 PID 时应结合rest_init()的任务创建顺序、函数调用栈和源码,而不是依赖这条宏表达式。
从启动过程看,内核首先在 PID 0 的初始任务上下文中完成start_kernel()的全局初始化;rest_init()创建 PID 1 的kernel_init和内核线程管理者kthreadd,并让 idle 任务进入调度;PID 1 完成剩余初始化后,通过run_init_process()执行 initrd 中的/init,最终启动用户空间程序。也就是说,Linux 启动不是从一个已经存在的用户进程开始,而是由内核逐层搭建运行环境,再创建并交接给第一个用户态进程。
**证据范围:**现有 GDB 截图停在调用/init的do_execve()之前;MenuOS>>来自普通启动截图。若要展示同一次 GDB 调试会话从断点继续到菜单提示符,还可以在该断点输入continue后补一张 QEMU 截图。
参考资料与课程声明
- Linux 3.18.6 源码:init/main.c
- 《Linux内核原理与分析》MOOC 课程:http://mooc.study.163.com/course/USTC-1000029000
- MenuOS 课程项目:GitHub - mengning/menu: cmdline menu libary · GitHub
叶丁再(20262809)。原创作品转载请注明出处:《Linux内核分析》MOOC 课程 http://mooc.study.163.com/course/USTC-1000029000。