当前位置: 首页 > 科技观察

Linux内核调试技术——进程D状态死锁检测

时间:2023-03-12 07:06:57 科技观察

Linux进程有多种状态,如TASK_RUNNING的运行状态、EXIT_DEAD的停止状态、TASK_INTERRUPTIBLE的接收信号等待状态等(在include/linux中可用)/sched.h)。其中,有一个等待TASK_UNINTERRUPTIBLE的状态,称为D状态。该状态下,进程不接收信号,只能被wake_up唤醒。这种状态有很多情况。例如,互斥锁可以将进程设置为这种状态。有时进程会设置进程在等待某个IO资源就绪时进入该状态(wait_event机制)。一般情况下,进程不会在这个状态停留太久,但是如果IO设备出现故障或者进程死锁,进程可能会长时间停留在这个状态,无法回到TASK_RUNNING状态。因此,内核为了便于发现此类情况,专门设计了一种挂起任务机制,用于检测长期处于D状态的进程,并发出警报。本文分析了内核挂起任务机制的源码,并给出了示例演示。1、挂起任务机制分析内核在很早的版本中就引入了挂起任务机制。本文以较新的Linux4.1.15版本的源码为例进行分析。代码量不多,源码文件为kernel/hung_task.c。首先给出总体流程图和设计思路:图D状态死锁流程图。核心思想是创建一个内核监控进程,循环监控每一个处于D状态的进程(任务),统计它们在两次检测之间的调度次数,如果发现两次监控之间没有任何任务的调度,可以判断进程一直处于D状态,很可能死锁,所以触发告警日志打印,输出过程的基本信息、堆栈回溯和寄存器保存信息供内核开发者定位。下面详细分析实现方法:[cpp]viewplaincopy查看CODE上的代码片导出到我的代码片staticint__inithung_task_init(void){atomic_notifier_chain_register(&panic_notifier_list,&panic_block);watchdog_task=kthread_run(watchdog,NULL,"khungtaskd");return0;}subsys_initcall(hung_task_init);首先,如果在内核配置中启用了这个机制,那么在内核的subsys初始化阶段会调用hung_task_init()函数来启用该功能。首先向内核的panic_notifier_list通知链注册回调:[cpp]viewplaincopyonCODE查看派生到我的代码片的代码片staticstructnotifier_blockpanic_block={.notifier_call=hung_task_panic,};hung_task_panic()函数会在内核触发panic时被调用,这个函数的作用后面会看到。继续初始化,调用kthread_run()函数创建一个名为khungtaskd的线程,执行watchdog()函数,立即尝试调度执行。该线程是后台内核线程,专用于检测D态死锁进程。[cpp]viewplaincopy查看派生到我的代码片的CODE上的代码片/**kthreadwhichchecksfortasksstuckinDstate*/staticintwatchdog(void*dummy){set_user_nice(current,0);for(;;){unsignedlongtimeout=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);}return0;}这个进程先设置优先级为0,为一般优先级,不影响其他进程。然后进入主循环(每超时执行一次),先让进程休眠,设置的休眠时间为CONFIG_DEFAULT_HUNG_TASK_TIMEOUT,可以通过内核配置选项修改,默认值为120s,休眠唤醒后,判断原子变量标识reset_hung_task,如果被设置,则跳过本轮监听,同时清除该标志。flag是通过reset_hung_task_detector()函数设置的(目前内核中没有其他程序使用这个接口):[cpp]viewplaincopy查看CODE上的代码片导出到我的代码片voidreset_hung_task_detector(void){atomic_set(&reset_hung_task,1);}EXPORT_SYMBOL_GPL(reset_hung_task_detector);下一个循环的关键是监控函数check_hung_uninterruptible_tasks(),函数的入参是监控超时时间。[cpp]viewplaincopy在CODE上查看代码片派生到我的代码片/**CheckwhetheraTASK_UNINTERRUPTIBLEdoesnotgetwokenupfor*areallylongtime(120seconds).Ifthathappens,printout*awarning.*/staticvoidcheck_hung_uninterruptible_tasks(unsignedlongtimeout){intmax_count=sysctl_hung_task_check_count;intbatch_count=HUNG_TASK_BATCHING;structtask_struct*g,*t;/**如果系统已经崩溃,那么所有的赌注都关闭,*donotreportextrahungtasks:*/if(test_taint(TAINT_DIE)||did_panic)return;rcu_read_lock();for_each_process_thread(g,t){if(!max_count--)gotounlock;if(!--batch_count){batch_count=HUNG_TASK_BATCHING;if(!rcu_lock_break(g,t))gotounlock;}/*使用"=="跳过等待NFS的TASK_KILLABLE任务*/if(t->state==TASK_UNINTERRUPTIBLE)check_hung_task(t,timeout);}unlock:rcu_read_unlock();}首先检查内核是否挂掉或者panic了,如果是则说明内核挂了,不用监听,直接return即可。注意这里的did_panicflag是在上一篇panic通知链的回调函数中的hung_task_panic()中设置的:[cpp]viewplaincopy查看CODE上的代码片导出到我的代码片staticinthung_task_panic(structnotifier_block*this,unsignedlongevent,void*ptr){did_panic=1;returnNOTIFY_DONE;}接下来,如果还没有触发kernelcrash,则进入监控进程,对kernel中的所有进程(task任务)进行一一检测。在很多情况下,锁定时间过长。这里设置了一个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结构体和timeouttime(120s):[cpp]viewplaincopy查看CODE上的代码片导出到我的代码片staticvoidcheck_hung_task(structtask_struct*t,unsignedlongtimeout){unsignedlongswitch_count=t->nvcsw+t->nivcsw;(t->flags&(PF_FROZEN|PF_FREEZER_SKIP)))return;/**当一个新创建的taskisscheduleonce,changesitsstateto*TASK_UNINTERRUPTIBLEwithouthavingeverbeenswitchedoutonce,it*musn'tbechecked.*/if(unlikely(!switch_count))return!st_switch=switch(){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表示进程主动让出CPU的次数,t->nivcsw表示被强行抢占的次数。然后函数判断几个flags:(1)如果进程被冻结,则跳过检测;(2)不检测调度号是否为0。接下来判断上次检测保存的进程调度号是否与本次相同。如果没有,说明进程在本轮超时时间(120s)内已经被调度,则更新调度值并返回,否则,说明进程已经被调度。没有被调度超时(120s),一直处于D状态。接下来的trace_sched_process_hang()函数不清楚,再判断sysctl_hung_task_warnings标志位,表示需要触发多少次报警。用户也可以通过sysctl命令进行配置。默认值为10,即如果当前检测到的进程一直处于D状态,则默认每2分钟发出一次报警,共10次,之后不再发出报警。我们看报警代码:[cpp]viewplaincopy查看CODE上的代码片Derivedtomycodeslice/**Ok,thetaskdidnotgetscheduledformorethan2minutes,*complain:*/pr_err("INFO:task%s:%dblockedformorethan%ldseconds.\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("\"echo0>/proc/sys/kernel/hung_task_timeout_secs\"""disablesthismessage.\n");sched_show_task(t);debug_show_held_locks(t);touch_nmi_watchdog();在这里,死锁任务名称、PID号、超时时间、内核污染信息、sysinfo、内核堆栈跟踪和注册信息将打印在控制台和日志中。如果开启debug锁,会打印锁占用,touchnmi_watchdog防止nmi_watchdog超时(我的ARM环境,不需要考虑nmi_watchdog)。[cpp]viewplaincopy在CODE上查看代码片并将其派生到我的代码片中直接触发(这个值可以通过内核配置文件配置,也可以通过sysctl设置)。二、示例演示演示环境:RaspberryPib(Linux4.1.15)1.首先确认内核配置选项,确认启用了hungstak机制[cpp]viewplaincopy查看CODE上的代码片导出到我的代码片#include#include#include#includeDEFINE_MUTEX(dlock);staticint__initdlock_init(void){mutex_lock(&dlock);mutex_lock(&dlock);return0;}staticvoid__exitdlock_exit(void){return;}module_init(dlock_init);module_exit(dlock_exit);MODULE_GPLICENSE(";本示例程序定义了互斥锁,然后在模块的init函数中重复加锁,人为造成死锁现象(mutex_lock()函数会调用__mutex_lock_slowpath()将进程设置为TASK_UNINTERRUPTIBLE状态),进程后进入D状态是不可能退出的,可以用ps命令查看:root@apple:~#busyboxpsPIDUSERTIMECOMMAND......521root0:00insmoddlock.ko......然后查看进程状态,可以看出它有e进入了D状态。root@apple:~#cat/proc/521/statusName:insmodState:D(disksleep)Tgid:521Ngid:0Pid:521至此,等待两分钟后,调试串口会输出如下信息,说明已经将每两分钟输出一次:[.625466]INFO:tasksinsmod:521blockedformorethan120seconds.[360.631878]Tainted:GO4.1.15#5[360.637042]"echo0>/proc/sys/kernel/hung_task_timeout_secs"disablesthismessage.[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]信息:taskinsmod:521blockedformorethan120seconds.[480.731759]Tainted:GO4.1.15#5[480.736917]"echo0>/proc/sys/kernel/hung_task_timeout_secs"禁用此消息。[480.744842][](__schedule)from[](schedule+0x4)0[/0xa4480.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+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:taskinsmod:521blockedformorethan120seconds.[600.831759]Tainted:GO4.1.15#5[600.836916]"kernel_time_schoy0>/_secs"disablesthismessage.[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])[339]886[](dlock_init[dlock])来自[](do_one_initcall+0x90/0x1e8)[600.894835][](do_one_initcall)来自[](do_init_module+0x6c/0x1c0)[600.903023][](do_init_module)来自[](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).在开发过程中比较常见,和不好定位,内核提供了这种挂起任务机制,开发者只需要捕获并保存输出的定位信息即可快速定位