news 2026/10/21 22:32:45

Linux内核0号进程:idle进程的诞生与启动全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Linux内核0号进程:idle进程的诞生与启动全解析

讲个冷知识:你从ps aux里永远找不到swapper进程,但整个系统里的每个 CPU 其实都在被它"兜底"运行着。这个进程的 pid 永远是 0,内核源码里习惯叫它 idle、boot idle task,或者进程 0。这篇文章是idle 的启动系列的第一篇,我准备先把 0 号进程是怎么"出生"的、它和start_kernel到rest_init这条启动主线有什么关系、以及我平时在真机上验证这套机制时用的几个土办法,一次性讲透。

这篇内容适合三类人看:想读懂内核启动源码的人、做底层性能排查时被ksoftirqd和swapper搞晕的人,以及正在看《深入理解Linux内核》想找一个具体切入点的人。

1. 为什么要专门分析 idle 进程的启动过程

1.1 一个没有 ppid、没有命令行、永远 R 状态的"幽灵进程"

我刚接触内核调度时有个很深的困惑:运行队列上如果真的没有任务了,调度器应该选谁?如果什么都不选,那 CPU 上的"当前进程"就悬空了,中断回来之后switch_to根本不知道该切给谁。idle 进程就是为这个场景准备的。

它在每个 CPU 上都存在,调度器实在挑不出其他任务时,就选它。pick_next_task的最后一个return一般就是pick_next_task_idle(rq),也就是说 idle 永远处于可运行状态。它的 task 结构里comm通常是swapper/0、swapper/1,没有用户态地址空间,也没有父进程概念,某些发行版的 ps 里看不到它。第一眼确实挺"幽灵"的。

这里有个容易被误导的点:它的 state 往往显示为 R(running),但不代表系统很忙。恰恰相反,说明没有其他任务可运行,CPU 才会一直待在 idle 里。这个区别后面我会在常见问题里展开。

1.2 看懂 idle 的启动,是理解内核启动收尾与调度器的捷径

很多介绍内核启动的文章会顺着start_kernel把setup_arch、mm_init、sched_init都讲一遍,但我更推荐先盯住rest_init之后发生的事情。因为从rest_init开始,系统才正式从"单线程初始化"跳进"多任务调度",而 0 号进程恰好就是这个转变的枢纽。

start_kernel干了很多事,但它自己并不进入用户态。它最后要做的是把 pid 1 和 pid 2 造出来,然后自己变成 idle。你读懂了 rest_init 的三步,基本上就把调度器启动前的逻辑打通了。后续不管是看cpuidle省电、看 CPU 热插拔、还是排查某个核上的软中断为啥一直跑,都绕不开这条线。

2. 从 start_kernel 到 rest_init:0 号进程的诞生路径

2.1 启动主线里哪一步轮到 idle 登场

整个启动过程用一句话概括:固件把内核镜像放进内存,然后跳到内核入口,入口经过一系列汇编和 C 语言的初始化,最终在start_kernel里把核心子系统全部点亮。

start_kernel的位置在init/main.c。它的执行顺序并不是随便排的,比如必须先setup_arch(解析体系结构相关参数和内存布局),再mm_init(建立页表),然后sched_init(让调度器基础设施可用),之后才敢说"我可以开始创建进程了"。最后那一步就是rest_init():

asmlinkage __visible void __init start_kernel(void) { char *command_line; char *after_dashes; setup_arch(&command_line); mm_init(); sched_init(); ... rest_init(); }

也就是说,idle不是启动早期的某个瞬间"生成"的,而是启动主线走到最后,当前正在执行start_kernel的那个线程直接把身份切换成了 idle。整个过程没有新进程,没有 fork,只是"同一个执行上下文"换了角色。

2.2 rest_init 里那三个关键的"造人"动作

rest_init这个函数不长,但信息量非常大。第一次读的时候建议拆成三步理解:

static noinline void __ref rest_init(void) { struct task_struct *tsk; int pid; rcu_scheduler_starting(); pid = user_mode_thread(kernel_init, NULL, CLONE_FS | CLONE_SIGHAND); ... pid = kernel_thread(kthreadd, NULL, NULL, CLONE_FS | CLONE_FILES); ... system_state = SYSTEM_SCHEDULING; complete(&kthreadd_done); schedule_preempt_disabled(); cpu_startup_entry(CPUHP_ONLINE); }

