hung 之 hung task 检测

目录

1. hung task 的含义

2. 检测 hung task 的机制

3. khungtaskd

3.1 配置

3.1.1 首先是 hung task 超时时间

3.1.2 然后是 check 周期

3.1.3 检查到 hung task 时,是否触发 panic

3.1.4 hung task 的最大警告次数

3.1.5 hung task 警告时是否打印每个 CPU 上正在运行 task 的调用栈

3.2 huang task 检查

3.3 打印 hungtask debug 信息

3.4 enhance


1. hung task 的含义

hung 本意是 "挂起",hung task 本意即 "任务被挂起,得不到执行"。

但是如果我们对 linux kernel 的任务调度机制有所了解的话,应该会知道 "任务被挂起,得不到执行" 是很正常的事情。

而在 linux kernel 中,hung task 是指长时间处于 D 状态(TASK_UNINTERRUPTIBLE,即 uninterruptible sleep)的进程。

2. 检测 hung task 的机制

linux kernel 检测 hung task 的机制是 khungtaskd。

khungtaskd 是 kthreadd 的子进程。

yudi:/ # ps -A|grep khungtaskd
root            80     2          0      0 watchdog            0 S [khungtaskd]

通过 sysctl 或者 /proc/sys/kernel/hung_task_xxx 节点可以读、写 khungtaskd 配置。

3. khungtaskd

3.1 配置

3.1.1 首先是 hung task 超时时间

缺省的超时时间由宏 DEFAULT_HUNG_TASK_TIMEOUT 配置,默认是 120 秒。

// lib/Kconfig.debug

config DEFAULT_HUNG_TASK_TIMEOUT
        int "Default timeout for hung task detection (in seconds)"
        depends on DETECT_HUNG_TASK
        default 120
        help
          This option controls the default timeout (in seconds) used
          to determine when a task has become non-responsive and should
          be considered hung.

          It can be adjusted at runtime via the kernel.hung_task_timeout_secs
          sysctl or by writing a value to
          /proc/sys/kernel/hung_task_timeout_secs.

          A timeout of 0 disables the check.  The default is two minutes.
          Keeping the default should be fine in most cases.

