一、什么是Linux内核死锁
死锁是指多个进程(线程)因为长久等待已被其他进程占有的的资源而陷入阻塞的一种状态。当等待的资源一直得不到释放,死锁会一直持续下去。死锁一旦发生,程序本身是解决不了的,只能依靠外部力量使得程序恢复运行,例如重启,开门狗复位等。
#Linux 提供了检测死锁的机制,主要分为 D 状态死锁和 R 状态死锁。
1.D 状态死锁:
进程等待 I/O 资源无法得到满足,长时间(系统默认配置 120 秒)处于 TASK_UNINTERRUPTIBLE 睡眠状态,这种状态下进程不响应异步信号(包括 kill -9)。如:进程与外设硬件的交互(如 read),通常使用这种状态来保证进程与设备的交互过程不被打断,否则设备可能处于不可控的状态。对于这种死锁的检测 Linux 提供的是 hung task 机制。触发该问题成因比较复杂多样,可能因为 synchronized_irq、mutex lock、内存不足等。D 状态死锁只是局部多进程间互锁,一般来说只是 hang 机、冻屏,机器某些功能没法使用,但不会导致没喂狗,而被狗咬死。
2.R 状态死锁:
进程长时间(系统默认配置 60 秒)处于 TASK_RUNNING 状态垄断 CPU 而不发生切换,一般情况下是进程关抢占或关中断后长时候执行任务、死循环,此时往往会导致多 CPU 间互锁,整个系统无法正常调度,导致喂狗线程无法执行,无法喂狗而最终看门狗复位的重启。该问题多为原子操作,spinlock 等 CPU 间并发操作处理不当造成。
#死锁的产生需要满足以下4个条件:
1》 互斥条件:指运算单元(进程、线程或协程)对所分配到的资源具有排它性,也就是说在一段时间内某个锁资源只能被一个运算单元所占用。
2》 请求和保持条件:指运算单元已经保持至少一个资源,但又提出了新的资源请求,而该资源已被其它运算单元占有,此时请求运算单元阻塞,但又对自己已获得的其它资源保持不放。
3》 不可剥夺条件:指运算单元已获得的资源,在未使用完之前,不能被剥夺。
4》 环路等待条件:指在发生死锁时,必然存在运算单元和资源的环形链,即运算单元正在等待另一个运算单元占用的资源,而对方又在等待自己占用的资源,从而造成环路等待的情况。
#常见的死锁有以下4种情况:
(1)进程重复申请同一个锁,称为AA死锁。例如,重复申请同一个自旋锁;使用读写锁,第一次申请读锁,第二次申请写锁。
(2)进程申请自旋锁时没有禁止硬中断,进程获取自旋锁以后,硬中断抢占,申请同一个自旋锁。这种AA死锁很隐蔽,人工审查很难发现。
(3)两个进程都要获取锁L1和L2,进程1持有锁L1,再去获取锁L2,如果这个时候进程2持有锁L2并且正在尝试获取锁L1,那么进程1和进程2就会死锁,称为AB-BA死锁。
(4)在一个处理器上进程1持有锁L1,再去获取锁L2,在另一个处理器上进程2持有锁L2,硬中断抢占进程2以后获取锁L1。这种AB-BA死锁很隐蔽,人工审查很难发现。
二、如何解决Linux内核死锁
死锁的常用解决方案有以下两个:
1.按照顺序加锁:尝试让所有线程按照同一顺序获取锁,从而避免死锁。
2.设置获取锁的超时时间:尝试获取锁的线程在规定时间内没有获取到锁,就放弃获取锁,避免因为长时间等待锁而引起的死锁。
Linux内核死锁检测lockdep
1.1 使用方法
死锁检测工具lockdep的配置宏如下:
(1)CONFIG_LOCKDEP:在配置菜单中看不到这个配置宏,打开配置宏CONFIG_PROVE_LOCKING或CONFIG_DEBUG_LOCK_ALLOC的时候会自动打开这个配置宏。
(2)CONFIG_PROVE_LOCKING:允许内核报告死锁问题。
(3)CONFIG_DEBUG_LOCK_ALLOC:检查内核是否错误地释放被持有的锁。
(4)CONFIG_DEBUG_LOCKING_API_SELFTESTS:内核在初始化的过程中运行一小段自我测试程序,自我测试程序检查调试机制是否可以发现常见的锁缺陷。
1.2 技术原理
死锁检测工具lockdep操作的基本对象是锁类,例如结构体里面的锁是一个锁类,结构体的每个实例里面的锁是锁类的一个实例。
lockdep跟踪每个锁类的自身状态,也跟踪各个锁类之间的依赖关系,通过一系列的验证规则,确保锁类状态和锁类之间的依赖总是正确的。另外,锁类一旦在初次使用时被注册,后续就会一直存在,它的所有具体实例都会关联到它。
1)锁类状态
lockdep为锁类定义了(4n+1)种使用历史状态,其中的4指代如下:
(1)该锁曾在STATE上下文中被持有过。
(2)该锁曾在STATE上下文中被以读锁形式持有过。
(3)该锁曾在开启STATE的情况下被持有过。
(4)该锁曾在开启STATE的情况下被以读锁形式持有过。
其中的n是STATE状态的个数,STATE状态包括硬中断(hardirq)、软中断(softirq)和reclaim_fs(__GFP_FS分配,表示允许向下调用到文件系统。如果文件系统持有锁以后使用标志位__GFP_FS申请内存,在内存严重不足的情况下,需要回收文件页,把修改过的文件页写回到存储设备,递归调用文件系统的函数,可能导致死锁)。其中的1是指该锁曾经被使用过。
如果锁曾在硬中断上下文中被持有过,那么锁是硬中断安全的(hardirq-safe);
如果锁曾在开启硬中断的情况下被持有过,那么锁是硬中断不安全的(hardirq-unsafe)。
2)检查规则
单锁状态规则如下:
(1)一个软中断不安全的锁类也是硬中断不安全的锁类。
(2)任何一个锁类,不可能同时是硬中断安全的和硬中断不安全的,也不可能同时是软中断安全的和软中断不安全的。也就是说:硬中断安全和硬中断不安全是互斥的,软中断安全和软中断不安全也是互斥的。
多锁依赖规则如下:
(1)同一个锁类不能被获取两次,否则可能导致递归死锁(AA死锁)。
(2)不能以不同顺序获取两个锁类,否则导致AB-BA死锁。
(3)不允许在获取硬中断安全的锁类之后获取硬中断不安全的锁类。
硬中断安全的锁类可能被硬中断获取。假设处理器0上的进程首先获取硬中断安全的锁类A,然后获取硬中断不安全的锁类B;处理器1上的进程获取锁类B,硬中断抢占进程,获取锁类A,可能导致AB-BA死锁。
(4)不允许在获取软中断安全的锁类之后获取软中断不安全的锁类。
软中断安全的锁类可能被软中断获取。假设处理器0上的进程首先获取软中断安全的锁类A,然后获取软中断不安全的锁类B;处理器1上的进程获取锁类B,软中断抢占进程,获取锁类A,可能导致AB-BA死锁。
当锁类的状态发生变化时,检查下面的依赖规则:
(1)如果锁类的状态变成硬中断安全,检查过去是否在获取它之后获取硬中断不安全的锁。
(2)如果锁类的状态变成软中断安全,检查过去是否在获取它之后获取软中断不安全锁。
(3)如果锁类的状态变成硬中断不安全,检查过去是否在获取硬中断安全的锁之后获取它。
(4)如果锁类的状态变成软中断不安全,检查过去是否在获取软中断安全的锁之后获取它。
如何防止Linux内核死锁
1.减少同步代码块嵌套操作
2.降低锁的使用粒度,不要几个功能共用一把锁
3.尽量采用tryLock(timeout)的方法,可以设置超时时间,这样超时之后,就可以主动退出,防止死锁(关键)