第一步,用user_mode_thread(kernel_init, ...)创建出 pid 1,也就是我们熟悉的 init 进程。注意它带着CLONE_FS | CLONE_SIGHAND,说明会进入用户态执行/sbin/init。第二步,用kernel_thread(kthreadd, ...)创建 pid 2,这是所有内核线程的"接生婆"。第三步有点意思:当前这个执行流没有退出,没有消失,而是调用schedule_preempt_disabled()先主动让出一次 CPU,让刚出生的 init 有机会跑起来,然后走进cpu_startup_entry,正式开始 idle 循环。

我会在后面的文章里拆解kernel_init和kthreadd,但第一篇先记住一个结论:0 号进程不是被"创建"的,它是启动流程走到最后"演变"出来的。

2.3 为什么 pid 0 不是 fork 出来的

这个问题我问过不少人,很多人第一反应是"内核里肯定有个fork_idle之类的函数把它造出来"。对了一半。对于非 0 号 CPU,确实有fork_idle(cpu)这个函数。但对于 boot CPU 上那个最原始的 0 号进程,没有任何 fork 动作。

原因很简单:fork 需要一个父进程、需要 pid 分配器、需要把一个现有任务的上下文复制一份。但在启动最早期,这些机制本来就不存在。0 号进程是直接静态定义在镜像里的。代码里对应着两个标志性的东西:init_task和init_thread_union。

init_task是struct task_struct的静态实例,用INIT_TASK(init_task)宏初始化,里头把state、prio、stack、flags全部预设好。init_thread_union是它的内核栈。启动早期,CPU 在进入start_kernel之前用的就是这块栈,所以与其说 0 号进程被创建,不如说"整个内核启动过程本身就是跑在 0 号进程的上下文里"。

3. 原地变身:0 号进程如何成为各 CPU 的 idle

3.1 init_task 与 init_thread_union:最原始的执行流

翻内核源码时你会看到init/init_task.c里有类似这样的定义:

struct task_struct init_task = INIT_TASK(init_task); EXPORT_SYMBOL(init_task);

INIT_TASK宏会把init_task初始化为:state = 0(可运行)、prio = MAX_PRIO - 20(nice 级别约 -20)、stack指向内嵌的init_thread_union栈。也就是说,它在系统刚开始时就存在,甚至比 pid 1、 pid 2 上的任何代码都要更早出现在内存里。

理解这一点,再看启动早期的汇编入口会有豁然开朗的感觉:从startup_64到start_kernel,那些代码运行在什么栈上?就是init_thread_union。那这个上下文是谁?就是 0 号进程。所以"idle 的启动"实际上从 CPU 上电那一刻就开始了,系统在用它完成所有初始化工作之后,才把它最后定义为 idle。

3.2 do_idle 死循环的内部逻辑

rest_init最后调用cpu_startup_entry(CPUHP_ONLINE),而cpu_startup_entry实现就是一个死循环:

void cpu_startup_entry(enum cpuhp_state state) { current->flags |= PF_NOFREEZE; arch_cpu_idle_prepare(); cpuhp_online_idle(state); while (1) do_idle(); }

真正的核心在kernel/sched/idle.c里的do_idle(),它的逻辑可以理解成"望风":

static void do_idle(void) { int cpu = smp_processor_id(); __current_set_polling(); while (!need_resched()) { rmw_barrier(); if (cpu_is_offline(cpu)) cpuhp_report_idle_dead(); local_irq_disable(); arch_cpu_idle(); } __current_clr_polling(); ... schedule_idle(); }

简单拆一下这个过程:

  • 先看need_resched(),如果有人给当前 CPU 设置了调度请求,就不 idle 了,直接去schedule_idle()。
  • 没调度请求,就把本地中断关掉,然后执行arch_cpu_idle(),在 x86 上最终一般会落到hlt指令或者mwait之类,让 CPU 真正歇着。
  • 等外部中断一来,CPU 从 halt 醒来,重新循环,检查有没有新的调度需求。

这个过程是不是很像一个值班保安:没人来找,就坐下休息;有人按门铃(中断),就醒来看看情况。idle 进程的 R 状态其实是调度器视角的"永远可运行",但物理上 CPU 通过 hlt 把功耗降下来了。这两个视角叠加在一起,就是很多人观察 idle 时觉得矛盾的地方。

3.3 非 0 号 CPU 的 idle 线程创建机制

boot CPU 上的 0 号进程不需要重新造,但其他 CPU 呢?在 SMP 系统里,每个 CPU 都要有一个自己的 idle 线程。这部分由smpboot机制负责。

