订阅以接收新文章的通知:

寻找 Linux 内核中任务挂起的原因

2025-02-14

7 分钟阅读时间
这篇博文也有 English繁體中文版本。

根据您的配置,Linux 内核可能在其日志中生成挂起任务警告消息。搜索互联网和内核文档,您可以找到一个简短的解释,即内核进程卡在不可中断状态,并且在意外长的时间内没有被调度到 CPU 上运行。这解释了警告的含义,但没有提供它发生的原因。在这篇博文中,我们将探讨挂起任务警告的工作原理、它为什么会发生、这是 Linux 内核还是应用程序本身的错误,以及是否值得对其进行监控。

INFO:任务 XXX:1495882 已被阻止超过 YYY 秒。

内核日志中的挂起任务消息如下所示:

INFO: task XXX:1495882 blocked for more than YYY seconds.
     Tainted: G          O       6.6.39-cloudflare-2024.7.3 #1
"echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
task:XXX         state:D stack:0     pid:1495882 ppid:1      flags:0x00004002
. . .

Linux 中的进程可以处于不同的状态。一些进程正在运行或准备在 CPU 上运行——它们处于 TASK_RUNNING 状态。另一些进程正在等待某些信号或事件发生,例如网络数据包到达或来自用户的终端输入。它们处于 TASK_INTERRUPTIBLE 状态,并且可以在此状态下停留任意长的时间,直到被信号唤醒。关于这些状态,最重要的一点是它们仍然可以接收信号,并被信号终止。相比之下,处于 TASK_UNINTERRUPTIBLE 状态的进程只等待某些特殊类型的事件将其唤醒,并且不能被信号中断。直到进程退出此状态后,才会发送信号,只有系统重新启动才能清除进程。在上面显示的日志中,它用字母 D 标记。

如果这个唤醒事件没有发生或者发生时有显著延迟,会怎么样?(“显著延迟”可能是几秒或几分钟,具体取决于系统。)那么我们的依赖进程就会以这个状态挂起。如果这个依赖进程持有某个锁并阻止其他进程获取该锁,会怎么样?或者如果我们看到许多进程处于 D 状态,又会怎么样?它可能会告诉我们,一些系统资源不堪重负或无法正常工作。同时,这种状态非常有价值,特别是当我们想保留进程内存时。如果部分数据写入磁盘,而另一部分仍在进程内存中,这种状态就很有用——我们不希望磁盘上的数据不一致。或者,当出现错误时,我们可能想要进程内存的快照。为了保留此行为,但使其更受控制,内核中引入了一种新状态:TASK_KILLABLE——它仍然保护进程,但允许使用致命信号终止进程。

Linux 如何识别挂起的进程

Linux 内核有一个特殊的线程,称为 khungtaskd。它会根据设置定期运行,迭代所有处于 D 状态的进程。如果一个进程处于这种状态超过 YYY 秒,我们将在内核日志中看到一条消息。您可以根据意愿对这个后台程序的一些设置进行更改:

$ sudo sysctl -a --pattern hung
kernel.hung_task_all_cpu_backtrace = 0
kernel.hung_task_check_count = 4194304
kernel.hung_task_check_interval_secs = 0
kernel.hung_task_panic = 0
kernel.hung_task_timeout_secs = 10
kernel.hung_task_warnings = 200

在 Cloudflare,我们将通知阈值 kernel.hung_task_timeout_secs 从默认的 120 秒更改为 10 秒。您可以根据配置以及此延迟对您的重要性来调整系统的值。如果进程在 D 状态下停留的时间超过 hung_task_timeout_secs 秒,则会写入日志条目,并且我们的内部监控系统会根据此日志发出警报。这里的另一个重要设置是 kernel.hung_task_warnings:将发送到日志的消息总数。我们将其限制为 200 条消息,并每 15 分钟重置一次。这让我们不会被同一个问题所困扰,同时不会让我们的监控停止太久。您可以通过将值设置为“-1”来使其无限制。