下面是本地 Android 设备的 DEFAULT_HUNG_TASK_TIMEOUT 值,也是默认值 120(单位是秒

yudi:/ # zcat /proc/config.gz |grep T_HUNG_TASK_TIMEOUT
CONFIG_DEFAULT_HUNG_TASK_TIMEOUT=120

表示 hungtask 超时时间的变量是 sysctl_hung_task_timeout_secs。

/*
 * Zero means infinite timeout - no checking done:
 */
unsigned long __read_mostly sysctl_hung_task_timeout_secs = CONFIG_DEFAULT_HUNG_TASK_TIMEOUT;

sysctl_hung_task_timeout_secs 可以通过 sysctl 或者写 /proc/sys/kernel/hung_task_timeout_secs 节点来改变。

本地 Android 设备的 /proc/sys/kernel/hung_task_timeout_secs 值是 0。

yudi:/ # cat /proc/sys/kernel/hung_task_timeout_secs
0

 Android 设备的 /proc/sys/kernel/hung_task_timeout_secs 值为 0,应该是 init.rc 中的默认配置。

// system/core/rootdir/init.rc

97 on init
...
302     write /proc/sys/kernel/panic_on_oops 1
303     write /proc/sys/kernel/hung_task_timeout_secs 0
304     write /proc/cpu/alignment 4

llkd.rc 也会配置 hung_task_timeout_secs 的值,不过是在 khungtask.enable 使能时设置。

本地 Android 设备并未使能 khungtask.enable。

// system/core/llkd/llkd.rc

24 # Configure [khungtaskd]
25 on property:khungtask.enable=true
26     write /proc/sys/kernel/hung_task_timeout_secs ${ro.khungtask.timeout:-720}
27     write /proc/sys/kernel/hung_task_warnings 65535
28     write /proc/sys/kernel/hung_task_check_count 65535
29     write /proc/sys/kernel/hung_task_panic 1
30 
31 on property:khungtask.enable=false
32     write /proc/sys/kernel/hung_task_panic 0
yudi:/ # getprop |grep khungtask.enable
[khungtask.enable]: [false]

3.1.2 然后是 check 周期

表示 check 周期的变量是 sysctl_hung_task_check_interval_secs,缺省值是 0。

sysctl_hung_task_check_interval_secs 可以通过 sysctl 或者写 /proc/sys/kernel/hung_task_check_interval_secs 节点来改变。

本地 Android 设备的 /proc/sys/kernel/hung_task_check_interval_secs 值是也 0

yudi:/ # cat /proc/sys/kernel/hung_task_check_interval_secs
0

3.1.3 检查到 hung task 时,是否触发 panic

变量:sysctl_hung_task_panic

缺省值由宏 CONFIG_BOOTPARAM_HUNG_TASK_PANIC_VALUE 设置。

/*
 * Should we panic (and reboot, if panic_timeout= is set) when a
 * hung task is detected:
 */
unsigned int __read_mostly sysctl_hung_task_panic =
                                CONFIG_BOOTPARAM_HUNG_TASK_PANIC_VALUE;

本地 Android 设备的 CONFIG_BOOTPARAM_HUNG_TASK_PANIC_VALUE 值为 0。

yudi:/ # zcat /proc/config.gz |grep CONFIG_BOOTPARAM_HUNG_TASK_PANIC_VALUE
CONFIG_BOOTPARAM_HUNG_TASK_PANIC_VALUE=0

sysctl_hung_task_panic 可以由 sysctl 或 /proc/sys/kernel/hung_task_panic 读、写。

本地 Android 设备的 /proc/sys/kernel/hung_task_panic 值也是 0。

yudi:/ # cat /proc/sys/kernel/hung_task_panic
0

3.1.4 hung task 的最大警告次数

变量:sysctl_hung_task_warnings

缺省值是 10。

int __read_mostly sysctl_hung_task_warnings = 10;

sysctl_hung_task_warnings 可以由 sysctl 或 /proc/sys/kernel/hung_task_warnings 读、写。

本地 Android 设备的 /proc/sys/kernel/hung_task_warnings 值也是 10。

yudi:/ # cat /proc/sys/kernel/hung_task_warnings
10

每检测到一次 hung task,就会将 sysctl_hung_task_warnings 值减一,sysctl_hung_task_warnings 为 0 后不再警告。

sysctl_hung_task_warnings 和 sysctl_hung_task_panic 可以同时开启。

3.1.5 hung task 警告时是否打印每个 CPU 上正在运行 task 的调用栈

变量:sysctl_hung_task_all_cpu_backtrace

缺省值是 0.

#ifdef CONFIG_SMP
/*
 * Should we dump all CPUs backtraces in a hung task event?
 * Defaults to 0, can be changed via sysctl.
 */
unsigned int __read_mostly sysctl_hung_task_all_cpu_backtrace;
#endif /* CONFIG_SMP */

sysctl_hung_task_all_cpu_backtrace 可以由 sysctl 或 /proc/sys/kernel/hung_task_all_cpu_backtrace 读、写。

本地 Android 设备的 /proc/sys/kernel/hung_task_all_cpu_backtrace 值也是 0。

yudi:/ # cat /proc/sys/kernel/hung_task_all_cpu_backtrace
0

注意,sysctl_hung_task_all_cpu_backtrace 要跟 sysctl_hung_task_warnings 结合使用,即只有在 hung task 警告使能时,sysctl_hung_task_all_cpu_backtrace 才有效。

3.2 huang task 检查

huang task 检查的核心是 check_hung_task 方法。

// kernel/hung_task.c

static void check_hung_task(struct task_struct *t, unsigned long timeout)
{
        // 目标 task 状态切换次数
        unsigned long switch_count = t->nvcsw + t->nivcsw;

        /*
         * Ensure the task is not frozen.
         * Also, skip vfork and any other user process that freezer should skip.
         */
        // 忽略被冻结的进程
        if (unlikely(t->flags & (PF_FROZEN | PF_FREEZER_SKIP)))
            return;

        /*
         * When a freshly created task is scheduled once, changes its state to
         * TASK_UNINTERRUPTIBLE without having ever been switched out once, it
         * musn't be checked.
         */
         // 如果目标 task 状态切换次数为 0,也忽略(说明该 task 应该是刚创建的)
        if (unlikely(!switch_count))
                return;

        // 如果目标 task 状态切换次数相比上一次 check 时发生了变化,也忽略(说明 task 唤醒过了,不是一直是 D 状态)
        if (switch_count != t->last_switch_count) {
                t->last_switch_count = switch_count;
                t->last_switch_time = jiffies;
                return;
        }
        // time_is_after_jiffies 检查目标时间是否在当前时间之后,
        // 这里即表示检查当前是否还没到 hungtask 超时时间,如果没到超时的话,也忽略
        if (time_is_after_jiffies(t->last_switch_time + timeout * HZ))
                return;

        // 到这里就表示目标 task 已经被确定为 hungtask 了!
        trace_sched_process_hang(t);

        // 如果 sysctl_hung_task_panic 使能了,则将变量 hung_task_show_lock 和 hung_task_call_panic 置为 true。
        // 1. 设置变量 hung_task_show_lock 为 true,会在后面(check_hung_task 方法的调用函数中)控制打印所有 task 的持锁信息
        // 2. 设置变量 hung_task_call_panic 为 true,会在后面(check_hung_task 方法的调用函数中)控制触发 panic
        if (sysctl_hung_task_panic) {
                console_verbose();   // 设置向控制台输出日志的等级
                hung_task_show_lock = true;
                hung_task_call_panic = true;
        }

        /*
         * Ok, the task did not get scheduled for more than 2 minutes,
         * complain:
         */
        // sysctl_hung_task_warnings 表示 hungtask 警告次数!
        // 如果 sysctl_hung_task_warnings 使能了(数值 > 0),则
        // 1. 将 sysctl_hung_task_warnings 减 1
        // 2. 打印 error log,记录 hungtask 的 task 名、task pid,以及持续了多长时间的 block 状态
        // 3. 打印 error log,提示用户可以通过 echo 0 > /proc/sys/kernel/hung_task_timeout_secs 关闭 hungtask 检查
        // 4. 设置变量 hung_task_show_lock 值为 true
        // 5. 调用 sched_show_task 方法打印 hungtask 任务的状态、内核态调用栈、调度信息(调度策略等)
        // 6. 如果 sysctl_hung_task_all_cpu_backtrace 使能,则设置变量 hung_task_show_all_bt 值为 true,这个变量会在后面(check_hung_task 方法的调用函数中)控制打印所有 cpu 上正在执行的任务的调用栈
        if (sysctl_hung_task_warnings) {
                if (sysctl_hung_task_warnings > 0)
                        sysctl_hung_task_warnings--;
                pr_err("INFO: task %s:%d blocked for more than %ld seconds.\n",
                       t->comm, t->pid, (jiffies - t->last_switch_time) / HZ);
                pr_err("      %s %s %.*s\n",
                        print_tainted(), init_utsname()->release,
                        (int)strcspn(init_utsname()->version, " "),
                        init_utsname()->version);
                pr_err("\"echo 0 > /proc/sys/kernel/hung_task_timeout_secs\""
                        " disables this message.\n");
                sched_show_task(t);
                hung_task_show_lock = true;

                if (sysctl_hung_task_all_cpu_backtrace)
                        hung_task_show_all_bt = true;
        }

        touch_nmi_watchdog();
}

下面是一些需要注意的点。

1. time_is_after_jiffies 方法的作用是检查目标时间是否在当前时间之后。

2. sched_show_task 方法的作用是打印目标 task 的状态、内核态调用栈、调度信息(调度策略等)

3. console_verbose 方法设置向控制台输出日志的等级

4. 变量 hung_task_show_lock 控制打印所有 task 的持锁信息;

变量 hung_task_call_panic 控制触发 panic;

变量 hung_task_show_all_bt 控制打印所有 cpu 上正在执行的任务的调用栈。

这些信息都是在 check_hung_task 的调用者 check_hung_uninterruptible_tasks 函数中打印的。

sysctl_hung_task_panic 使能时,会使能 hung_task_show_lock 和 hung_task_call_panic;

sysctl_hung_task_warnings 使能时,会使能 hung_task_show_lock,可能会使能 hung_task_show_all_bt。

5. 为什么每次 check_hung_task 最后都要执行一次 touch_nmi_watchdog ?

check_hung_task 对一个目标 task 进行 hungtask 检查,也算是耗时动作;遍历所有 task 调用 check_hung_task 的话耗时很长,所以每次检查完一个 task 就调用一次 touch_nmi_watchdog 忽略 hardlockup。

类似的还有 lockdep 模块的 debug_show_all_locks 方法,它在遍历所有 task 打印持锁信息时,每次遍历都会执行 touch_nmi_watchdog 忽略 hardlockup 检查,以及执行 touch_all_softlockup_watchdogs 忽略 softlockup 检查。

3.3 打印 hungtask debug 信息

// kernel/hung_task.c

/*
 * Check whether a TASK_UNINTERRUPTIBLE does not get woken up for
 * a really long time (120 seconds). If that happens, print out
 * a warning.
 */
static void check_hung_uninterruptible_tasks(unsigned long timeout)
{
        int max_count = sysctl_hung_task_check_count;
        unsigned long last_break = jiffies;
        struct task_struct *g, *t;

        /*
         * If the system crashed already then all bets are off,
         * do not report extra hung tasks:
         */
        if (test_taint(TAINT_DIE) || did_panic)
                return;

        hung_task_show_lock = false;
        rcu_read_lock();
        for_each_process_thread(g, t) {
                if (!max_count--)
                        goto unlock;
                if (time_after(jiffies, last_break + HUNG_TASK_LOCK_BREAK)) {
                        if (!rcu_lock_break(g, t))
                                goto unlock;
                        last_break = jiffies;
                }
                /* use "==" to skip the TASK_KILLABLE tasks waiting on NFS */
                if (t->state == TASK_UNINTERRUPTIBLE)
                        check_hung_task(t, timeout);
        }
 unlock:
        rcu_read_unlock();
        // 打印所有 task 的持锁信息
        if (hung_task_show_lock)
                debug_show_all_locks();

        // 打印各 CPU 上正在运行 task 的调用栈
        if (hung_task_show_all_bt) {
                hung_task_show_all_bt = false;
                trigger_all_cpu_backtrace();
        }

        // 触发 panic
        if (hung_task_call_panic)
                panic("hung_task: blocked tasks");
}

需要注意,lockdep 模块的 debug_show_all_locks 方法会打印所有 task 的持锁信息。

3.4 enhance

大米对  hung task 检查做了 enhance,有机会再专门讲。

yudi:/ # ls -l /proc/sys/hung_task_enh/
total 0
-rw-r--r-- 1 root root 0 2024-07-10 11:10 global_detect_mode
-rw-r--r-- 1 root root 0 2024-07-10 11:10 max_iowait_task_cnt
-rw-r--r-- 1 root root 0 2024-07-10 11:10 max_iowait_timeout_cnt
-rw-r--r-- 1 root root 0 2024-07-10 11:10 per_task_detect_mode
-rw-r--r-- 1 root root 0 2024-07-10 11:10 read_pid

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值