关键的流程是:系统枚举可能的 CPU 编号,对每个非 boot CPU 调用fork_idle(cpu),用dup_task_struct以 0 号进程为模板复制出一个新线程,然后调用init_idle(task, cpu)把它挂到目标 CPU 的运行队列上,最后设置rq->idle指针指向它。

这里有一个很有意思的点:init_idle不只是简单地把 task 塞进去,它要把这个 idle 线程的cpu字段改成目标 CPU,更新其调度相关属性,还必须保证它是"最后一个被选中的人"。就算配置了isolcpus或者设置了亲和性,idle 依然承担着兜底职责。如果你做实时性调优,会发现隔离核上的空闲进程还是会以某种形式存在,这就是原因之一。

4. 实操:在你的机器上给 idle 进程"验明正身"

4.1 从 /proc 和 ps 观察 0 号进程

我之前在排查一个"内核线程 CPU 高"的问题时,养成了先看 0 号进程状态的习惯。在宿主机上直接访问/proc/0是可以的,只是 ps 命令通常不会显示它。如果想"看一眼本尊",可以用:

cat /proc/0/comm cat /proc/0/stat

如果/proc/0不存在(某些容器或 pid namespace 隔离环境下),说明你看到的是当前命名空间内的进程视角,并不是内核态真的没有 pid 0。这一点在做排查时特别容易踩坑。

想看看每个 CPU 的 idle 线程 comm 是什么,可以用:

for f in /proc/[0-9]*/comm; do c=$(cat "$f" 2>/dev/null) if [[ "$c" == swapper* ]]; then echo "$f -> $c" fi done

能看到的输出通常是swapper/0、swapper/1、swapper/2这样的形式,有的内核版本里也会显示成idle/1,这个差异取决于发行版改没改 task comm。

4.2 用 ftrace 抓取 idle 进程的调度切换

观察 idle 进程最直观的办法,不是看静态状态,而是看调度切换事件。用 ftrace 的sched_switch事件可以看到 CPU 上到底从谁切到了谁:

mount -t tracefs tracefs /sys/kernel/tracing cd /sys/kernel/tracing echo 0 > tracing_on echo > trace echo "sched_switch" > set_event echo 1 > tracing_on sleep 2 echo 0 > tracing_on grep -E "swapper|idle" trace | head -20

只要机器上有任何进程在睡眠和唤醒,你基本一定会看到swapper/0在切换记录中出现。比如:

bash-1234 [000] d..2. 12345.678901: sched_switch: prev_comm=bash prev_pid=1234 prev_state=S ==> next_comm=swapper/0 next_pid=0 next_state=R

看到next_comm=swapper/0和next_pid=0,就说明那个 CPU 上暂时没事可做,0 号进程接管了。这个输出还能用来验证load average高低和 idle 切换频率之间的关系,排查性能问题时比 top 精准得多。

4.3 用启动日志确认 rest_init 之后系统状态

如果你还想确认 rest_init 在真实启动链路中的位置,最简单的方法是在启动参数里加上loglevel=8,然后看 dmesg 中 "Freeing unused kernel image memory" 前后打印的内容。free_initmem相关的日志通常是在rest_init之后的kernel_init阶段触发的,所以它的出现意味着 0 号进程已经成功让位给了 1 号进程。

对内核调试比较熟的朋友,可以在 QEMU 里用 gdb 给rest_init打断点。断点命中后,你会看到$rip停在 rest_init 函数头,再单步到cpu_startup_entry,能看到调用链从start_kernel平滑切到 idle 循环。

5. 常见问题与排查技巧实录

5.1 启动卡在 "Freeing unused kernel memory" 之后

我遇到过几次内核启动卡住的现象,日志停在类似 "Freeing unused kernel image memory" 的位置,后面用户态迟迟不起来。很多人第一反应是 init 程序的问题,但实际上这个位置恰好是 0 号进程把控制权移交给 pid 1 的临界点。如果 0 号进程在schedule_preempt_disabled()之后没有正确把 CPU 让出去,或者 pid 1 的内核态入口kernel_init里在run_init_process之前就失败,日志就会卡在这一带。

排查时我会先做三件事:看 dmesg 最后几条是否出现Kernel panic - not syncing、检查有没有Unable to handle kernel paging request at virtual address、再用earlycon串口输出把更早的日志抓下来。这类问题十有八九不是 idle 本身坏了,而是它的"下线交接"机制没有完成,比如某个驱动在initcall阶段死锁。

5.2 top 看到 swapper/0 占 100%,正常吗

