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状态,很有可能已经死锁,因此触发报警日志打印,输出进程的基本信息,栈回溯以及寄存器保存信息以供内核开发人员定位。

下面详细分析实现方式:


  1. [cpp] view plain copy  在CODE上查看代码片派生到我的代码片 
  2. static int __init hung_task_init(void)   
  3. {   
  4.     atomic_notifier_chain_register(&panic_notifier_list, &panic_block);   
  5.     watchdog_task = kthread_run(watchdog, NULL, "khungtaskd");   
  6.    
  7.     return 0;   
  8. }   
  9. subsys_initcall(hung_task_init);   

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


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

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


  1. [cpp] view plain copy  在CODE上查看代码片派生到我的代码片 
  2. /*  
  3.  * kthread which checks for tasks stuck in D state  
  4.  */   
  5. static int watchdog(void *dummy)   
  6. {   
  7.     set_user_nice(current, 0);   
  8.    
  9.     for ( ; ; ) {   
  10.         unsigned long timeout = sysctl_hung_task_timeout_secs;   
  11.    
  12.         while (schedule_timeout_interruptible(timeout_jiffies(timeout)))   
  13.             timeout = sysctl_hung_task_timeout_secs;   
  14.    
  15.         if (atomic_xchg(&reset_hung_task, 0))   
  16.             continue;   
  17.    
  18.         check_hung_uninterruptible_tasks(timeout);   
  19.     }   
  20.    
  21.     return 0;   
  22. }   

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

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


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

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


  1. [cpp] view plain copy  在CODE上查看代码片派生到我的代码片 
  2. /*  
  3.  * Check whether a TASK_UNINTERRUPTIBLE does not get woken up for  
  4.  * a really long time (120 seconds). If that happens, print out  
  5.  * a warning.  
  6.  */   
  7. static void check_hung_uninterruptible_tasks(unsigned long timeout)   
  8. {   
  9.     int max_count = sysctl_hung_task_check_count;   
  10.     int batch_count = HUNG_TASK_BATCHING;   
  11.     struct task_struct *g, *t;   
  12.    
  13.     /*  
  14.      * If the system crashed already then all bets are off,  
  15.      * do not report extra hung tasks:  
  16.      */   
  17.     if (test_taint(TAINT_DIE) || did_panic)   
  18.         return;   
  19.    
  20.     rcu_read_lock();   
  21.     for_each_process_thread(g, t) {   
  22.         if (!max_count--)   
  23.             goto unlock;   
  24.         if (!--batch_count) {   
  25.             batch_count = HUNG_TASK_BATCHING;   
  26.             if (!rcu_lock_break(g, t))   
  27.                 goto unlock;   
  28.         }   
  29.         /* use "==" to skip the TASK_KILLABLE tasks waiting on NFS */   
  30.         if (t->state == TASK_UNINTERRUPTIBLE)   
  31.             check_hung_task(t, timeout);   
  32.     }   
  33.  unlock:   
  34.     rcu_read_unlock();   
  35. }   

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


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

