Linux中如何实现进程D状态死锁检测

37次阅读
没有评论

共计 9361 个字符,预计需要花费 24 分钟才能阅读完成。

丸趣 TV 小编给大家分享一下 Linux 中如何实现进程 D 状态死锁检测,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!

Linux 的进程存在多种状态,如 TASK_RUNNING 的运行态、EXIT_DEAD 的停止态和 TASK_INTERRUPTIBLE 的接收信号的等待状态等等 (可在 include/linux/sched.h 中查看)。其中有一种状态等待为 TASK_UNINTERRUPTIBLE,称为 D 状态,该种状态下进程不接收信号,只能通过 wake_up 唤醒。处于这种状态的情况有很多,例如 mutex 锁就可能会设置进程于该状态,有时候进程在等待某种 IO 资源就绪时(wait_event 机制) 会设置进程进入该状态。一般情况下,进程处于该状态的时间不会太久,但若 IO 设备出现故障或者出现进程死锁等情况,进程就可能长期处于该状态而无法再返回到 TASK_RUNNING 态。因此,内核为了便于发现这类情况设计出了 hung  task 机制专门用于检测长期处于 D 状态的进程并发出告警。本文分析内核 hung task 机制的源码并给出一个示例演示。

一、hung task 机制分析

内核在很早的版本中就已经引入了 hung task 机制,本文以较新的 Linux  4.1.15 版本源码为例进行分析,代码量并不多,源代码文件为 kernel/hung_task.c。

首先给出整体流程框图和设计思想:

图 D 状态死锁流程图

其核心思想为创建一个内核监测进程循环监测处于 D 状态的每一个进程(任务),统计它们在两次检测之间的调度次数,如果发现有任务在两次监测之间没有发生任何的调度则可判断该进程一直处于 D 状态,很有可能已经死锁,因此触发报警日志打印,输出进程的基本信息,栈回溯以及寄存器保存信息以供内核开发人员定位。

下面详细分析实现方式:

[cpp] view plain copy  在 CODE 上查看代码片派生到我的代码片  static int __init hung_task_init(void) { atomic_notifier_chain_register( panic_notifier_list,  panic_block); watchdog_task = kthread_run(watchdog, NULL,  khungtaskd  return 0; } subsys_initcall(hung_task_init);

首先,若在内核配置中启用了该机制,在内核的 subsys 初始化阶段就会调用 hung_task_init()函数启用功能,首先向内核的 panic_notifier_list 通知链注册回调:

[cpp] view plain copy  在 CODE 上查看代码片派生到我的代码片  static struct notifier_block panic_block = { .notifier_call = hung_task_panic, };

在内核触发 panic 时就会调用该 hung_task_panic()函数,这个函数的作用稍后再看。继续往下初始化,调用 kthread_run()函数创建了一个名为 khungtaskd 的线程,执行 watchdog()函数,立即尝试调度执行。该线程就是专用于检测 D 状态死锁进程的后台内核线程。

[cpp] view plain copy  在 CODE 上查看代码片派生到我的代码片  /* * kthread which checks for tasks stuck in D state */ static int watchdog(void *dummy) { set_user_nice(current, 0); for ( ; ; ) { unsigned long timeout = sysctl_hung_task_timeout_secs; while (schedule_timeout_interruptible(timeout_jiffies(timeout))) timeout = sysctl_hung_task_timeout_secs; if (atomic_xchg( reset_hung_task, 0)) continue; check_hung_uninterruptible_tasks(timeout); } return 0; }

本进程首先设置优先级为 0,即一般优先级,不影响其他进程。然后进入主循环(每隔 timeout 时间执行一次),首先让进程睡眠,设置的睡眠时间为

CONFIG_DEFAULT_HUNG_TASK_TIMEOUT,可以通过内核配置选项修改,默认值为 120s,睡眠结束被唤醒后判断原子变量标识 reset_hung_task,若被置位则跳过本轮监测,同时会清除该标识。该标识通过 reset_hung_task_detector()函数设置(目前内核中尚无其他程序使用该接口):

[cpp] view plain copy  在 CODE 上查看代码片派生到我的代码片  void reset_hung_task_detector(void) { atomic_set( reset_hung_task, 1); } EXPORT_SYMBOL_GPL(reset_hung_task_detector);

接下来循环的 *** 即为监测函数 check_hung_uninterruptible_tasks(),函数入参为监测超时时间。

[cpp] view plain copy  在 CODE 上查看代码片派生到我的代码片  /* * 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; int batch_count = HUNG_TASK_BATCHING; 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; rcu_read_lock(); for_each_process_thread(g, t) { if (!max_count--) goto unlock; if (!--batch_count) { batch_count = HUNG_TASK_BATCHING; if (!rcu_lock_break(g, t)) goto unlock; } /* use  ==  to skip the TASK_KILLABLE tasks waiting on NFS */ if (t- state == TASK_UNINTERRUPTIBLE) check_hung_task(t, timeout); } unlock: rcu_read_unlock(); }