这个问题我解释过很多次,尤其在内核线程排查时容易误报。top 里如果某个 CPU 长时间显示swapper/0占用很高,恰恰说明该 CPU 绝大部分时间没有任务可跑,处于真正的空闲状态。idle 进程的"R 状态 + 高占用"是调度器视角的正常情况,因为它永远可运行,物理上却通过 hlt 指令把功耗压到最低。

真正要警惕的是ksoftirqd/0、rcuos这类内核线程占用高,那才是网络中断、RCU 回调、软中断处理异常的典型信号。判断"CPU 是不是真的空闲",最简单有力的指标是top里的%Cpu(s): id那一栏,以及mpstat -P ALL 1输出的%idle,而不是看某个进程到底叫不叫 swapper。

5.3 0 号进程为什么没有 mm 和命令行

用/proc/0/status看时你会发现它的VmSize之类全是 0,也很少有正常的 cmdline。因为swapper/0是一个纯内核线程,不访问用户空间页表,mm指针为空,active_mm在init_idle时被显式设置成init_mm。它的"命令行"其实就是内核线程的 comm 字段,没有 argv 环境。

理解这一点有助于区分内核线程和用户进程:所有用[]括起来的 ps 输出条目都是内核线程,而swapper/0连[]都不一定显式给你。它不是一个"残缺的进程",而是"不进入用户态的执行流",所以没有文件描述符表、没有信号处理这些用户态概念也很正常。

结尾的一点个人体会

linux 里几百个内核线程里,idle 算是最"闷声干活"的一个。我刚调试调度器时,总觉得它不过是个空转的占位符,后来顺着启动代码读下来,才发现系统把所有初始化做完之后,是把整台机器"托付"给这个 0 号进程的。它不参与业务,却是整个调度体系里最不可能缺席的角色。

另外分享一个我调试时的习惯:与其从start_kernel从头看,不如直接从rest_init往回看,因为这条线能把你瞬间带进进程管理、调度器、SMP 初始化这几个核心交汇点。后面我会在这个系列里继续拆cpu_startup_entry里的 cpuidle 框架、非 boot CPU 的 idle 初始化细节,还有schedule_idle后面的调度路径,每一个都比表面看起来更有意思。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/21 22:32:03

AI客服接入小红书私信:从选型配置到高转化调优全指南

可能你也遇到过这样的情况:小红书笔记发出去,评论区互动不错,私信也进了不少,但就是迟迟没人下单。问了一圈下来,才发现很多用户都在私信里问过价格、库存、尺码这类问题,可你要是晚回半小时,那…

作者头像 李华
网站建设 2026/10/21 22:30:22

浏览器原生网络感知:NetworkInformation API 实战与降级策略

1. 被忽视的浏览器原生能力:NetworkInformation API 到底能做什么 第一次接触 NetworkInformation 这个 API 是在做一个移动端视频播放器的时候。产品经理提了一个需求:用户在地铁里、电梯里这种弱网环境下,视频要自动降码率,别…

作者头像 李华
网站建设 2026/10/21 22:30:18

NetTerm5.4绿色版实战:Windows 11串口与Telnet连接配置及避坑指南

简介:NetTerm5.4绿色版是一款面向网络运维人员、程序员及需要远程登录Unix主机的用户提供的经典终端工具,免安装即可运行,适合本地局域网或拨号入网场景下使用。它内置FTP服务器,支持IP地址分辨与网址监控,并提供从Uni…

作者头像 李华
网站建设 2026/10/21 22:28:07

Spring Boot影楼管理系统实战:从数据库设计到权限与状态流转

这学期又到了毕设季,我前前后后帮人看过的Spring Boot项目中,“影楼管理系统”出现频率相当高。说实话,这个题目非常典型——名字听起来偏垂直行业,但剥离掉“影楼”这层外衣后,它其实就是一个标准的业务管理系统&…

作者头像 李华
网站建设 2026/10/21 22:27:56

SpringBoot+Vue手机商城系统源码详解:前后端分离与订单库存设计

第一次拿到这套“欢迪迈手机商城”源码的时候,我其实没抱太大期望。毕竟SpringBootVue的商城类项目,市面上多到泛滥,很多就是把开源后台改个Logo就拿出来卖。但翻了半小时之后,我承认它的完成度确实比一般毕设要高:用户…

作者头像 李华
网站建设 2026/10/21 22:25:34

知识管理插件化实战:从采集到输出打通效率链路

做知识管理这些年,我最大的感受是:工具不是不够多,而是插件生态让人眼花缭乱。早期我一度迷恋各种“全家桶”式知识软件,装上几十个插件,光配置就折腾了一周,最后发现真正的产出没增加多少,反而…

作者头像 李华