为了更好地理解挂起任务的根本原因以及系统会受到怎样的影响,我们来看几个更详细的示例。

示例 1:XFS

通常,日志中会显示一个有意义的进程或应用程序名称,但有时您可能会看到这样的内容:

INFO: task kworker/13:0:834409 blocked for more than 11 seconds.
 	Tainted: G      	O   	6.6.39-cloudflare-2024.7.3 #1
"echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
task:kworker/13:0	state:D stack:0 	pid:834409 ppid:2   flags:0x00004000
Workqueue: xfs-sync/dm-6 xfs_log_worker

在这条日志中,kworker 是内核线程。它用作一种推迟机制,即某项工作将安排在未来执行。在 kworker 下,工作是从不同的任务中汇总而来的,这使得很难判断哪个应用程序正在经历延迟。幸运的是,kworker 会伴随着 Workqueue 行出现。Workqueue 是一个链接列表,通常在内核中预定义,这些工作将由 kworker 添加到队列中并按照添加顺序执行。Workqueue 名称 xfs-sync其指向的函数xfs_log_worker,可能提供了很好的线索。在这里,我们可以假设 XFS 承受压力并检查相关指标。它帮助我们发现,由于某些配置更改,我们忘记了前段时间为加快 Linux 磁盘加密速度而引入的 no_read_workqueue/no_write_workqueue 标志。

总结:这种情况下,系统没有发生任何严重的问题,但是挂起任务警告提醒我们,文件系统的速度已经变慢。

示例 2:Coredump

我们来看看下一个挂起任务日志及其解码的堆栈跟踪:

INFO: task test:964 blocked for more than 5 seconds.
      Not tainted 6.6.72-cloudflare-2025.1.7 #1
"echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
task:test            state:D stack:0     pid:964   ppid:916    flags:0x00004000
Call Trace:
<TASK>
__schedule (linux/kernel/sched/core.c:5378 linux/kernel/sched/core.c:6697) 
schedule (linux/arch/x86/include/asm/preempt.h:85 (discriminator 13) linux/kernel/sched/core.c:6772 (discriminator 13)) 
[do_exit (linux/kernel/exit.c:433 (discriminator 4) linux/kernel/exit.c:825 (discriminator 4)) 
? finish_task_switch.isra.0 (linux/arch/x86/include/asm/irqflags.h:42 linux/arch/x86/include/asm/irqflags.h:77 linux/kernel/sched/sched.h:1385 linux/kernel/sched/core.c:5132 linux/kernel/sched/core.c:5250) 
do_group_exit (linux/kernel/exit.c:1005) 
get_signal (linux/kernel/signal.c:2869) 
? srso_return_thunk (linux/arch/x86/lib/retpoline.S:217) 
? hrtimer_try_to_cancel.part.0 (linux/kernel/time/hrtimer.c:1347) 
arch_do_signal_or_restart (linux/arch/x86/kernel/signal.c:310) 
? srso_return_thunk (linux/arch/x86/lib/retpoline.S:217) 
? hrtimer_nanosleep (linux/kernel/time/hrtimer.c:2105) 
exit_to_user_mode_prepare (linux/kernel/entry/common.c:176 linux/kernel/entry/common.c:210) 
syscall_exit_to_user_mode (linux/arch/x86/include/asm/entry-common.h:91 linux/kernel/entry/common.c:141 linux/kernel/entry/common.c:304) 
? srso_return_thunk (linux/arch/x86/lib/retpoline.S:217) 
do_syscall_64 (linux/arch/x86/entry/common.c:88) 
entry_SYSCALL_64_after_hwframe (linux/arch/x86/entry/entry_64.S:121) 
</TASK>

堆栈跟踪显示进程或应用程序 test 被阻止 for more than 5 seconds。我们可以通过名称认出这个用户空间应用程序,但它为什么被阻止了?在查找原因时,检查堆栈跟踪总是有帮助的。这里最值得注意的一行是 do_exit (linux/kernel/exit.c:433 (discriminator 4) linux/kernel/exit.c:825 (discriminator 4))源代码指向 coredump_task_exit 函数。此外,检查进程指标发现,应用程序在日志中出现警告消息时崩溃。当进程基于某些信号集(异常)终止时,Linux 内核可以提供核心转储文件(如果启用)。“当进程终止时,内核会在退出之前为进程内存创建快照,并将其写入文件或通过套接字发送到另一个处理程序”——这样的机制可以是 systemd-coredump 或由您自定义。发生这种情况时,内核会将进程移至 D 状态以保留其内存并提前终止。进程内存使用率越高,获取核心转储文件所需的时间就越长,并且收到挂起任务警告的可能性就越高。

让我们用一个小型 Go 程序来触发它,以验证我们的假设。我们将使用默认的 Linux coredump 处理程序,并将挂起任务阈值降低至 1 秒。

Coredump 设置:

$ sudo sysctl -a --pattern kernel.core
kernel.core_pattern = core
kernel.core_pipe_limit = 16
kernel.core_uses_pid = 1

您可以对 sysctl 进行更改:

$ sudo sysctl -w kernel.core_uses_pid=1

挂起任务设置:

$ sudo sysctl -a --pattern hung
kernel.hung_task_all_cpu_backtrace = 0
kernel.hung_task_check_count = 4194304
kernel.hung_task_check_interval_secs = 0
kernel.hung_task_panic = 0
kernel.hung_task_timeout_secs = 1
kernel.hung_task_warnings = -1

Go 程序:

$ cat main.go
package main

import (
	"os"
	"time"
)

func main() {
	_, err := os.ReadFile("test.file")
	if err != nil {
		panic(err)
	}
	time.Sleep(8 * time.Minute) 
}

该程序将一个 10 GB 的文件读入进程内存。让我们来创建文件:

$ yes this is 10GB file | head -c 10GB > test.file

最后一步是构建 Go 程序,使其崩溃,并观察内核日志:

$ go mod init test
$ go build .
$ GOTRACEBACK=crash ./test
$ (Ctrl+\)

太棒了!我们可以看到挂起任务警告:

$ sudo dmesg -T | tail -n 31
INFO: task test:8734 blocked for more than 22 seconds.
      Not tainted 6.6.72-cloudflare-2025.1.7 #1
      Blocked by coredump.
"echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.
task:test            state:D stack:0     pid:8734  ppid:8406   task_flags:0x400448 flags:0x00004000

顺便问一下,您注意到日志中的 Blocked by coredump. 行了吗?它最近被添加到上游代码中,以提高可见性并消除进程本身的责任。该补丁还添加了 task_flags 信息,因为通过标志 PF_POSTCOREDUMP 检测到 Blocked by coredump,了解所有任务标志对于进一步分析根本原因很有用。

总结:此示例表明,即使所有迹象都表明问题出在应用程序上,但真正的根本原因可能是其他内容——在本例中是 coredump

示例 3:rtnl_mutex

这个问题很难调试。通常,警报仅限于一两个不同的进程,这意味着只有特定的应用程序或子系统遇到问题。在这个示例中,我们看到数十个不相关的任务挂起数分钟,而且随着时间的推移并没有任何改进。日志中没有其他内容,大多数系统指标都没有问题,并且现有流量得到服务,但无法通过 ssh 连接到服务器。新的 Kubernetes 容器创建也停滞不前。初始分析不同任务的堆栈跟踪表明,所有跟踪都仅限于三个函数:

rtnetlink_rcv_msg+0x9/0x3c0
dev_ethtool+0xc6/0x2db0 
bonding_show_bonds+0x20/0xb0

进一步调查显示,所有这些函数都在等待获取 rtnl_lock。看起来某个应用程序获取了 rtnl_mutex 但没有释放它。所有其他进程都处于 D 状态,等待此锁。

RTNL 锁主要由内核网络子系统用于与网络相关的配置,包括写入和读取。RTNL 是一个全局 mutex 锁,但正在进行上游工作来按网络命名空间 (netns) 拆分 RTNL。

从挂起任务报告中,我们可以观察到因等待锁而停滞的“受害者”,但我们如何识别持有此锁时间过长的任务?为了解决这个问题,我们通过 bpftrace 脚本利用 BPF,因为这使我们能够检查正在运行的内核状态。内核的 mutex 实现有一个名为 owner 的结构体成员。它包含一个指针,从拥有 mutex 的进程指向 task_struct,只不过它被编码为 atomic_long_t 类型。这是因为 mutex 实现将一些状态信息存储在此指针的低 3 位(掩码 0x7)中。因此,要读取和取消引用此 task_struct 指针,我们必须首先屏蔽低位 (0x7)。

用于确定谁持有 mutex 的 bpftrace 脚本如下所示:

#!/usr/bin/env bpftrace
interval:s:10 {
  $rtnl_mutex = (struct mutex *) kaddr("rtnl_mutex");
  $owner = (struct task_struct *) ($rtnl_mutex->owner.counter & ~0x07);
  if ($owner != 0) {
    printf("rtnl_mutex->owner = %u %s\n", $owner->pid, $owner->comm);
  }
}

在此脚本中,rtnl_mutex 锁是一个全局锁,其地址可通过 /proc/kallsyms 公开——使用 bpftrace 辅助函数 kaddr(),我们可以从 kallsyms 访问 struct mutex 指针。因此,我们可以定期(通过 interval:s:10)检查是否有进程持有此锁。

输出如下:

rtnl_mutex->owner = 3895365 calico-node

这使我们能够快速识别出 calico-node 是持有 RTNL 锁时间过长的进程。为了快速观察此进程本身停滞的位置,可以通过 /proc/3895365/stack 获取调用堆栈。这向我们表明,根本原因是 Wireguard 配置更改,函数 wg_set_device() 持有 RTNL 锁,而 peer_remove_after_dead() 等待 napi_disable() 调用的时间过长。我们继续通过一个名为 drgn 的工具进行调试,这是一个可编程的调试器,可以通过类似 Python 的交互式 shell 调试正在运行的内核。我们仍然没有发现 Wireguard 问题的根本原因,并向上游寻求帮助,这就是另一个故事了。

总结:挂起任务消息是我们在内核日志中唯一拥有的消息。这些消息的每个堆栈跟踪都是唯一的,但通过仔细分析,我们可以发现相似之处,并继续使用其他工具进行调试。

结语

您的系统可能有不同的挂起任务警告,我们也有许多其他情况未在此处提及。每种情况都是独一无二的,并没有标准的调试方法。但希望这篇博客文章能帮助您更好地理解为什么需要启用这些警告、它们如何工作,以及它们背后的含义。我们也试图为调试过程提供一些导航指导:

  • 分析堆栈跟踪可能是一个不错的调试起点,即使所有消息看起来都不相关,就像我们在示例 3 中看到的那样

  • 请记住,警报可能会产生误导,指向受害者而不是犯罪者,如示例 2 和示例 3 中所示

  • 如果内核没有将应用程序调度到 CPU 上,而是将其置于 D 状态并发出警告——真正的问题可能存在于应用程序代码中

祝您调试顺利,并希望本文内容将对您有所帮助!

我们保护整个企业网络,帮助客户高效构建互联网规模的应用程序,加速任何网站或互联网应用程序抵御 DDoS 攻击,防止黑客入侵,并能协助您实现 Zero Trust 的过程

从任何设备访问 1.1.1.1,以开始使用我们的免费应用程序,帮助您更快、更安全地访问互联网。要进一步了解我们帮助构建更美好互联网的使命,请从这里开始。如果您正在寻找新的职业方向,请查看我们的空缺职位
深入剖析Linux内核监控

在 X 上关注

Cloudflare|@cloudflare

相关帖子