尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

跟踪分析 Linux 内核的启动过程

发布时间:2026/9/28 20:15:53

资讯中心
01
ARTICLE

跟踪分析 Linux 内核的启动过程

跟踪分析 Linux 内核的启动过程
作者李令琪原创作品转载请注明出处《Linux 内核分析》MOOC课程http://mooc.study.163.com/course/USTC-1000029000一、实验环境与 QEMU 启动实验目录为cd~/LinuxKernel/首先检查实验目录pwdls本实验使用 Linux 3.18.6 的内核镜像bzImage和课程提供的rootfs.imgqemu-kernellinux-3.18.6/arch/x86/boot/bzImage-initrdrootfs.img实验截图 1QEMU 启动 Linux 3.18.6 并进入 MenuOS从实验结果可以看到QEMU 成功装载 Linux 3.18.6 内核和rootfs.img。完成内核初始化后系统显示 MenuOS 标志并进入MenuOS命令提示符。这说明内核已经完成启动并成功运行 rootfs中提供的用户空间程序。随后执行help、version等命令可以看到 MenuOS 提供的基本命令。实验截图 2MenuOS 的help和version命令这一步证明实验所使用的用户空间环境能够正常工作也为后续分析内核如何最终启动/init提供了可观察的终点。实验截图 3执行quit后的内核异常信息在 MenuOS 中执行quit后QEMU中出现调用栈和 panic 信息其中能够看到do_exit、do_group_exit、SyS_exit_group等函数。这并不是普通应用程序退出时应有的现象。结合后面的 GDB调试可以知道本实验的/init最终作为PID 1运行。PID 1 是 Linux用户空间的初始进程具有特殊地位当它退出后内核无法按照普通用户进程退出的方式继续维持系统因此会进入严重错误状态。这个现象从另一个角度验证了MenuOS/init与 1 号进程之间的关系。二、使用 GDB 远程调试 Linux 内核为了调试内核重新使用以下命令启动 QEMUqemu-kernellinux-3.18.6/arch/x86/boot/bzImage-initrdrootfs.img-s-S其中-SQEMU 启动后暂时冻结虚拟 CPU等待调试器控制-s等价于开启-gdb tcp::1234即在 TCP 1234 端口等待 GDB 连接。然后打开另一个终端cd~/LinuxKernel/ gdb在 GDB 中加载包含调试符号的vmlinuxfile linux-3.18.6/vmlinux break start_kernel target remote :1234 continue这里需要区分bzImage和vmlinuxQEMU 使用压缩后的bzImage启动内核而 GDB 使用包含符号信息的vmlinux来完成源码级调试。实验截图 4GDB 成功命中start_kernel截图中可以看到Breakpoint 1, start_kernel() at init/main.c:501因此已经成功进入 Linux 3.18.6 的start_kernel()。这也是本实验分析内核C 语言初始化过程的主要起点。三、start_kernel()的入口与调用关系在start_kernel()断点处执行bt list实验截图 5start_kernel()的调用栈与源代码从调用栈可以看到#0 start_kernel() at init/main.c:501 #1 i386_start_kernel() at arch/x86/kernel/head32.c:49这说明在当前 32 位 x86实验环境中体系结构相关的早期启动代码完成必要准备后由i386_start_kernel()进入通用内核初始化函数start_kernel()。因此start_kernel()并不是计算机上电后执行的第一条 Linux指令。在它之前已经经历了体系结构相关的启动过程。但是从 Linux 通用内核C 代码的角度看start_kernel()是最重要的初始化入口之一。四、start_kernel()执行过程分析Linux 3.18.6 的start_kernel()位于init/main.c。这个函数非常长因为内核必须在创建正常用户空间环境之前建立几乎所有基础运行条件。从整体功能上可以把start_kernel()的执行过程理解为以下几个阶段。1. 建立最基本的内核运行环境内核首先完成与启动CPU、体系结构和启动参数有关的准备工作包括处理命令行参数以及调用体系结构相关的初始化代码。其中setup_arch()是非常重要的一步它负责完成大量与 x86体系结构相关的初始化并为后续通用内核代码准备硬件和内存布局信息。2. 建立内存管理机制Linux 内核运行离不开内存管理。启动早期只能使用有限的内存管理手段因此start_kernel()需要逐渐建立正式的页分配、slab/slub、内核对象等内存管理基础设施。这一阶段完成后内核后面的子系统才可以更自由地进行动态内存分配。3. 初始化调度系统start_kernel()会初始化 Linux调度器。调度器是多任务系统的核心组件之一它决定可运行任务如何获得 CPU。这一阶段非常关键因为后面的rest_init()将真正产生新的内核执行流。如果调度系统没有建立就无法正常调度这些任务。4. 初始化中断、时钟和定时器内核还需要建立中断处理、时钟源和定时器等基础设施。中断使 CPU能够响应硬件事件时钟和定时器则为调度、超时、延迟任务等功能提供时间基础。因此这些组件都是系统进入正常运行状态之前必须完成的初始化内容。5. 初始化控制台和其他核心子系统随着初始化继续进行内核逐步建立 console、VFS、缓存、RCU等基础机制并继续处理各种核心子系统。从启动过程的角度看start_kernel()的核心意义不是启动一个普通程序而是从一个功能非常有限的早期内核环境逐步构造出能够进行进程调度、内存分配、中断处理、文件系统访问和设备初始化的完整内核运行环境。6. 进入rest_init()当start_kernel()的主体初始化工作完成后会进入rest_init()。这一步是本实验中非常重要的转折点此前主要是在当前启动执行流中建立内核基础设施而从rest_init()开始Linux将创建最初的重要任务并逐步把系统带入正常的多任务运行状态。五、设置关键断点为了避免对庞大的内核启动代码逐条单步执行本实验采用关键函数断点 continue的方法跟踪启动主线。实验中设置的关键断点包括break start_kernel break rest_init break kernel_init break kernel_init_freeable break do_basic_setup break do_initcalls break run_init_process并使用info breakpoints检查断点。实验截图 6关键断点设置结果截图表明已经成功设置了从start_kernel到run_init_process的关键调试点。值得注意的是在编译优化开启的情况下某些函数可能被内联因此 GDB显示的断点位置可能落在调用者kernel_init_freeable()的具体源代码行而不是显示成完全独立的函数入口。这属于编译优化后的正常调试现象并不意味着相应初始化过程没有执行。六、rest_init()从单一启动执行流走向多任务继续执行后GDB 命中Breakpoint 2, rest_init() at init/main.c:394实验截图 7进入rest_init()调用栈显示rest_init() start_kernel() i386_start_kernel()因此可以清楚地看到i386_start_kernel ↓ start_kernel ↓ rest_initrest_init()的重要任务之一是创建 Linux 启动后最早的一批特殊任务。从概念上可以表示为start_kernel() | v rest_init() / \ / \ v v kernel_init kthreadd PID 1 PID 2 原有启动任务 -------------------------------- idle / swapper PID 0这里最需要注意的是PID 0、PID 1、PID 2 并不是完全以相同方式产生的。七、idle 进程是怎么来的PID 0 经常被称为swapper或 idle 任务。它不能简单理解成rest_init()使用fork()新建出来的第一个进程。Linux在内核启动早期就已经存在一个静态初始化的初始任务即init_task。start_kernel()的启动上下文本身就建立在这个初始任务基础之上。因此更准确的理解是0 号任务不是在rest_init()中通过普通进程创建机制新创建出来的而是Linux 启动时就存在的初始任务。当rest_init()创建出后续重要任务并完成相应同步工作以后原来的启动执行流最终进入 CPUidle 路径。CPU 没有其他可运行任务时就执行 idle 任务。因此 PID 0 的来源可以概括为内核早期静态初始化的 init_task ↓ 执行早期内核初始化 ↓ start_kernel() ↓ rest_init() ↓ 原启动执行流进入 idle ↓ PID 0 / swapper / idle八、1 号进程kernel_init()继续运行后GDB 命中Breakpoint 3, kernel_init (unused0x0) at init/main.c:931实验截图 8进入kernel_init()截图中的源代码可以看到staticint__refkernel_init(void*unused){intret;kernel_init_freeable();...}rest_init()创建执行kernel_init()的任务。由于它是启动过程中创建的第一个新任务因此获得特殊的PID 1。此时它仍然执行内核代码所以不能简单地说一创建就是普通用户空间 init程序。更准确地说它首先作为内核中的kernel_init执行流继续完成剩余初始化之后才会通过执行用户空间/init完成角色转换。因此 PID 1 的演化过程可以理解为rest_init() ↓ 创建执行 kernel_init() 的任务 ↓ PID 1 ↓ kernel_init_freeable() ↓ 完成剩余内核初始化 ↓ run_init_process(/init) ↓ do_execve(...) ↓ 用户空间 /init这正是1 号进程怎么来的这个问题的核心。九、2 号进程kthreaddrest_init()还会创建kthreadd它通常获得 PID 2。kthreadd是 Linux中非常重要的内核线程管理者后续许多内核线程的创建都与它有关。因此 Linux 启动后最初三个特殊 PID 可以概括为PID 典型名称 来源与作用0 idle / swapper 启动时已经存在的初始任务最终进入CPU idle1 initrest_init()创建的kernel_init执行流最终执行用户空间/init2 kthreaddrest_init()创建用于支持和管理后续内核线程其中最容易混淆的是 PID 0它不是和 PID 1、PID 2 一样在rest_init()中新创建的普通任务。十、kernel_init_freeable()完成可释放的初始化工作继续执行后GDB 命中Breakpoint 4, kernel_init_freeable() at init/main.c:976实验截图 9进入kernel_init_freeable()截图中能够看到wait_for_completion(kthreadd_done);说明kernel_init_freeable()会等待kthreadd完成必要的建立过程然后继续进行剩余初始化。函数名中的freeable也很有意义Linux中很多启动阶段的代码和数据只在初始化时需要。系统启动完成以后这部分带有初始化属性的内存可以被释放从而避免长期占用宝贵的内核内存。十一、do_basic_setup()与do_initcalls()继续跟踪后实验停在kernel_init_freeable()中调用do_basic_setup()的位置kernel_init_freeable() at init/main.c:1004 do_basic_setup();实验截图 10执行do_basic_setup()截图中还可以看到smp_init();sched_init_smp();do_basic_setup();说明在进入do_basic_setup()之前内核已经继续完成 SMP和调度相关初始化。do_basic_setup()会进一步完成设备和内核子系统的初始化其中非常重要的一项就是执行do_initcalls()。Linux 中大量驱动程序和子系统并不是全部直接写死在start_kernel()中依次调用而是通过 initcall机制注册初始化函数。内核启动时再按照不同初始化级别逐步调用这些函数。常见的初始化级别包括pure_initcall core_initcall postcore_initcall arch_initcall subsys_initcall fs_initcall device_initcall late_initcall因此可以把do_initcalls()理解成一个重要的批量初始化调度器它按照预定顺序执行各个子系统和驱动注册的初始化函数。从整体上看start_kernel() | -- 建立内核最基础的运行环境 | v rest_init() | -- 创建 PID 1 -- 创建 PID 2 | v kernel_init() | v kernel_init_freeable() | -- SMP / scheduler 等后续初始化 | v do_basic_setup() | v do_initcalls() | -- 文件系统、设备、驱动、子系统等初始化这说明 Linux的启动并不是一个函数把所有组件一次性初始化完毕而是分阶段逐步建立系统。十二、run_init_process()从内核走向用户空间本实验最关键的最后一个断点是Breakpoint 7, run_init_process (init_filename0xc18d295d /init) at init/main.c:907实验截图 11run_init_process(/init)这个截图给出了非常直接的证据init_filename /init同时源代码显示staticintrun_init_process(constchar*init_filename){argv_init[0]init_filename;returndo_execve(getname_kernel(init_filename),(constchar__user*const__user*)argv_init,(constchar__user*const__user*)envp_init);}也就是说kernel_init()最终调用run_init_process()而run_init_process()进一步通过do_execve()执行/init。这里发生的是 Linux 启动过程中的关键转换PID 1 没有消失也不是重新创建了一个新的 PID 1而是当前 PID 1 通过exec 机制用/init的用户空间程序映像替换当前执行映像。因此从进程身份上它仍然是 PID 1但执行内容已经从内核启动阶段的kernel_init过渡到了用户空间的/init。十三、继续运行后进入 MenuOS在run_init_process()断点处执行continueQEMU 继续运行最终出现 MenuOS。实验截图 12GDB 放行后/init成功进入 MenuOS这张截图将 GDB 中的run_init_process(init_filename/init)与 QEMU 中实际出现的MenuOS放在同一个实验现场中因此形成了非常完整的证据链kernel_init() ↓ run_init_process(/init) ↓ do_execve(/init, ...) ↓ PID 1 开始执行用户空间 /init ↓ MenuOS至此本实验已经从start_kernel()一直跟踪到了用户空间 init程序的实际启动。十四、完整启动路径总结结合本次 GDB 实验可以将观察到的 Linux 3.18.6 启动主线概括为x86 体系结构相关早期启动代码 ↓ i386_start_kernel() ↓ start_kernel() ↓ 体系结构、内存、调度、中断、时钟等核心初始化 ↓ rest_init() ┌────┴───────────────┐ ↓ ↓ kernel_init kthreadd PID 1 PID 2 ↓ kernel_init_freeable() ↓ do_basic_setup() ↓ do_initcalls() ↓ 完成剩余初始化 ↓ run_init_process(/init) ↓ do_execve(/init, ...) ↓ 用户空间 /init仍为 PID 1 ↓ MenuOS 与此同时 原始启动任务 → idle / swapperPID 0十五、对 Linux 系统启动过程的理解通过本次实验我对 Linux 启动过程最大的认识是Linux的启动不是简单地加载内核然后运行init而是一个从极简执行环境逐步建立完整操作系统运行环境的过程。start_kernel()是理解这一过程的重要入口。进入该函数时体系结构相关的早期代码已经完成了一部分准备工作但通用内核中的大量核心机制仍需要建立。start_kernel()逐步初始化内存管理、调度、中断、时钟以及其他核心子系统使内核从只能执行有限启动代码的状态逐渐具备正常管理CPU、内存和任务的能力。随后进入rest_init()启动过程出现了一个关键变化Linux开始建立正常的任务体系。我认为理解 idle 进程时最重要的一点是PID 0 并不是rest_init()中通过普通 fork 过程创建出来的。它来源于内核启动时已经存在的初始任务init_task。早期启动代码本身就在这一初始任务上下文中执行。完成创建其他关键任务等工作以后这条原始执行流最终进入idle 路径因此形成我们通常所说的 0 号进程swapper/idle。PID 1 的来源则不同。rest_init()创建执行kernel_init()的任务使它成为系统中的 1 号进程。kernel_init()并不会立即进入普通用户程序而是继续执行kernel_init_freeable()等函数完成剩余的内核初始化工作。实验中能够实际跟踪到do_basic_setup()等初始化过程。当这些工作完成后kernel_init()最终尝试运行用户空间 init。本实验在run_init_process()断点处清楚地观察到了init_filename/init并且源代码显示它调用do_execve()。这意味着 PID 1 通过 exec机制开始执行 rootfs 中的/init。继续运行后QEMU 中实际出现MenuOS因此从 GDB调用路径和实际运行现象两个方面都验证了从内核空间到用户空间的转换。另外本实验在 MenuOS 中执行quit后观察到了do_exit、do_group_exit等调用以及内核严重错误信息。这使我进一步认识到 PID 1与普通进程不同本实验的 MenuOS/init就处于 PID 1的位置它承担用户空间初始进程的角色。一旦这个初始进程退出系统便无法像普通程序退出那样简单回到另一个父进程继续工作。因此我对 Linux 启动过程最终形成的整体理解是Linux 先依靠启动时已经存在的 PID 0 执行内核初始化start_kernel()建立操作系统的核心基础设施rest_init()创建 PID 1 和 PID2使系统进入正常的多任务阶段PID 0 最终成为 idlePID 2 成为kthreadd而 PID 1 继续完成剩余初始化并最终通过exec执行用户空间/init。至此系统才真正完成从内核自身启动到用户空间开始运行的关键跨越。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。