.. Kenneth Lee 版权所有 2019-2020
:Authors: Kenneth Lee :Version: 1.0
一个Linux死锁信息分析
这两天在遇到一个死锁的问题,信息大概是这样的:::
======================================================
WARNING: possible circular locking dependency detected
...
------------------------------------------------------
test_dummy/827 is trying to acquire lock:
(____ptrval____) (kn->count#4){++++}, at: kernfs_remove_by_name_ns+0x5c/0xb8
but task is already holding lock:
(____ptrval____) (xxxxx_mutex){+.+.}, at: xxxxxxxxxx+0x30/0xb8
which lock already depends on the new lock.
the existing dependency chain (in reverse order) is:
-> #2 (xxxxx_mutex){+.+.}:
lock_acquire+0xd4/0x250
__mutex_lock+0x8c/0x868
mutex_lock_nested+0x3c/0x50
...
work_pending+0x8/0x14
-> #1 (&hw->mutex){+.+.}:
lock_acquire+0xd4/0x250
__mutex_lock+0x8c/0x868
...
el0_svc+0x8/0xc
-> #0 (kn->count#4){++++}:
__lock_acquire+0x10ac/0x11d0
lock_acquire+0xd4/0x250
__kernfs_remove+0x2f4/0x348
kernfs_remove_by_name_ns+0x5c/0xb8
...
work_pending+0x8/0x14
other info that might help us debug this:
Chain exists of:
kn->count#4 --> &hw->mutex --> xxxxx_mutex
Possible unsafe locking scenario:
CPU0 CPU1
---- ----
lock(xxxxx_mutex);
lock(&hw->mutex);
lock(xxxxx_mutex);
lock(kn->count#4);
*** DEADLOCK ***
2 locks held by test_dummy/827:
#0: (____ptrval____) (&hw->mutex){+.+.}, at: xxxxxxxxx+0x34/0x98
#1: (____ptrval____) (xxxxx_mutex){+.+.}, at: xxxxxxxxxx+0x30/0xb8
这个事情很奇怪,我不觉得它提出来的Possible unsafe locking scenario真的会死锁啊 。
我个人原来一直没有看过Linux的死锁跟踪机制,为了看懂这个问题,我先速成一下,整理 一下笔记。内核代码基于5.2-rc3。
查了一下git历史,这个死锁跟踪功能最初是Ingo Molnar 2006年引入的。网上有人说第一 个版本就解决掉了大部分Linux内核的死锁问题。不过它的设计目标不是用于产品 (release)版本的,对性能有不小的影响,所以一般用于内部测试阶段。
Linux内核的lockdep-design.txt对这个东西有介绍,但我觉得文档写得很烂,前后矛盾, 语焉不详,还不如直接看代码。不过这个代码也很不规整,基本上都是细节,我也耗不起 这个时间。所以我还是聚焦到看个整体,然后重点搞清那个错误输出什么意思。
从文档建立的概念再去对了一下代码,大概的原理是这样的:给每种类型的锁都定义一个 class(相当于锁的类型,比如所有的mutex就是一个class),为每个class定义一组rules (非抽象概念,都是具体插入的不同代码),然后根据相同class的锁有没有违反rules的 行为(比如A-B, B-A互锁,在上了spinlock的情况下开中断之类的),由此判断锁设计是 否有问题。由于每次上锁解锁的过程都要加上一堆的rules判断,这个对性能的影响是摆在 那里的,但测试阶段能把问题挖出来,到正式产品中出问题的可能性也不大了,所以用于 测试是个很好的方案。
从接口上看,这个功能主要通过在锁初始化代码上加静态定义定义那个class,然后在第一 次使用的时候注册到子系统中。这是默认的情况,如果你要对你的锁做专门处理,也可以 通过lockdep_set_class()自行创建一种新的class。很多复杂的子系统都自己设置自己的 class,比如inode,各种文件系统等。
之后在上锁和解锁的代码里加lock_acquire()和lock_release(),建立那锁类型和 lockdep_map对象的映射,然后就在这些流程里进行死锁Pattern的匹配,检测出有可能的 死锁场景来。
为了增加检测的机会,在部分和锁有关的代码中,还会主动插入might_lock增加检查,这 个本质是主动把lock_acquire和lock_release调一次,就是为了检查而做的。
除了这些基本接口,lockdep还有可以用来检查某个锁肯定已经上了的 lockdep_assert_isheld(),或者确认锁不会被中途释放的lockdep*pin_lock()等辅助性 的函数。
具体的检查算法都是细节,大概的意思就是判断依赖关系是否有循环(注1),是否重复上 锁和是否在不安全上下文中上安全的锁(比如开着中断上spinlock,这会引起spinlock进 入中断,并在中断中再次spinlock,导致死锁)。我先忽略这些细节,重点解决两个问题 :
第一,错误输出中,每个锁后面{+.+.}是什么意思。从代码上看(吐一句槽:这个代码写 得极其晦涩,看着难受),这是4个上下文的状态标记。上下文分别是:::
LOCK_USED_IN_HARDIRQ
LOCK_USED_IN_HARDIRQ_READ
LOCK_USED_IN_SOFTIRQ
LOCK_USED_IN_SOFTIRQ_READ
这个标记记录的是上锁的时候是否“曾经”在对应的状态过,具体的标记表示这个上下文当 时的状态:::
. 状态关,非状态上下文(也可能就没有发生过)
- 状态关,状态上下文
+ 状态开,非状态上下文
? 状态开,状态上下文
“状态”对应上面那四个上下文标记提到的中断状态,比如第一个标记是+,就表示hardirq 开,非hardirq上下文。
而例子中的{+.+.},表示这个锁“在线程上下文没关软硬中断的情况下上过锁(非读写锁) ”,基本上可以认为全是线程之间的交互。
第二个问题是那个“Chain exists of”打印的是什么东西。这个打印并非打印一个任意长度 的列表,它只打印三个对象:source,parent,target。
source是检查的时候本线程正要上的锁
parent是当前线程上一个拿着的锁
target是发现在本线程中锁住了,但以前曾经依赖过source的锁。
这样,我们就可以面对本文开始的问题了:这个场景为什么会死锁?
我觉得这主要是打印的锅。其实这个死锁场景想表达的是:你在给kn->count#4上锁,但你 已经给xxxxx_mutex上锁了,但之前我们发现过你在上了kn->count#4的情况下,给 xxxxx_mutex上过锁,所以,这有可能是一个循环依赖。
这里报错是没有问题的,代码也应该修改,但lockdep的打印是误导的,基本上可以认为是 个Bug,但如果你能看得懂source, parent,target的意思,这个不影响你使用就是了。
注1:lockdep用的搜索算法叫bfs,我猜了很久都没有搞明白是个什么算法,后来无意中看 了一个Patch,才发现这就是简单的“Breadth-First Search”。