Lockdep定位linux驱动死锁的bug

死锁一般比较难定位。

lockdep 死锁检测模块

介绍了最简单的 ABBA 死锁的形成,回到正题,回到 kernel, 里面有千千万万锁,错综复杂,也不可能要求所有开发人员熟悉 spin_lock, spin_lock_irq, spin_lock_irqsave, spin_lock_nested 的区别。所以,在锁死发生前,还是要做好预防胜于治疗,防患于未然的工作,尽量提前发现并且提前在开发阶段发现和解决这其中潜在的死锁风险,而不是等到最后真正出现死锁时给用户带来糟糕的体验。应运而生的就是 lockdep 死锁检测模块,在 2006 年已经引入内核(https://lwn.net/Articles/185666/)。

1. 相关内核配置选项

  • CONFIG_PROVE_LOCKINGThis feature enables the kernel to report locking related deadlocks before they actually occur. For more details, see Documentation/locking/lockdep-design.txt.
  • CONFIG_DEBUG_LOCK_ALLOCDetect incorrect freeing of live locks.
  • CONFIG_DEBUG_LOCKDEPThe lock dependency engine will do additional runtime checks to debug itself, at the price of more runtime overhead.
  • CONFIG_LOCK_STATLock usage statistics. For more details, see Documentation/locking/lockstat.txt
  • CONFIG_DEBUG_LOCKING_API_SELFTESTSThe kernel to run a short self-test during bootup instart_kernel(). The self-test checks whether common types of locking bugs are detected by debugging mechanisms or not. For more details, see lib/locking-selftest.c

2. 基本实现

lockdep 操作的基本单元并非单个的锁实例,而是锁类(lock-class),事实上,也没必要跟踪千千万万的锁,完全可以用同一方式对待同一类锁的行为。比如,struct inode 结构体中的自旋锁 i_lock 字段就代表了这一类锁,而具体每个 inode 节点的锁只是该类锁中的一个实例。

对于每个锁的初始化,这段代码创建了一个静态变量 (__key),并使用它的地址作为识别锁的类型。因此,系统中的每个锁 ( 包括 rwlocks 和 mutexes ) 都被分配一个特定的 key 值,并且都是静态声明的,同一类的锁会对应同一个 key 值。这里用得是哈希表来存储。

Lockdep 为每个锁类维护了两个链表:

  • before 链:锁类 L 前曾经获取的所有锁类,也就是锁类 L 前可能获取的锁类集合。
  • after 链:锁类 L 后曾经获取的所有锁类。

Lockdep 逻辑:

当获取 L 时,检查 after 链中的锁类是否已经被获取,如果存在则报重复上锁。联合 L 的 after 链,和已经获取的锁的 before 链。递归检查是否某个已经获取的锁中包含 L after 锁。为了加速,lockdep 检查锁类顺序关系,计算出 64bit 的 hash key。当新的 lock 顺序出现则计算 hash key 并放入表中。当获取锁时,则直接扫描表,用于加速。

也由于上述的设计逻辑,不可避免会存在误报。例如,同一类(对应相同 key 值)的多个锁同时持有时,Lockdep 会误报“重复上锁”的警报。此时,你就需要使用 spin_lock_nested 这类 API 设置不同的子类来区分同类锁,消除警报。

随便找一个代码例子:

1)初始化

2)获取锁

3. 检查规则

1)概述

Lockdep 操作的基本单元并非单个的锁实例,而是锁类(lock-class)。比如,struct inode 结构体中的自旋锁 i_lock 字段就代表了这一类锁,而具体每个 inode 节点的锁只是该类锁中的一个实例。

lockdep 跟踪每个锁类的自身状态,也跟踪各个锁类之间的依赖关系,通过一系列的验证规则,以确保锁类状态和锁类之间的依赖总是正确的。另外,锁类一旦在初次使用时被注册,那么后续就会一直存在,所有它的具体实例都会关联到它。

2)状态