首先检测内核是否已经 DIE 了或者已经 panic 了,如果是则表明内核已经 crash 了,无需再进行监测了,直接返回即可。注意这里的 did_panic 标识在前文中的 panic 通知链回调函数中 hung_task_panic()置位:

[cpp] view plain copy  在 CODE 上查看代码片派生到我的代码片  static int hung_task_panic(struct notifier_block *this, unsigned long event, void *ptr) { did_panic = 1; return NOTIFY_DONE; }

接下去若尚无触发内核 crash,则进入监测流程并逐一检测内核中的所有进程(任务 task),该过程在 RCU 加锁的状态下进行,因此为了避免在进程较多的情况下加锁时间过长,这里设置了一个 batch_count,一次最多检测 HUNG_TASK_BATCHING 个进程。于此同时用户也可以设定 *** 的检测个数 max_count=sysctl_hung_task_check_count,默认值为 ***PID 个数 PID_MAX_LIMIT(通过 sysctl 命令设置)。

函数调用 for_each_process_thread()函数轮询内核中的所有进程 (任务 task),仅对状态处于 TASK_UNINTERRUPTIBLE 状态的进程进行超时判断,调用 check_hung_task() 函数,入参为 task_struct 结构和超时时间(120s):

[cpp] view plain copy  在 CODE 上查看代码片派生到我的代码片  static void check_hung_task(struct task_struct *t, unsigned long timeout) { 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. */ if (unlikely(!switch_count)) return; if (switch_count != t- last_switch_count) { t- last_switch_count = switch_count; return; } trace_sched_process_hang(t); if (!sysctl_hung_task_warnings) return; if (sysctl_hung_task_warnings   0) sysctl_hung_task_warnings--;

首先通过 t - nvcsw 和 t - nivcsw 的计数累加表示进程从创建开始至今的调度次数总和,其中 t - nvcsw 表示进程主动放弃 CPU 的次数,t- nivcsw 表示被强制抢占的次数。随后函数判断几个标识:(1)如果进程被 frozen 了那就跳过检测;(2)调度次数为 0 的不检测。

接下来判断从上一次检测时保存的进程调度次数和本次是否相同,若不相同则表明这轮 timeout(120s)时间内进程发生了调度,则更新该调度值返回,否则则表明该进程已经有 timeout(120s)时间没有得到调度了,一直处于 D 状态。接下来的 trace_sched_process_hang()暂不清楚作用,然后判断 sysctl_hung_task_warnings 标识,它表示需要触发报警的次数,用户也可以通过 sysctl 命令配置,默认值为 10,即若当前检测的进程一直处于 D 状态,默认情况下此处每 2 分钟发出一次告警,一共发出 10 次,之后不再发出告警。下面来看告警代码:

[cpp] view plain copy  在 CODE 上查看代码片派生到我的代码片  /* * Ok, the task did not get scheduled for more than 2 minutes, * complain: */ pr_err(INFO: task %s:%d blocked for more than %ld seconds.\n , t- comm, t- pid, timeout); 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); debug_show_held_locks(t); touch_nmi_watchdog();

这里会在控制台和日志中打印死锁任务的名称、PID 号、超时时间、内核 tainted 信息、sysinfo、内核栈 barktrace 以及寄存器信息等。如果开启了 debug  lock 则打印锁占用的情况,并 touch  nmi_watchdog 以防止 nmi_watchdog 超时(对于我的 ARM 环境无需考虑 nmi_watchdog)。

