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() 创建线程,但这样有几个问题:
- 线程的创建参数不统一,代码分散。
- 缺乏统一的生命周期管理和清理机制。
- 线程创建时的调度优先级、CPU 亲和性等难以统一控制。
kthreadd 模式把创建逻辑收拢到一处,内核线程变成了一种“资源”,由专门的线程来管理。后续的 kthread_run()、kthread_stop() 等接口,都是在这套机制之上封装出来的。
3. 从用户态视角重新认识 kthreadd
3.1 进程树里看到的 kthreadd
在用户态,你可以用下面几条命令看看 kthreadd 的“势力范围”:
bash复制ps -ef | grep kthreadd
ps -eo pid,ppid,comm | awk '$2==2 {print}'
pstree -p 2
执行后你会看到一大串带方括号的进程名,比如 [ksoftirqd/0]、[kworker/0:1]、[kswapd0]、[kblockd]、[scsi_eh_0] 等等。方括号的含义是:这些进程没有用户态内存映射,不占用虚拟地址空间,是纯内核线程。
如果你用 pstree -p 2 看,输出会非常长,因为几乎所有内核线程都挂在 kthreadd 下。这也是判断一个进程是不是内核线程的快捷方法——PPID 是 2 且进程名带方括号。
3.2 kthreadd 和普通进程的本质区别
普通进程和内核线程最大的区别在于:
- 普通进程有独立的地址空间,通过用户态和内核态切换来执行系统调用。
- 内核线程没有用户态地址空间,
mm字段为 NULL,只在内核态运行,永远不会切换到用户态。 - 内核线程的调度同样遵循 CFS 调度器规则,但它的优先级和 nice 值对用户不可见。
所以你去 top 里看,根本没法通过 renice 调整内核线程的优先级。如果你想限制某个内核线程对 CPU 的占用,能用的手段极其有限,通常只能通过调整触发频率、修改内核参数来间接影响。
3.3 一个容易被误解的事实:kthreadd 不是“守护进程”
很多人把 kthreadd 理解成守护进程,这其实是错误的。守护进程(Daemon)是用户态的概念,比如 sshd、crond,它们由 init 系统接管,脱离终端,在后台运行。内核线程根本不属于用户态体系,没有终端、没有工作目录、没有打开的文件描述符(除了内核内部使用的),所以拿“守护进程”这个类比去理解 kthreadd 并不准确。
更贴切的类比是:kthreadd 像一个“接线员”,自己不干活,但所有内核线程的“上线”都要经过它。
4. 排查实践中如何区分“真凶”和“背锅侠”
4.1 通过 pidstat 精确追踪内核线程 CPU 使用
前面说了,top 显示 kthreadd 高 CPU 是假象,那怎么找到真正的线程?推荐用 pidstat:
bash复制pidstat -t -p 2 1
-t 表示显示线程级别信息,-p 2 指定 kthreadd 进程。这会把 kthreadd 下面所有线程的 CPU 使用率单独列出来。哪个线程占用高,一眼就能看出来。
我之前遇到过一次典型案例:某台机器 top 里 kthreadd 显示 300% 多 CPU,实际上是 [kworker] 线程在疯狂跑,再往下追,发现是某块 SSD 的固件在持续做内部垃圾回收,导致 nvme 驱动不停地提交 workqueue。这种问题你盯着 kthreadd 看永远找不到根因。
4.2 用 ftrace 追踪内核线程创建路径
如果你在做内核开发,或者遇到了“内核线程被频繁创建和销毁”的奇怪现象,可以用 ftrace 来追踪:
bash复制cd /sys/kernel/debug/tracing
echo kthread_create > set_ftrace_filter
echo function > current_tracer
echo 1 > tracing_on
cat trace_pipe
这样能看到 kthread_create() 的调用栈,搞清楚是哪个内核模块在频繁创建线程。这个场景在企业级服务器上很常见,比如某些驱动存在线程泄漏,每次 IO 错误都会创建新线程但不销毁,最后 kthreadd 下面挂了几千个线程。
4.3 为什么 kthreadd 的 CPU 使用有时会“虚高”
top 在计算 CPU 使用率时,会把进程的所有线程的 CPU 时间累加起来。内核线程虽然名义上挂在 kthreadd 下,但每个线程有自己的 task_struct,调度器是按线程维度记账的。如果某个内核线程出了问题,占用大量 CPU,top 里显示在 kthreadd 头上,是因为:
- 某些版本的 procps 工具在计算进程 CPU 时,对内核线程的处理存在汇总误差。
- 更常见的原因是用户根本没去展开线程视图,只看了进程汇总。
所以排查的第一步永远是:展开线程视图,而不是盯着父进程看。
5. 常见“假凶手”案例分析:kthreadd 高占用的真实面纱
5.1 kworker 高占用的定位思路
kworker 是 kthreadd 下属里最常“惹事”的一类线程。它代表内核工作队列,任何驱动、子系统都可以把任务丢给 workqueue,由 kworker 线程执行。
定位 kworker 高占用的思路:
bash复制# 查看具体是哪个 kworker 线程
pidstat -t -p 2 1
# 查看 kworker 对应的工作队列信息
cat /proc/2/task/*/stack
/proc/PID/task/TID/stack 能打印出这个线程在内核里的调用栈,这是定位内核线程异常最直接的手段之一。如果调用栈里反复出现某个驱动函数,基本就能锁定问题了。
5.2 kswapd 高占用与内存压力
kswapd 也是挂在 kthreadd 下的重要线程,负责内存回收。当系统内存压力大时,kswapd 会被唤醒,尝试回收 page cache、匿名页等。如果 top 里显示 kthreadd 高 CPU,展开后发现是 [kswapd0] 高,说明系统内存不足,或者在频繁进行内存 compaction。
这时候正确的排查方向是:
- 看
free -h确认内存水位。 - 查
/proc/pagetypeinfo了解内存碎片情况。 - 调整
vm.swappiness、vm.min_free_kbytes等参数。
而不是去“优化 kthreadd”,因为 kthreadd 本身什么都没做错。
5.3 ksoftirqd 与网络软中断瓶颈
ksoftirqd 负责处理软中断。当网络包量巨大、硬中断处理不过来时,软中断会被推迟到 ksoftirqd 中执行。在 top 里你可能会看到 kthreadd 下多个 [ksoftirqd/N] 线程占用 CPU。
排查网络软中断瓶颈通常要结合:
cat /proc/softirqs看软中断计数。ethtool -S eth0看网卡队列是否均衡。- 必要时开启 RPS(Receive Packet Steering),把软中断分散到多核。
6. 实操:正确查看和管理内核线程状态的方法
6.1 查看内核线程的完整生命周期
每一个内核线程在创建时,都会通过 kthread_create() 返回一个 task_struct,随后通过 wake_up_process() 唤醒。如果你想观察某个内核线程是否还活着、状态是否正常,可以用:
bash复制cat /proc/2/task/*/stat | awk '{print $1, $2, $3}'
这里面第三列是进程状态:R 运行、S 睡眠、D 不可中断睡眠、Z 僵尸。内核线程如果进入 D 状态卡住,通常意味着它在等待某个 IO 或者内核资源,这时候系统往往会出现“uninterruptible sleep”相关的告警。
6.2 内核线程的 PID 为什么不能 rename
你可能会想:既然 kthreadd 是内核线程的管理者,那我能不能像设置用户态进程的 comm 一样,给内核线程改个名字?答案是可以的,但没必要。内核线程可以通过 kthread_create() 传入一个 name 参数,创建时就会设置好;但已经创建的线程,除非是驱动代码里显式调用 set_task_comm(),否则不会改。手动 prctl 改名字只对用户态进程有效。
6.3 设置 CPU 亲和性限制内核线程
有些场景下,某个内核线程确实占用过高,但你暂时没法停掉它,那可以用 taskset 把它绑到空闲核心上:
bash复制taskset -pc 3 $(pidof kworker/0:1)
这条命令把 kworker/0:1 绑定到 CPU 3。虽然这不能降低总 CPU 消耗,但能让它不干扰关键业务所在的核。我之前在数据库服务器上用过这一招:后台备份触发的大量 kworker 全被绑到非业务核,主库查询延迟直接降了一个量级。
6.4 监控内核线程数量的异常变化
内核线程数量暴增,往往意味着某个驱动或内核模块存在线程泄漏。可以写个简单的循环监控:
bash复制for i in {1..60}; do
echo "$(date +%H:%M:%S) $(ls /proc/2/task | wc -l)"
sleep 5
done
正常情况下,一台服务器的内核线程总数在几十到几百之间。如果持续增长且不回落,说明有问题。这时候再结合 dmesg 和 /proc/slabinfo 排查内存是否被内核栈耗尽。
7. 内核线程调试中那些“坑”和非常规操作
7.1 误杀内核线程的后果
之前公司有个新来的同事排查问题,看到 kthreadd 占用高,直接 kill -9 2,结果整个系统瞬间失去响应。为什么?因为 kill -9 对内核线程也有效,它会触发内核线程退出,但很多内核线程的生命周期绑定着关键子系统,比如存储、网络、调度。一个关键内核线程被杀,系统基本就 hang 了。
所以这里必须强调:内核线程不可通过 kill 清理,唯一的正常退出方式是驱动代码里显式调用 kthread_stop()。 遇到异常内核线程,正确做法是定位它为什么异常,而不是杀。
7.2 内核线程的栈信息怎么看
定位内核线程卡死的原因,最常用的手段是看它的内核栈:
bash复制cat /proc/2/task/<TID>/stack
如果输出是空的,可能是因为权限不足,或者线程被 TASK_RUNNING 调度出去了。此时可以用:
bash复制echo t > /proc/sysrq-trigger
dmesg | tail -n 200
触发一次系统任务转储,把所有任务的内核栈打印到内核日志。这是排查内核线程死锁、卡死的经典手段。
7.3 “D 状态”内核线程与 IO 卡死的联调
我遇到过最棘手的一类问题是:ps 里一堆内核线程处于 D 状态,系统负载飙到几百,但 CPU 使用率并不高。这类问题的根因通常是底层存储(比如 NFS、iSCSI)不可用,内核线程在等待 IO 返回,而我们没法直接“重启”这些内核线程。
处理思路一般是:
- 先确认存储链路是否正常。
- 再通过
echo 1 > /proc/sys/kernel/hung_task_timeout_secs配合内核 hung task 检测机制,让内核主动汇报卡住的线程。 - 最终可能需要重启系统或恢复存储服务才能解开死锁。
8. 从 kthreadd 延伸:理解整个 Linux 线程模型的实践心得
8.1 kthreadd 是理解 Linux “一切皆文件”之外的另一个入口
很多人从用户态学习 Linux,理解的进程模型是 fork/exec、父子进程、进程组、会话期。但 kthreadd 打开了另一扇门:内核线程也是一种 task,它同样有 task_struct,同样参与调度,同样可以被追踪和调试,只是没有用户态地址空间。
一旦理解了这个模型,再看问题会通透很多。比如:
- 为什么
ps -eLf能看到很多[]括起来的线程。 - 为什么
top里线程总数远远大于ps -ef显示的进程数。 - 为什么某些 CPU 使用率无法归因到任何用户态进程。
8.2 学会用“按线程查账”代替“按进程查账”
排查问题最大的误区,是盯着顶层进程名看。比如 kthreadd、systemd、java,这些进程本身往往只是“容器”,真正干活的是它们的线程或子进程。正确姿势是先看线程:
bash复制top -H -p 2
或者:
bash复制ps -eLo pid,tid,pcpu,comm --sort=-pcpu | head -n 30
后者能直接列出整个系统 CPU 占用最高的 30 个线程,不需要一层层往下钻。
8.3 内核线程与系统稳定性的关系
内核线程并不是“永远稳定可靠”的。驱动 bug、固件问题、异常硬件都可能导致内核线程崩溃。而内核线程一旦崩溃,往往不是它自己完蛋,而是整个系统跟着遭殃。所以生产环境里:
- 尽量使用稳定的内核版本。
- 对内核线程数量做基础监控。
- 定期查看 dmesg,及时发现驱动异常。
9. 一个真实案例:把“kthreadd 高占用”从误判到根因排查的全过程
记录一次完整的排查过程,也许比前面所有理论都更有参考价值。
现象:某数据库服务器 top 里 kthreadd CPU 持续 200% 以上,数据库查询变慢。值班同学初步判断是内核出问题,准备重启。
我没让重启,按下面步骤走了一遍:
第一步,pidstat -t -p 2 1 看具体线程。结果发现 [kworker/5:1] 占 150%,[kworker/7:3] 占 80%。
第二步,cat /proc/2/task/<TID>/stack 查看栈,发现都阻塞在 nvme_queue_rq 相关的函数上。
第三步,dmesg -T | tail -n 100,看到大量 NVMe 控制器超时日志。
第四步,检查 RAID 卡和 NVMe 固件版本,发现这批 SSD 固件存在已知的垃圾回收 bug,会周期性发起大量同步操作。联系厂商更新固件后,问题消失。
整个过程里,kthreadd 全程都是“背锅侠”。如果当时一看到它占高就重启,虽然也能暂时恢复,但根因没有解决,后面还会复发。通过“按线程定位、查栈、查日志”的三步走,才能在半小时内锁定真实问题。
这也是我想强调的:遇到 kthreadd 相关异常,不要慌,先展开线程,再查栈,再查日志,大部分问题都能顺藤摸瓜找到根源。
10. 内核线程排查工具清单与个人经验总结
最后整理一张实用的工具清单,方便你遇到问题时快速选用。
| 场景 | 命令/工具 | 用途 |
|---|---|---|
| 查看 kthreadd 子线程 CPU | pidstat -t -p 2 1 |
定位具体高占用线程 |
| 查看全局线程 CPU 排名 | ps -eLo pid,tid,pcpu,comm --sort=-pcpu |
快速找出最繁忙线程 |
| 查看线程调用栈 | cat /proc/<pid>/task/<tid>/stack |
定位内核线程卡住原因 |
| 全局任务转储 | echo t > /proc/sysrq-trigger 配合 dmesg |
查看所有任务内核栈 |
| 线程状态汇总 | cat /proc/2/task/*/stat |
检查 D 状态线程 |
| 追踪内核线程创建 | ftrace 的 kthread_create 过滤器 | 排查线程泄漏 |
| 修改 CPU 亲和性 | taskset -pc <cpu> <tid> |
限制内核线程对业务核的干扰 |
| 监控线程数量 | 脚本循环数 /proc/2/task/ 目录 |
判断是否存在线程泄漏 |
我个人的经验是:内核线程类问题,难度不在“找不到原因”,而在“容易被现象误导”。kthreadd 这个名字看起来像个核心守护神,但它其实只是内核所有 “线程工人” 的调度站。遇到任何跟它有关的异常,先把它底下的线程逐个过一遍,比什么都管用。
如果你在排查中看到 kthreadd 下面某个 [kworker] 或 [kswapd] 线程长期处于 R 状态,但 CPU 使用率又不高,可以考虑是不是内核版本和硬件之间存在兼容性问题。这时候升级内核、更新驱动固件往往比调参数更有效。我在生产环境处理过不少这类“玄学”问题,最后都归结到底层固件或者内核模块的 bug 上,而不是 kthreadd 本身的逻辑出了问题。