锁类有 4n + 1 种不同的使用历史状态:

其中的 4 是指:

  • ‘ever held in STATE context’ –> 该锁曾在 STATE 上下文被持有过
  • ‘ever held as readlock in STATE context’ –> 该锁曾在 STATE 上下文被以读锁形式持有过
  • ‘ever held with STATE enabled’ –> 该锁曾在启用 STATE 的情况下被持有过
  • ‘ever held as readlock with STATE enabled’ –> 该锁曾在启用 STATE 的情况下被以读锁形式持有过

其中的 n 也就是 STATE 状态的个数:

  • hardirq –> 硬中断
  • softirq –> 软中断
  • reclaim_fs –> fs 回收

其中的 1 是:

  • ever used [ == !unused ] –> 不属于上面提到的任何特殊情况,仅仅只是表示该锁曾经被使用过

当触发 lockdep 检测锁的安全规则时,会在 log 中提示对应的状态位信息

比如:

注意大括号内的符号,一共有 6 个字符,分别对应 STATE 和 STATE-read 这六种(因为目前每个 STATE 有 3 种不同含义)情况,各个字符代表的含义分别如下:

  • ’.’ 表示在在进程上下文,在 irq 关闭时获得一把锁
  • ’-‘ 表示在中断上下文,获得一把锁
  • ’+’ 表示在 irq 打开时获得一把锁
  • ’?’ 表示在中断上下文,在 irq 打开时获得一把锁

3)单锁状态规则(Single-lock state rules)

  • 一个软中断不安全 (softirq-unsafe) 的锁类也是硬中断不安全 (hardirq-unsafe) 的锁类。
  • 对于任何一个锁类,它不可能同时是 hardirq-safe 和 hardirq-unsafe,也不可能同时是 softirq-safe 和 softirq-unsafe,即这两对对应状态是互斥的。

上面这两条就是 lockdep 判断单锁是否会发生死锁的检测规则。

关于四个名称的概念如下 :

  • ever held in hard interrupt context (hardirq-safe);
  • ever held in soft interrupt context (softirg-safe);
  • ever held in hard interrupt with interrupts enabled (hardirq-unsafe);
  • ever held with soft interrupts and hard interrupts enabled (softirq-unsafe);

4)多锁依赖规则(Multi-lock dependency rules)

  • 同一个锁类不能被获取两次,否则会导致递归死锁(AA)。
  • 不能以不同的顺序获取两个锁类,即:

是不行的。因为这会非常容易的导致 AB-BA 死锁。当然,下面这样的情况也不行,即在中间插入了其它正常顺序的锁也能被 lockdep 检测出来:

  • 同一个锁实例在任何两个锁类之间,嵌套获取锁的状态前后需要保持一致,即:

这意味着,如果同一个锁实例,在某些地方是 hardirq-safe(即采用 spin_lock_irqsave(…)),而在某些地方又是 hardirq-unsafe(即采用 spin_lock(…)),那么就存在死锁的风险。这应该容易理解,比如在进程上下文中持有锁 A,并且锁 A 是 hardirq-unsafe,如果此时触发硬中断,而硬中断处理函数又要去获取锁 A,那么就导致了死锁。后面会有例子分析。

在锁类状态发生变化时,进行如下几个规则检测,判断是否存在潜在死锁。比较简单,就是判断 hardirq-safe 和 hardirq-unsafe 以 及 softirq-safe 和 softirq-unsafe 是否发生了碰撞,直接引用英文,如下:

  • if a new hardirq-safe lock is discovered, we check whether it took any hardirq-unsafe lock in the past.
  • if a new softirq-safe lock is discovered, we check whether it took any softirq-unsafe lock in the past.
  • if a new hardirq-unsafe lock is discovered, we check whether any hardirq-safe lock took it in the past.
  • if a new softirq-unsafe lock is discovered, we check whether any softirq-safe lock took it in the past.

所以要注意嵌套获取锁前后的状态需要保持一致,避免死锁风险。

