1. 为什么系统一卡,你总能在进程列表里看到 kthreadd
先从一个最常见的场景说起:某台 Linux 服务器白天还好好的,到了晚上业务高峰 CPU 使用率直接拉满,你 SSH 上去敲 top,按 CPU 排序,结果看到 kthreadd 这个名字。如果你对内核不太熟,第一反应大概率是:这是什么进程?能不能 kill 掉?
这里先给一个明确结论:不能 kill,而且绝大多数情况下你看到的“kthreadd 占 CPU”是假象。
kthreadd 是 Linux 内核中所有内核线程的父进程,它的 PID 是 2,仅次于 PID 为 1 的 init/systemd。它不负责具体业务,也不处理你的 IO 和网络,它只做一件事——在系统启动和运行期间,随时孵化出新的内核线程。换句话说,你看到的 kthreadd 高占用,本质上是它下属的某个内核线程在“干活”,只是 top 这类工具把账算到了父进程头上。
这篇文章我想从几个层面把 kthreadd 讲透。先解释它到底是怎么工作的,再讲排查时怎么区分“真问题”和“假凶手”,最后给几个实际操作命令,帮你快速定位内核线程相关的故障。
适合谁来读?如果你是 Linux 运维、后端开发、SRE,或者正在学内核原理、经常被各种进程搞得一头雾水,这篇文章应该能帮你省掉不少弯路。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. kthreadd 在内核启动过程中扮演的角色
2.1 从 start_kernel 到 kthreadd 的诞生
Linux 内核启动的入口是 start_kernel(),在这之后会做一大堆初始化工作,包括内存管理、调度器、中断、定时器等等。但真正和进程相关的内核线程创建流程,要从 rest_init() 这个函数说起。
rest_init() 干了几件关键的事:
- 创建
kernel_init线程,这个线程最终会变成 PID 1 的init进程,负责启动用户态的所有服务。 - 创建
kthreadd线程,这就是我们这篇文章的主角,PID 固定为 2。 - 创建
khugepaged等其它早期内核线程。
这里有个很容易混淆的点:很多人以为 init 是所有进程的祖先,从用户态进程树来看确实如此,但从内核线程的角度看,kthreadd 才是所有内核线程的根。ps -ef 输出里,[kthreadd] 的 PPID 是 0,而它下面挂着 ksoftirqd、kswapd、kworker 等等一大堆方括号进程。
2.2 kthreadd 的核心职责
kthreadd 的内核函数是 kthreadd()。它的工作模式其实是一个典型的“生产者-消费者”模型:
- 内核中任何模块想创建一个内核线程,不是直接调用
kernel_thread()(早期是这样,后来统一收口),而是调用kthread_create()或kthread_create_on_node()。 - 这些接口会把“待创建的线程信息”封装成一个
kthread_create_info结构体,挂到全局链表kthread_create_list上,然后唤醒kthreadd。 kthreadd被唤醒后,从链表上取出一个待创建任务,调用create_kthread()真正完成线程创建。
所以你可以这样理解:kthreadd 是一个内核线程的“孵化器”。任何内核子系统要开新线程,都要经过它。
这里面有一个值得注意的细节:kthreadd 在循环里会执行 set_current_state(TASK_INTERRUPTIBLE),然后检查链表是否为空。如果为空,就调度出去睡眠;如果有任务,就切到 TASK_RUNNING 并处理。这个“先置为可中断睡眠、再检查条件”的模式,是内核编程里非常经典的“等待事件”写法,防止在条件满足后、真正睡眠前丢失唤醒信号。
2.3 为什么不是每个内核线程都直接调用 kernel_thread
早期内核确实是各模块自己调 kernel_thread() 创建线程,但这样有几个问题:
- 线程的创建参数不统一,代码分散。
- 缺乏统一的生命周期管理和清理机
