前言

在零碎的日常运维中,最令人头疼的莫过于各种应用程序或者操作系统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中,次要实现逻辑:

  1. 每隔一段时间(hung_task_timeout_secs定义的超时工夫),查看零碎中所有过程
  2. 针对处于D状态的过程,记录并查看它的上下文切换次数,如果和上次记录的上下文切换次数雷同,则阐明此过程在超时工夫内始终处于D状态。
  3. 依据配置抉择打印告警并触发零碎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中,次要实现逻辑:

  1. 为每个CPU core创立一个(watchdog/%u)内核过程,它会周期性的更新watchdog_touch_ts变量
  2. 设置一个时钟中断,它会周期性的更新hrtimer_interrupts变量 \
    同时负责检测soft_lockup,通过查看watchdog_touch_ts的值是否被更新,来判断(watchdog/%u)过程是否被执行,从而判断CPU是否被其余过程始终占用
  3. 设置一个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