5) 出错处理

当检测到死锁风险时,lockdep 会打印下面几种类型的风险提示,更完整的 LOG 会在下面例子中展示。

  • [ INFO: possible circular locking dependency detected ] // 圆形锁,获取锁的顺序异常(ABBA)
  • [ INFO: %s-safe -> %s-unsafe lock order detected ] // 获取从 safe 的锁类到 unsafe 的锁类的操作
  • [ INFO: possible recursive locking detected ] // 重复去获取同类锁(AA)
  • [ INFO: inconsistent lock state ] // 锁的状态前后不一致
  • [ INFO: possible irq lock inversion dependency detected ] // 嵌套获取锁的状态前后需要保持一致,即 [hardirq-safe] -> [hardirq-unsafe],[softirq-safe] -> [softirq-unsafe] 会警报死锁风险
  • [ INFO: suspicious RCU usage. ] // 可疑的 RCU 用法

4. 使用实例

Lockdep 每次都只检测并 report 第一次出错的地方。

只报一次死锁风险打印提示就不报了,因为第一个报出来的可能会引发其他的风险提示,就像编译错误一样。并且,这只是一个 warning info, 在实时运行的系统中,LOG 可能一下子就被冲掉了。本着魅族手机对用户体验极致的追求,不允许任何一个死锁风险在开发阶段侥幸存在,我们会把 lockdep warning 转化为BUG_ON(),使机器在遇到死锁风险就主动重启来引起开发人员的关注,从而不放过每一个可能存在的漏洞。

下面是实际开发中遇到 lockdep 报的死锁风险 LOG:

从上面的 LOG 信息可以知道:system_server 已经合了一个 HARDIRQ-safe 的锁 __spm_lock, 此时再去拿一个 HARDIRQ-unsafe 的锁 resume_reason_lock,违反了嵌套获取锁前后的状态需要保持一致的规则。

记得上面说过一条规则吗?

if a new hardirq-unsafe lock is discovered, we check whether any hardirq-safe lock took it in the past.(当要获取一个 hardirq-unsafe lock 时,lockdep 就会检查该进程是否在之前已经获取 hardirq-safe lock)

HARDIRQ-safe 是不允许 irq 的锁,如:spin_lock_irqsave(&lock, flags);

HARDIRQ-unsafe 是允许 irq 的锁,如:spin_lock(&lock);

在之前已经使用 spin_lock_irqsave 的方式拿了 __spm_lock, 再以 spin_lock 的方式拿 resume_reason_lock。再来看看可能发生死锁的情景:

Lockdep 列出一个可能发生死锁的设想:

  • CPU0 先获取了一个 HARDIRQ-unsafe 的锁 lock(resume_reason_lock),CPU0 本地 irq 是开启的。
  • 接着 CPU1 再获取了 HARDIRQ-safe 的锁 lock(__spm_lock),此时 CPU1 本地 irq 是关闭的。
  • 接着 CPU1 又去获取 lock(resume_reason_lock),但此时该锁正在被 CPU0 锁持有,CPU1 唯有等待 lock(resume_reason_lock) 释放而无法继续执行。
  • 假如此时 CPU0 来了一个中断,并且在中断里去获取 lock(__spm_lock),CPU0 也会因为该锁被 CPU1 持有而未被释放而一直等待无法继续执行。
  • CPU0, CPU1 都因为互相等待对方释放锁而不能继续执行,导致 AB-BA 死锁。

分析到这里,自然知道死锁风险点和正确使用锁的规则了,按照这个规则去修复代码,避免死锁就可以了。解决办法: 1. 分析 resume_reason_lock 是否在其他地方中断上下文有使用这把锁。 2. 如果没有,直接把获取这把锁的地方 wakeup_reason_pm_event+0x54/0x9c 从 spin_lock 改成 spin_lock_irqsave 就可以了。保持嵌套获取锁前后的状态一致。


  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值