前言
在零碎的日常运维中,最令人头疼的莫过于各种应用程序或者操作系统 hung 住不响应的问题。对于处在用户态的程序相对来说还比拟容易排查,而一旦程序 hung 在内核态或者操作系统自身 hung 住,kill 信号甚至硬件中断都无奈响应,此时咱们能做的就只有重启了。然而重启并不能解决基本问题,更麻烦的是这种状况下咱们简直拿不到任何有用的信息,后续起因的剖析和排查更是寸步难行。好在 Linux 内核早已提供了一系列的机制来帮忙咱们剖析此类问题。上面咱们就来看下如何配置应用这些机制以及它们的实现原理。
不可中断睡眠
■概念
第一种比拟常见的景象是:过程长时间处于 D(不可中断睡眠)状态。依赖于它的过程也会因为期待它而阻塞。那么什么是 D 状态呢?顾名思义:首先它是一种睡眠状态,也就意味着处于此状态的过程不会耗费 CPU。其次睡眠的起因是因为期待某些资源(比方锁或者磁盘 IO),这也是咱们看到十分多 D 状态的过程都处在解决 IO 操作的起因。最初一点就是它不能被中断,这个要区别于“硬件中断”的中断,是指不心愿在其获取到资源或者超时前被终止。因而他不会被信号唤醒,也就不会响应 kill - 9 这类信号。这也是它跟 S(可中断睡眠)状态的区别。
过程进入 D 状态产生在内核代码或者底层驱动代码中,典型的场景是与硬件进行通信。通常状况下咱们能够通过 top 命令察看到过程疾速的的进入退出 D 状态,但当硬件故障或者驱动 bug 呈现时就会导致过程长时间处于 D 状态无奈退出,依赖或期待它的其余过程都被阻塞卡死。
- 处于 D 状态的过程
■机制
内核通常会创立一个 khungtaskd 的守护过程,它会周期性的查看所有过程的状态和上下文切换,进而判断是否有过程长时间处于 D 状态。咱们能够通过配置如下内核参数来管制检测的超时工夫、告警打印,以及是否触发 panic,以帮忙问题的后续剖析。
# 超时工夫 kernel.hung_task_timeout_secs = 120# 告警打印的次数 kernel.hung_task_warnings = 10# 是否触发零碎 panickernel.hung_task_panic = 0# 检测的最大过程数,零碎中过程数超过此值时会疏忽超出的局部 kernel.hung_task_check_count = 4194304
- 过程处于 D 状态的超时打印
■实现
khungtaskd 对应的代码在 hung_task.c 中,次要实现逻辑:
- 每隔一段时间(hung_task_timeout_secs 定义的超时工夫),查看零碎中所有过程
- 针对处于 D 状态的过程,记录并查看它的上下文切换次数,如果和上次记录的上下文切换次数雷同,则阐明此过程在超时工夫内始终处于 D 状态。
- 依据配置抉择打印告警并触发零碎 panic
static int __init hung_task_init(void){... ... watchdog_task = kthread_run(watchdog, NULL, "khungtaskd"); // 创立 khungtaskd 过程 ... ...}
static int watchdog(void *dummy){... ... for ( ; ;) {while (schedule_timeout_interruptible(timeout_jiffies(timeout))) // 休眠一段时间 timeout = sysctl_hung_task_timeout_secs; ... ... check_hung_uninterruptible_tasks(timeout); // 开始查看过程 }}
static void check_hung_task(struct task_struct *t, unsigned long timeout){ unsigned long switch_count = t->nvcsw + t->nivcsw; // 计算上下文切换次数 ... ...
if (switch_count != t->last_switch_count) {// 和上次切换次数进行比拟 t->last_switch_count = switch_count; return;} ... ...
printk(KERN_ERR "INFO: task %s:%d blocked for more than" // 打印告警 "%ld seconds.\n", t->comm, t->pid, timeout); printk(KERN_ERR ""echo 0 > /proc/sys/kernel/hung_task_timeout_secs"" "disables this message.\n");
... ... if (sysctl_hung_task_panic) {trigger_all_cpu_backtrace(); panic("hung_task: blocked tasks"); // 触发零碎 panic }}
软锁和硬锁
■概念
另外一种比拟常见的状况就是:一个过程始终占用 CPU,其余过程始终无奈被调度执行,极其状况下甚至无奈响应中断,此时零碎可能就会齐全 hung 住不响应任何用户操作。这种状况还是产生在内核代码或者驱动代码 bug 中。应用程序不会呈现这个问题,就如同咱们写一个死循序程序不会导致系统 hung 住一样。
要了解这种状况,首先得明确 Linux 是个抢占式内核,过程之间能够互相抢占 CPU,其次 Linux 会为每个 CPU core 设置一个固定周期的时钟中断,这个中断是一个很重要的抢占的机会,时钟中断处理程序会判断上面须要让哪个过程抢到 CPU。一个处在用户态的过程执行一段时间后,时钟中断触发,过程调度算法(例如:CFS)可能就会将 CPU 调配给其余过程,从而不会让这个过程始终占用 CPU。而一个处在内核态的过程则不同,首先它能够屏蔽中断响应,这就间接去除了抢占的机会,其次它也能够显示的敞开抢占,同时如果是个内核过程,他的优先级高于一般过程并且调度策略也不同于 CFS,以上这些状况下如果不被动让出 CPU,其余过程就无奈获取执行,最终就会导致问题呈现。
下面形容的景象在 linux 外面称为:软锁 (soft_lockup) 和硬锁(hard_lockup)
- soft_lockup
CPU 被某个过程长时间占用,其余过程得不到调用。例如:长时间禁用内核抢占
- soft_lockup 告警打印
- hard_lockup
CPU 被某个过程长时间占用,其余过程得不到调用,同时也不响应中断。例如:长时间屏蔽中断响应
- hard_lockup 告警打印
■机制
Linux 内核通过 watchdog 机制来查看零碎中是否呈现 soft_lockup 和 hard_lockup。watchdog 的次要思维是:通过优先级更高的工作来察看优先级较低的工作 (过程 / 中断) 是否被胜利执行调度,因而能够通过中断来察看过程是否被失常调度,而通过 NMI(不可屏蔽中断)来察看中断是否被响应。\
咱们能够通过如下内核参数来配置查看条件和是否触发 panic,以帮忙问题的后续剖析。
# 开启 watchdogkernel.watchdog = 1# hardlockup 超时工夫,softlockup 超时工夫 =2*watchdog_threshkernel.watchdog_thresh = 10# 是否触发 panickernel.hardlockup_panic = 1kernel.softlockup_panic = 1
■实现
watchdog 对应的代码在 watchdog.c 中,次要实现逻辑:
- 为每个 CPU core 创立一个 (watchdog/%u) 内核过程,它会周期性的更新 watchdog_touch_ts 变量
- 设置一个时钟中断,它会周期性的更新 hrtimer_interrupts 变量 \
同时负责检测 soft_lockup,通过查看 watchdog_touch_ts 的值是否被更新,来判断 (watchdog/%u) 过程是否被执行,从而判断 CPU 是否被其余过程始终占用 - 设置一个 NMI(不可屏蔽中断),它会每 watchdog_thresh 秒触发一次 \
同时负责检测 hard_lockup,通过查看 hrtimer_interrupts 的值是否被更新,来判断是否呈现 hard_lockup
对应内核代码
#watchdog.cstatic int watchdog_enable_all_cpus(void){... ... if (!watchdog_running) {err = smpboot_register_percpu_thread(&watchdog_threads); // 创立(watchdog/%u)内核过程 ... ...}
static void __touch_watchdog(void){__this_cpu_write(watchdog_touch_ts, get_timestamp()); // 更新 watchdog_touch_ts}
static void watchdog_enable(unsigned int cpu){... ... hrtimer->function = watchdog_timer_fn; // 设置时钟中断 watchdog_nmi_enable(cpu); // 设置 NMI(非屏蔽中断) hrtimer_start(hrtimer, ns_to_ktime(sample_period), HRTIMER_MODE_REL_PINNED); // 启动时钟中断
}
static enum hrtimer_restart watchdog_timer_fn(struct hrtimer *hrtimer){... ... watchdog_interrupt_count(); // 更新 hrtimer_interrupts
wake_up_process(__this_cpu_read(softlockup_watchdog));
... ... duration = is_softlockup(touch_ts); // 查看 softlockup ... ...}
static bool is_hardlockup(void) // 查看 hardlockup{unsigned long hrint = __this_cpu_read(hrtimer_interrupts);
if (__this_cpu_read(hrtimer_interrupts_saved) == hrint) return true;
__this_cpu_write(hrtimer_interrupts_saved, hrint); return false;}
总结
以上介绍了三种最常见的导致过程或零碎 hung 住的场景和相干的背景及原理。在遇到这些状况的时候咱们能够更疾速的判断呈现问题的根本起因和可能的中央。同时也介绍了 linux 内核提供的一些机制,帮忙咱们查看并收集必要的日志和信息。有了这些信息,咱们就能够通过剖析日志、利用 kdump 等工具来进一步排查问题的最终起因。
上述代码源自 Redhat-7.5,kernel 版本:linux-3.10.0-862.el7