[cpp] view plain copy  在 CODE 上查看代码片派生到我的代码片  if (sysctl_hung_task_panic) { trigger_all_cpu_backtrace(); panic(hung_task: blocked tasks  }

*** 如果设置了 sysctl_hung_task_panic 标识则直接触发 panic(该值可通过内核配置文件配置也可以通过 sysctl 设置)。

二、示例演示

演示环境:树莓派 b(Linux 4.1.15)

1、首先确认内核配置选项以确认开启 hung stak 机制

[cpp] view plain copy  在 CODE 上查看代码片派生到我的代码片  #include #include #include #include DEFINE_MUTEX(dlock); static int __init dlock_init(void) { mutex_lock( dlock); mutex_lock(dlock); return 0; } static void __exit dlock_exit(void) { return; } module_init(dlock_init); module_exit(dlock_exit); MODULE_LICENSE(GPL

本示例程序定义了一个 mutex 锁,然后在模块的 init 函数中重复加锁,人为造成死锁现象 (mutex_lock() 函数会调用__mutex_lock_slowpath()将进程设置为 TASK_UNINTERRUPTIBLE 状态),进程进入 D 状态后是无法退出的。可以通过 ps 命令来查看:

root@apple:~# busybox ps PID USER TIME COMMAND ...... 521 root 0:00 insmod dlock.ko ......

然后查看该进程的状态,可见已经进入了 D 状态。

root@apple:~# cat /proc/521/status Name: insmod State: D (disk sleep) Tgid: 521 Ngid: 0 Pid: 521

至此在等待两分钟后调试串口就会输出以下信息,可见每两分钟就会输出一次:

[ 360.625466] INFO: task insmod:521 blocked for more than 120 seconds. [ 360.631878] Tainted: G O 4.1.15 #5 [ 360.637042]  echo 0   /proc/sys/kernel/hung_task_timeout_secs  disables this message. [ 360.644986] [] (__schedule) from [] (schedule+0x40/0xa4) [ 360.652129] [] (schedule) from [] (schedule_preempt_disabled+0x18/0x1c) [ 360.660570] [] (schedule_preempt_disabled) from [] (__mutex_lock_slowpath+0x6c/0xe4) [ 360.670142] [] (__mutex_lock_slowpath) from [] (mutex_lock+0x44/0x48) [ 360.678432] [] (mutex_lock) from [] (dlock_init+0x20/0x2c [dlock]) [ 360.686480] [] (dlock_init [dlock]) from [] (do_one_initcall+0x90/0x1e8) [ 360.694976] [] (do_one_initcall) from [] (do_init_module+0x6c/0x1c0) [ 360.703170] [] (do_init_module) from [] (load_module+0x1690/0x1d34) [ 360.711284] [] (load_module) from [] (SyS_init_module+0xdc/0x130) [ 360.719239] [] (SyS_init_module) from [] (ret_fast_syscall+0x0/0x54) [ 480.725351] INFO: task insmod:521 blocked for more than 120 seconds. [ 480.731759] Tainted: G O 4.1.15 #5 [ 480.736917]  echo 0   /proc/sys/kernel/hung_task_timeout_secs  disables this message. [ 480.744842] [] (__schedule) from [] (schedule+0x40/0xa4) [ 480.752029] [] (schedule) from [] (schedule_preempt_disabled+0x18/0x1c) [ 480.760479] [] (schedule_preempt_disabled) from [] (__mutex_lock_slowpath+0x6c/0xe4) [ 480.770066] [] (__mutex_lock_slowpath) from [] (mutex_lock+0x44/0x48) [ 480.778363] [] (mutex_lock) from [] (dlock_init+0x20/0x2c [dlock]) [ 480.786402] [] (dlock_init [dlock]) from [] (do_one_initcall+0x90/0x1e8) [ 480.794897] [] (do_one_initcall) from [] (do_init_module+0x6c/0x1c0) [ 480.803085] [] (do_init_module) from [] (load_module+0x1690/0x1d34) [ 480.811188] [] (load_module) from [] (SyS_init_module+0xdc/0x130) [ 480.819113] [] (SyS_init_module) from [] (ret_fast_syscall+0x0/0x54) [ 600.825353] INFO: task insmod:521 blocked for more than 120 seconds. [ 600.831759] Tainted: G O 4.1.15 #5 [ 600.836916]  echo 0   /proc/sys/kernel/hung_task_timeout_secs  disables this message. [ 600.844865] [] (__schedule) from [] (schedule+0x40/0xa4) [ 600.852005] [] (schedule) from [] (schedule_preempt_disabled+0x18/0x1c) [ 600.860445] [] (schedule_preempt_disabled) from [] (__mutex_lock_slowpath+0x6c/0xe4) [ 600.870014] [] (__mutex_lock_slowpath) from [] (mutex_lock+0x44/0x48) [ 600.878303] [] (mutex_lock) from [] (dlock_init+0x20/0x2c [dlock]) [ 600.886339] [] (dlock_init [dlock]) from [] (do_one_initcall+0x90/0x1e8) [ 600.894835] [] (do_one_initcall) from [] (do_init_module+0x6c/0x1c0) [ 600.903023] [] (do_init_module) from [] (load_module+0x1690/0x1d34) [ 600.911133] [] (load_module) from [] (SyS_init_module+0xdc/0x130) [ 600.919059] [] (SyS_init_module) from [] (ret_fast_syscall+0x0/0x54)

以上是“Linux 中如何实现进程 D 状态死锁检测”这篇文章的所有内容,感谢各位的阅读!相信大家都有了一定的了解,希望分享的内容对大家有所帮助,如果还想学习更多知识,欢迎关注丸趣 TV 行业资讯频道!

正文完
 
丸趣
版权声明:本站原创文章,由 丸趣 2023-08-16发表,共计9361字。
转载说明:除特殊说明外本站除技术相关以外文章皆由网络搜集发布,转载请注明出处。
评论(没有评论)