接下去若尚无触发内核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):


  1. [cpp] view plain copy  在CODE上查看代码片派生到我的代码片 
  2. static void check_hung_task(struct task_struct *t, unsigned long timeout)   
  3. {   
  4.     unsigned long switch_count = t->nvcsw + t->nivcsw;   
  5.    
  6.     /*  
  7.      * Ensure the task is not frozen.  
  8.      * Also, skip vfork and any other user process that freezer should skip.  
  9.      */   
  10.     if (unlikely(t->flags & (PF_FROZEN | PF_FREEZER_SKIP)))   
  11.         return;   
  12.    
  13.     /*  
  14.      * When a freshly created task is scheduled once, changes its state to  
  15.      * TASK_UNINTERRUPTIBLE without having ever been switched out once, it  
  16.      * musn't be checked.  
  17.      */   
  18.     if (unlikely(!switch_count))   
  19.         return;   
  20.    
  21.     if (switch_count != t->last_switch_count) {   
  22.         t->last_switch_count = switch_count;   
  23.         return;   
  24.     }   
  25.    
  26.     trace_sched_process_hang(t);   
  27.    
  28.     if (!sysctl_hung_task_warnings)   
  29.         return;   
  30.    
  31.     if (sysctl_hung_task_warnings > 0)   
  32.         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次,之后不再发出告警。下面来看告警代码:


  1. [cpp] view plain copy  在CODE上查看代码片派生到我的代码片 
  2. /*  
  3.  * Ok, the task did not get scheduled for more than 2 minutes,  
  4.  * complain:  
  5.  */   
  6. pr_err("INFO: task %s:%d blocked for more than %ld seconds.\n",   
  7.     t->comm, t->pid, timeout);   
  8. pr_err("      %s %s %.*s\n",   
  9.     print_tainted(), init_utsname()->release,   
  10.     (int)strcspn(init_utsname()->version, " "),   
  11.     init_utsname()->version);   
  12. pr_err("\"echo 0 > /proc/sys/kernel/hung_task_timeout_secs\""   
  13.     " disables this message.\n");   
  14. sched_show_task(t);   
  15. debug_show_held_locks(t);   
  16.    
  17. touch_nmi_watchdog();   

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


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

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

二、示例演示

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

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


  1. [cpp] view plain copy  在CODE上查看代码片派生到我的代码片 
  2. #include      
  3. #include      
  4. #include      
  5. #include    
  6.    
  7. DEFINE_MUTEX(dlock);   
  8.    
  9. static int __init dlock_init(void)   
  10. {   
  11.     mutex_lock(&dlock);   
  12.     mutex_lock(&dlock);    
  13.        
  14.     return 0;   
  15. }   
  16.    
  17. static void __exit dlock_exit(void)    
  18. {   
  19.     return;   
  20. }   
  21.    
  22. module_init(dlock_init);     
  23. module_exit(dlock_exit);     
  24. MODULE_LICENSE("GPL");     

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


  1. root@apple:~# busybox ps  
  2. PID USER TIME COMMAND  
  3. ...... 
  4.  
  5. 521 root 0:00 insmod dlock.ko  
  6. ...... 

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


  1. root@apple:~# cat /proc/521/status  
  2. Name: insmod  
  3. State: D (disk sleep)  
  4. Tgid: 521  
  5. Ngid: 0  
  6. Pid: 521 

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


  1. [ 360.625466] INFO: task insmod:521 blocked for more than 120 seconds.  
  2. [ 360.631878] Tainted: G O 4.1.15 #5  
  3. [ 360.637042] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.  
  4. [ 360.644986] [] (__schedule) from [] (schedule+0x40/0xa4)  
  5. [ 360.652129] [] (schedule) from [] (schedule_preempt_disabled+0x18/0x1c)  
  6. [ 360.660570] [] (schedule_preempt_disabled) from [] (__mutex_lock_slowpath+0x6c/0xe4)  
  7. [ 360.670142] [] (__mutex_lock_slowpath) from [] (mutex_lock+0x44/0x48)  
  8. [ 360.678432] [] (mutex_lock) from [] (dlock_init+0x20/0x2c [dlock])  
  9. [ 360.686480] [] (dlock_init [dlock]) from [] (do_one_initcall+0x90/0x1e8)  
  10. [ 360.694976] [] (do_one_initcall) from [] (do_init_module+0x6c/0x1c0)  
  11. [ 360.703170] [] (do_init_module) from [] (load_module+0x1690/0x1d34)  
  12. [ 360.711284] [] (load_module) from [] (SyS_init_module+0xdc/0x130)  
  13. [ 360.719239] [] (SyS_init_module) from [] (ret_fast_syscall+0x0/0x54)  
  14. [ 480.725351] INFO: task insmod:521 blocked for more than 120 seconds.  
  15. [ 480.731759] Tainted: G O 4.1.15 #5  
  16. [ 480.736917] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.  
  17. [ 480.744842] [] (__schedule) from [] (schedule+0x40/0xa4)  
  18. [ 480.752029] [] (schedule) from [] (schedule_preempt_disabled+0x18/0x1c)  
  19. [ 480.760479] [] (schedule_preempt_disabled) from [] (__mutex_lock_slowpath+0x6c/0xe4)  
  20. [ 480.770066] [] (__mutex_lock_slowpath) from [] (mutex_lock+0x44/0x48)  
  21. [ 480.778363] [] (mutex_lock) from [] (dlock_init+0x20/0x2c [dlock])  
  22. [ 480.786402] [] (dlock_init [dlock]) from [] (do_one_initcall+0x90/0x1e8)  
  23. [ 480.794897] [] (do_one_initcall) from [] (do_init_module+0x6c/0x1c0)  
  24. [ 480.803085] [] (do_init_module) from [] (load_module+0x1690/0x1d34)  
  25. [ 480.811188] [] (load_module) from [] (SyS_init_module+0xdc/0x130)  
  26. [ 480.819113] [] (SyS_init_module) from [] (ret_fast_syscall+0x0/0x54)  
  27. [ 600.825353] INFO: task insmod:521 blocked for more than 120 seconds.  
  28. [ 600.831759] Tainted: G O 4.1.15 #5  
  29. [ 600.836916] "echo 0 > /proc/sys/kernel/hung_task_timeout_secs" disables this message.  
  30. [ 600.844865] [] (__schedule) from [] (schedule+0x40/0xa4)  
  31. [ 600.852005] [] (schedule) from [] (schedule_preempt_disabled+0x18/0x1c)  
  32. [ 600.860445] [] (schedule_preempt_disabled) from [] (__mutex_lock_slowpath+0x6c/0xe4)  
  33. [ 600.870014] [] (__mutex_lock_slowpath) from [] (mutex_lock+0x44/0x48)  
  34. [ 600.878303] [] (mutex_lock) from [] (dlock_init+0x20/0x2c [dlock])  
  35. [ 600.886339] [] (dlock_init [dlock]) from [] (do_one_initcall+0x90/0x1e8)  
  36. [ 600.894835] [] (do_one_initcall) from [] (do_init_module+0x6c/0x1c0)  
  37. [ 600.903023] [] (do_init_module) from [] (load_module+0x1690/0x1d34)  
  38. [ 600.911133] [] (load_module) from [] (SyS_init_module+0xdc/0x130)  
  39. [ 600.919059] [] (SyS_init_module) from [] (ret_fast_syscall+0x0/0x54) 

三、总结

D状态死锁一般在驱动开发的过程中比较常见,且不太容易定位,内核提供这种hung task机制,开发人员只需要将这些输出的定位信息抓取并保留下来就可以快速的进行定位。

作者:围城

来源:51CTO

时间: 2024-10-22 19:36:03

Linux内核调试技术——进程D状态死锁检测的相关文章

Linux内核调试技术之自构proc

1.简介 在内核中使用printk可以讲调试信息保存在log_buf缓冲区中,可以使用命令 #cat /proc/kmsg 将缓冲区的数区的数数据打印出来,今天我们就来研究一下,自己写kmsg这个文件,我们取名叫做 mymsg. 2.查看内核中 /proc/kmsg怎么写的! 在Proc_misc.c (fs\proc) 文件中: void __init proc_misc_init(void){      .........................          struct pr

深入理解linux内核之(二)进程

                                      深入理解linux内核之(二)进程       程序是静态的,进程是正在执行的程序的一个实例,一个程序可以由多个进程组成.进程是资源分配的实体.在进程被创建出来之后,该子进程几乎和父进程一样.子进程复制了父进程的地址空间,从fork()之后的第一条指令开始执行,和父进程有同样的程序可执行代码(exec调用除外).尽管子进程和父进程具有同样的程序执行代码,但是子进程拥有自己的stack和heap,因此,子进程对数据的修改对

Linux内核剖析 之 进程简介

1.概念    1.1  什么是进程?     进程是程序执行的一个实例,可以看作充分描述程序已经执行到何种程度的数据结构的汇集.     从内核观点看,进程的目的就是担当分配系统资源(CPU时间,内存等)的实体.     我们熟悉的fork()库函数,它有两种用法:     (1).一个父进程希望复制自己,使父子进程执行不同的代码段,常用于网络服务程序.     (2).一个进程要执行一个不同的程序,fork()后立即exec(),如shell. 1.2  什么是线程?     有时候,一个进

浅谈Linux内核创建新进程的全过程_Linux

进程描述 进程描述符(task_struct) 用来描述进程的数据结构,可以理解为进程的属性.比如进程的状态.进程的标识(PID)等,都被封装在了进程描述符这个数据结构中,该数据结构被定义为task_struct 进程控制块(PCB) 是操作系统核心中一种数据结构,主要表示进程状态. 进程状态 fork() fork()在父.子进程各返回一次.在父进程中返回子进程的 pid,在子进程中返回0. fork一个子进程的代码   #include <stdio.h> #include <std

Linux内核剖析 之 进程地址空间(一)

绪论     内核获取内存方式--直接了当:     1. 从分区页框分配器获取内存(__get_free_pages()或alloc_pages()):     2. 使用slab分配器为专用或通用对象分配内存(kmem_cache_alloc()或kmalloc()):     3. 使用vmalloc或vmalloc_32获取一块非连续内存区.     如果申请的内存得以满足,这些函数返回一个页描述符地址或线性地址.     *内核申请内存使用这些简单方法基于以下两个原因:     1.内

linux内核分析之进程地址空间【转】

转自:http://blog.csdn.net/bullbat/article/details/7106094 版权声明:本文为博主原创文章,未经博主允许不得转载. 本文主要介绍linux内核中进程地址空间的数据结构描述,包括mm_struct/vm_area_struct.进程线性地址区间的分配流程,并对相应的源代码做了注释.  内核中的函数以相当直接了当的方式获得动态内存.当给用户态进程分配内存时,情况完全不同了.进程对动态内存的请求被认为是不紧迫的,一般来说,内核总是尽量推迟给用户态进程分

Linux内核剖析 之 进程地址空间(三)

本节主要讲述缺页异常处理程序和堆的管理等内容. 缺页异常处理程序 触发缺页异常程序的两种情况: 1. 由编程错误引起的异常(如访问越界,地址不属于进程地址空间). 2. 地址属于线性地址空间,但内核还未分配相应的物理页,导致缺页异常. 缺页异常处理程序总体方案: 线性区描述符可以让缺页异常处理程序非常有效的完成它的工作. do_page_fault()函数是80x86上的缺页中断服务程序,它把引起缺页的线性地址和当前进程的线性区相比较,从而根据具体方案选择适当的方法处理此异常. 标识符vmall

Linux内核剖析 之 进程地址空间(二)

//接前一章,本节主要介绍线性区以及相关线性区的操作. 线性区 Linux通过类型为vm_area_struct的对象实现线性区. vm_area_struct: struct vm_area_struct { struct mm_struct * vm_mm; /* The address space we belong to. */ unsigned long vm_start; /* Our start address within vm_mm. */ unsigned long vm_e

使用 ftrace 调试 Linux 内核【转】

转自:http://blog.csdn.net/adaptiver/article/details/7930646 使用 ftrace 调试 Linux 内核,第 1 部分 http://blog.csdn.net/tommy_wxie/article/details/7340701 简介: ftrace 是 Linux 内核中提供的一种调试工具.使用 ftrace 可以对内核中发生的事情进行跟踪,这在调试 bug 或者分析内核时非常有用.本系列文章对 ftrace 进行了介绍,分为三部分.本文