问题描述
实际产品运行过程中,Linux系统僵死,屏幕无任何有效串口打印信息,网络中断、键盘鼠标没有任何响应。
这种故障现象,可能是因为Linux内核死锁导致。由于无任何有效打印信息,内核日志中也没有记录,就无法定位故障根因。
如何让Linux内核在僵死前打印相关信息,对问题定位尤为关键。其中一个有效手段是打开“Kernel Hacking”选项,然后重新编译内核。对于Linux(3.14.28)内核死锁有帮助的几个配置选项有:
Kernel hacking --->Debug Lockups and Hangs --->
[*] Detect Hard and Soft Lockups
[*] Panic (Reboot) On Soft Lockups
[*] Detect Hung Tasks
(120) Default timeout for hung task detection (in seconds)
[*] Panic (Reboot) On Hung Tasks
soft lockup 和 hard lockup?
soft lockup:抢占被长时间关闭而导致进程无法调度.
hard lockup : 中断被长时间关闭而导致更严重的问题.
后面会分析这种lockup的内核检测原理.
lockdep 死锁检测模块
介绍了最简单的 ABBA 死锁的形成,回到正题,回到 kernel, 里面有千千万万锁,错综复杂,也不可能要求所有开发人员熟悉 spin_lock, spin_lock_irq, spin_lock_irqsave, spin_lock_nested 的区别。所以,在锁死发生前,还是要做好预防胜于治疗,防患于未然的工作,尽量提前发现并且提前在开发阶段发现和解决这其中潜在的死锁风险,而不是等到最后真正出现死锁时给用户带来糟糕的体验。应运而生的就是 lockdep 死锁检测模块,在 2006 年已经引入内核(https://lwn.net/Articles/185666/)。
相关内核配置选项
CONFIG_DEBUG_RT_MUTEXES=y
检测rt mutex的死锁,并自动报告死锁现场信息。
CONFIG_DEBUG_SPINLOCK=y
检测spinlock的未初始化使用等问题。配合NMI watchdog使用,能发现spinlock死锁。
CONFIG_DEBUG_MUTEXES=y
检测并报告mutex错误
CONFIG_DEBUG_WW_MUTEX_SLOWPATH=y
检测wait/wound类型mutex的slowpath测试。
CONFIG_DEBUG_LOCK_ALLOC=y
检测使用中的锁(spinlock/rwlock/mutex/rwsem)被释放,或者使用中的锁被重新初始化,或者在进程退出时持有锁。
CONFIG_PROVE_LOCKING=y
使内核能在死锁发生前报告死锁详细信息。参见/proc/lockdep_chains。
CONFIG_LOCKDEP=y
整个Lockdep的总开关。参见/proc/lockdep、/proc/lockdep_stats。
CONFIG_LOCK_STAT=y
记锁持有竞争区域的信息,包括等待时间、持有时间等等信息。参见/proc/lock_stat。
CONFIG_DEBUG_LOCKDEP=y
会对Lockdep的使用过程中进行更多的自我检测,会增加很多额外开销。
CONFIG_DEBUG_ATOMIC_SLEEP=y
在atomic section中睡眠可能造成很多不可预测的问题,这些atomic section包括spinlock持锁、rcu读操作、禁止内核抢占部分、中断处理中等等。
死锁问题分析
死锁就是多个进程(线程)因为等待别的进程已占有的自己所需要的资源而陷入阻塞的一种状态,
死锁状态一旦形成,进程本身是解决不了的,需要外在的推动,才能解决,最重要的是死锁不仅仅影响进程业务,而且还会占用系统资源,影响其他进程。
所以内核中设计了内核死锁检测机制,一旦发现死锁进程,就重启OS,快刀斩乱麻解决问题。
之所以使用重启招数,还是在于分布式系统中可以容忍单点崩溃,不能容忍单点进程计算异常,否则进行死锁检测重启OS就得不偿失了。
什么是死锁
互斥锁是保护临界资源被线程间(或进程间)互斥的访问临界资源,当一个线程得到锁不释放时另一个线程申请时必须等待。
当多个线程因为竞争资源而造成的一种僵局(互相等待),如果不施以援手,这些进程将永远等待。
简单的说:就是俩个或多个进程无止境的等候,永远不会成立的条件的一种系统状态。
死锁产生的原因
① 系统资源不足:系统中所拥有的资源其数量不足以满足线程运行的需要,使得在运行过程中,因争夺资源而陷入僵局。
② 线程间推进顺序非法:线程间在运行过程中,申请和释放的顺序不合法。
③ 资源分配不当。
死锁产生的四个条件
互斥条件:在一段时间内,某资源只能被一个线程所占用,若此时其它线程申请则必须等待,直到当前线程用完释放。
不可剥夺条件:线程在已获得的资源未使用完之前不能被抢占,只能自己使用完后自己释放
请求和保持条件:线程已拥有一个资源,此时它又申请新的资源,若新资源被其它线程占用,当前线程则会阻塞等待,但其会保持不放自己获得的资源
循环等待条件:在发生死锁时,必然存在一个循环链,即线程集合{T0,T1,T2,…,Tn}中T0正在等待T1占用的资源,T1正在等待T2占用的资源,………,Tn正在等待Tn占用的资源
常见错误
AA: 重复上锁
ABBA: 曾经使用 AB 顺序上锁,又使用 BA 上锁
ABBCCA: 这种类型是 ABBA 的扩展。AB 顺序 , AB 顺序,CA 顺序。这种锁人工很难发现。
多次 unlock
死锁检测与恢复
对于资源
抢占资源恢复
回滚到安全状态恢复
对于进程
杀死进程恢复
参考文章:
https://blog.csdn.net/ccwzhu/article/details/81171092
一、D状态死锁检测
所谓D状态死锁:进程长时间(系统默认配置120秒)处于TASK_UNINTERRUPTIBLE 睡眠状态,这种状态下进程不响应异步信号。如:进程与外设硬件的交互(如read),通常使用这种状态来保证进程与设备的交互过程不被打断,否则设备可能处于不可控的状态。
内核D状态死锁检测就是hung_task机制,主要代码就在kernel/hung_task.c文件。
具体实现原理:
1.创建Normal级别的khungtaskd内核线程,在死循环中每隔sysctl_hung_task_timeout_secs时间后check一下,用schedule_timeout定时(节约定时器浪费的CPU)。
2.调用do_each_thread,while_each_thread宏遍历所有的进程信息,如果有D状态进程,则检查最近切换次数和task计算是否一致,即最近是否有调度切换,如果一致,则没有切换,打印相关信息,并根据sysctl_hung_task_panic开关决定是否重启。
对应用户态控制的proc接口有:
/proc/sys/kernel/hung_task_timeout_secs,hung_task_panic等。
二、R状态死锁检测
所谓R状态死锁:进程长时间(系统默认配置60秒)处于TASK_RUNNING 状态垄断cpu而不发生切换,一般情况下是进程关抢占后长时候干活,有时候可能进程关抢占后处于死循环或者睡眠后,这样就造成系统异常。
补充:lockdep不是所谓的死锁。
内核R状态死锁检测机制就是lockdep机制,入口即是lockup_detector_init函数。
1.通过cpu_callback函数调用watchdog_enable,在每个CPU core上创建SCHED_FIFO级别的实时线程watchdog,其中使用了hrtimer定时器,控制检查周期。
2.hrtimer定时器调用watchdog_timer_fn进行清狗的时间检查,而线程则每次重置清狗时间,如果watchdog_timer_fn发现狗的重置时间已经和当前时间差出危险值,则根据开关进行panic处理。
对应用户态控制的proc接口有:
/proc/sys/kernel/watchdog_thresh,softlockup_panic等。
查看锁的相关信息
/proc/sys/kernel/lock_stat /* 置位则可以查看/proc/lock_stat统计信息,清除则关闭lockdep统计信息。 */
/proc/lock_stat /* 关于锁的使用统计信息 */
/proc/lockdep /* 存在依赖关系的锁 */
/proc/lockdep_stats /* 存在依赖关系锁的统计信息 */
/proc/lockdep_chains /* 依赖关系锁链表 */
/proc/locks /* */
/proc/sys/kernel/prove_locking
/proc/sys/kernel/max_lock_depth
/sys/kernel/debug/tracing/events/lock /* 内核提供了Tracepoint协助发现锁的使用问题 */
写一个简单的死锁demo
#include <linux/module.h>
#include <linux/kernel.h>
static spinlock_t hack_spinA;
static spinlock_t hack_spinB;
void hack_spinAB(void)
{
printk("hack_lockdep:A->B\n");
spin_lock(&hack_spinA);
spin_lock(&hack_spinB);
}
void hack_spinBA(void)
{
printk("hack_lockdep:B->A\n");
spin_lock(&hack_spinB);
spin_lock(&hack_spinA);
}
static int __init lockdep_test_init(void)
{
printk("al: lockdep error test init\n");
spin_lock_init(&hack_spinA);
spin_lock_init(&hack_spinB);
hack_spinAB();
hack_spinBA();
return 0;
}
module_init(lockdep_test_init);
对应的Makefile
obj-m:=spin_lock_deadlock.o
#声明当前的架构,这里用户需要根据实际情况选择架构类型
export ARCH=arm
#声明交叉编译工具链,这里用户需要根据实际情况选择对应的工具链
export CROSS_COMPILE=arm-himix200-linux-
#源码目录变量,这里用户需要根据实际情况选择路径
#下面作者是将Linux的源码目录
KERDIR := /home1/zhugeyifan/source/K5/3519av100/packages/linux_lsp/kernel/linux-4.9.37/
#当前目录变量
CURDIR := $(shell pwd)
#make命名默认寻找第一个目标
#make -C就是指调用执行的路径
#$(KERDIR)Linux源码目录,作者这里指的是.../linux-4.9.37/
#$(CURDIR)当前目录变量
#modules要执行的操作
#注意!假如复制到Makefile后,下行提示红色,把 all:下面一行的make前面的空格删除后添加(Tab)制表符
all:
make -C $(KERDIR) M=$(CURDIR) modules
#make clean执行的操作是删除后缀为o的文件
#注意!假如复制到Makefile后,下行提示红色,把 clean:下面一行的make前面的空格删除后添加(Tab)制表符
clean:
make -C $(KERDIR) M=$(CURDIR) clean
假如报错,建议看这篇文章:操作系统 - Linux编译一个简单的ko模块
生成
生成对应ko模块:
spin_lock_deadlock.ko
执行
测试上面的死锁检测模块是否生效
执行,报错,代表死锁检测模块生效了:
watchdog: BUG: soft lockup - CPU#0 stuck for 22s! [insmod:306]
Modules linked in: spin_lock_deadlock(PO+) hi_user(O) hi_mipi_rx(O) hi3519av100_acodec(PO) hi3519av100_adec(PO) hi3519av100_aenc(PO) hi3519av100_ao(PO) hi3519av100_ai(PO) hi3519av100_aio(PO) hi3519av100_hdmi(PO) hi_sensor_spi(O) hi_sensor_i2c(O) hi_piris(O) hi_pwm(O) hi3519av100_dpu_match(PO) hi3519av100_dpu_rect(PO) hi3519av100_dsp(PO) hi3519av100_nnie(PO) hi_ipcm(O) hi3519av100_ive(PO) hi3519av100_vdec(PO) hi3519av100_vfmw(PO) hi3519av100_jpegd(PO) hi3519av100_jpege(PO) hi3519av100_h265e(PO) hi3519av100_h264e(PO) hi3519av100_vedu(PO) hi3519av100_chnl(PO) hi3519av100_venc(PO) hi3519av100_rc(PO) hifb(O) hi3519av100_vo(PO) hi3519av100_avs(PO) hi3519av100_vpss(PO) hi3519av100_vi(PO) hi3519av100_isp(PO) hi3519av100_dis(PO) hi3519av100_vgs(PO) hi3519av100_gdc(PO) hi3519av100_rgn(PO) hi3519av100_tde(PO) hi3519av100_sys(PO) hi3519av100_base(PO) hi_osal(O) sys_config(O)
CPU: 0 PID: 306 Comm: insmod Tainted: P O 4.9.37 #402
Hardware name: Generic DT based system
task: d47ace40 task.stack: d470c000
PC is at _raw_spin_lock+0x2c/0x40
LR is at hack_spinBA+0x20/0x2c [spin_lock_deadlock]
pc : [<c06a4a04>] lr : [<bf00204c>] psr: 80000013
sp : d470ddf8 ip : 00000000 fp : 00000024
r10: 00000000 r9 : bf002100 r8 : 00000000
r7 : d47a3000 r6 : d47a3ec0 r5 : ffffe000 r4 : bf002300
r3 : 00000000 r2 : 00000001 r1 : 00000000 r0 : bf002304
Flags: Nzcv IRQs on FIQs on Mode SVC_32 ISA ARM Segment none
Control: 10c5383d Table: 238dc06a DAC: 00000051
CPU: 0 PID: 306 Comm: insmod Tainted: P O 4.9.37 #402
Hardware name: Generic DT based system
[<c010f8b8>] (unwind_backtrace) from [<c010b4e0>] (show_stack+0x10/0x14)
[<c010b4e0>] (show_stack) from [<c0380bbc>] (dump_stack+0x84/0x98)
[<c0380bbc>] (dump_stack) from [<c0194ff8>] (watchdog_timer_fn+0x248/0x2c0)
[<c0194ff8>] (watchdog_timer_fn) from [<c016f534>] (__hrtimer_run_queues+0x128/0x1bc)
[<c016f534>] (__hrtimer_run_queues) from [<c016f71c>] (hrtimer_interrupt+0xa8/0x204)
[<c016f71c>] (hrtimer_interrupt) from [<c054b594>] (arch_timer_handler_phys+0x28/0x30)
[<c054b594>] (arch_timer_handler_phys) from [<c0163688>] (handle_percpu_devid_irq+0x74/0x134)
[<c0163688>] (handle_percpu_devid_irq) from [<c015eb30>] (generic_handle_irq+0x24/0x34)
[<c015eb30>] (generic_handle_irq) from [<c015f054>] (__handle_domain_irq+0x5c/0xb4)
[<c015f054>] (__handle_domain_irq) from [<c0101438>] (gic_handle_irq+0x48/0x8c)
[<c0101438>] (gic_handle_irq) from [<c010bfcc>] (__irq_svc+0x6c/0x90)
Exception stack(0xd470dda8 to 0xd470ddf0)
dda0: bf002304 00000000 00000001 00000000 bf002300 ffffe000
ddc0: d47a3ec0 d47a3000 00000000 bf002100 00000000 00000024 00000000 d470ddf8
dde0: bf00204c c06a4a04 80000013 ffffffff
[<c010bfcc>] (__irq_svc) from [<c06a4a04>] (_raw_spin_lock+0x2c/0x40)
[<c06a4a04>] (_raw_spin_lock) from [<bf00204c>] (hack_spinBA+0x20/0x2c [spin_lock_deadlock])
[<bf00204c>] (hack_spinBA [spin_lock_deadlock]) from [<d47a3ec0>] (0xd47a3ec0)
INFO: rcu_sched self-detected stall on CPU
0-...: (59672 ticks this GP) idle=4c3/140000000000001/0 softirq=34656/34656 fqs=14660
(t=60000 jiffies g=2778 c=2777 q=166)
Task dump for CPU 0:
insmod R running task 0 306 218 0x00000003
[<c010f8b8>] (unwind_backtrace) from [<c010b4e0>] (show_stack+0x10/0x14)
[<c010b4e0>] (show_stack) from [<c019857c>] (rcu_dump_cpu_stacks+0xa8/0xc4)
[<c019857c>] (rcu_dump_cpu_stacks) from [<c016bf54>] (rcu_check_callbacks+0x714/0x890)
[<c016bf54>] (rcu_check_callbacks) from [<c016e780>] (update_process_times+0x34/0x5c)
[<c016e780>] (update_process_times) from [<c017eaf0>] (tick_sched_timer+0x68/0x268)
[<c017eaf0>] (tick_sched_timer) from [<c016f534>] (__hrtimer_run_queues+0x128/0x1bc)
[<c016f534>] (__hrtimer_run_queues) from [<c016f71c>] (hrtimer_interrupt+0xa8/0x204)
[<c016f71c>] (hrtimer_interrupt) from [<c054b594>] (arch_timer_handler_phys+0x28/0x30)
[<c054b594>] (arch_timer_handler_phys) from [<c0163688>] (handle_percpu_devid_irq+0x74/0x134)
[<c0163688>] (handle_percpu_devid_irq) from [<c015eb30>] (generic_handle_irq+0x24/0x34)
[<c015eb30>] (generic_handle_irq) from [<c015f054>] (__handle_domain_irq+0x5c/0xb4)
[<c015f054>] (__handle_domain_irq) from [<c0101438>] (gic_handle_irq+0x48/0x8c)
[<c0101438>] (gic_handle_irq) from [<c010bfcc>] (__irq_svc+0x6c/0x90)
Exception stack(0xd470dda8 to 0xd470ddf0)
dda0: bf002304 00000000 00000001 00000000 bf002300 ffffe000
ddc0: d47a3ec0 d47a3000 00000000 bf002100 00000000 00000024 00000000 d470ddf8
dde0: bf00204c c06a4a04 80000013 ffffffff
[<c010bfcc>] (__irq_svc) from [<c06a4a04>] (_raw_spin_lock+0x2c/0x40)
[<c06a4a04>] (_raw_spin_lock) from [<bf00204c>] (hack_spinBA+0x20/0x2c [spin_lock_deadlock])
[<bf00204c>] (hack_spinBA [spin_lock_deadlock]) from [<d47a3ec0>] (0xd47a3ec0)
参考文章
https://blog.csdn.net/ccwzhu/article/details/81171092
死锁的形成
Linux 死锁检测模块 Lockdep 简介