故障分析:看Linux如何判断系统“死机”

发布时间:2021-11-08 | 信息来源: | 发布作者:沃趣科技

在系统的日常运维中,最令人头疼的莫过于各种应用程序或者操作系统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
# 是否触发系统panic
kernel.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.
", t->comm, t->pid, timeout);
    printk(KERN_ERR ""echo 0 > /proc/sys/kernel/hung_task_timeout_secs""
                " disables this message.
");

  ... ...
    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,以帮助问题的后续分析。


# 开启watchdog
kernel.watchdog = 1
# hardlockup超时时间,softlockup超时时间=2*watchdog_thresh
kernel.watchdog_thresh = 10
# 是否触发panic
kernel.hardlockup_panic = 1
kernel.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.c
static 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





沃趣科技,让客户用上更好的数据库技术!