常见的死锁有以下4种情况:
- 进程重复申请同一个锁,称为AA死锁。例如,重复申请同一个自旋锁;使用读写锁,第一次申请读锁,第二次申请写锁。
- 进程申请自旋锁时没有禁止硬中断,进程获取自旋锁以后,硬中断抢占,申请同一个自旋锁。
- 两个进程都要获取锁L1与L2,进程1持有L1,再去获取锁L2,此时进程2持有锁L2,尝试获取L1,那么进程1与进程2就会死锁,成为AB-BA死锁。
- 在一个处理器上进程1持有锁L1,再去获取锁L2,在另一个处理器上进程2持有锁L2,硬中断抢占进程2以后获取锁L1,这种AB-BA死锁很隐蔽。
避免AB-BA死锁最简单的方法就是定义锁的申请顺序,以破坏死锁的环形等待。但是如果一个系统拥有几百个甚至几千个锁,那么没法完全定义所有锁的申请顺序,更可行的办法是在开发阶段提前发现潜在的死锁风险。内核提供的死锁检测工具lockdep用来发现内核的死锁风险。
1、使用方法
死锁检测工具lockdep配置宏如下:
- CONFIG_LOCKDEP:在配置菜单中看不到这个配置宏,打开配置宏CONFIG_PROVE_LOCKING或CONFIG_DEBUG_LOCK_ALLOC会自动打开这个配置宏。
- CONFIG_PROVE_LOCKING:允许内核报告死锁问题。
- CONFIG_DEBUG_LOCK_ALLOC:检查内核是否错误地释放被持有的锁。
- CONFIG_DEBUG_LOCKINIG_API_SELFTESTS:内核在初始化的过程中运行一小段自我测试程序,自我测试程序检查调试机制是否可以发现常见的锁缺陷。
2、技术原理
死锁检测工具lockdep操作的基本对象是锁类,例如结构体里面的锁是一个锁类,结构体的每个实例里面的锁是锁类的一个实例。
lockdep跟踪每个锁类的自身状态,也跟踪各个锁类之间的依赖关系,通过一系列的验证规则,确保锁类状态和锁类之间的依赖总是正确的。锁类一旦在初次使用时被注册就会一直存在,它的所有具体实例都会关联到它。
2.1 锁类状态
lockdep为锁类定义了(4n+1)种使用历史状态,其中4表示:
- 该锁曾在STATE上下文被持有过
- 该锁曾在STATE上下文中被以读的形式持有过
- 该锁曾在开启STATE的情况下被持有过
- 该锁曾在开启STATE的情况下被以读的形式持有过
其中n是STATE状态的个数,STATE状态包括硬中断、软中断和reclaim_fs(__GFP_FS分配,表示允许向下调用到文件系统,如果文件系统持有锁以后使用锁以后使用标志位__GFP_FS申请内存,在内存严重不足的情况下,需要回收文件页,把修改过的文件页写回到存储设备,递归调用文件系统的函数,可能会导致死锁)。
其中的1表示该锁曾经被使用过。
2.2 检查规则
单锁状态规则如下:
- 一个软中断不安全的锁类也是硬中断不安全的锁类
- 任何一个锁类,不可能同时是硬中断安全的和硬中断不安全的,也不可能同时是软中断安全和软中断不安全的。也就是说,硬中断安全和硬中断不安全是互斥的,软中断安全和软中断不安全也是互斥的。
多锁依赖规则如下:
- 同一个锁类不能被获取两次,否则可能导致递归死锁(AA死锁);
- 不能以不同顺序获取两个锁类,否则导致AB-BA死锁;
- 不允许在获取硬中断安全的锁类之后获取硬中断不安全的锁类:例如,硬中断安全的锁类可能被硬中断获取,假设处理器0上的进程首先获取硬中断安全的锁类A,然后获取硬中断不安全的锁类B;处理器1上的进程获取锁类B,硬中断抢占进程,获取锁类A,可能导致AB-BA死锁。
- 不允许在获取软中断安全锁类之后获取软中断不安全的锁类:软中断安全的锁类可能被软中断获取,假设处理器0上的进程获取软中断安全的锁类A,然后获取软中断不安全的锁类B;处理器1上的进程获取锁类B,软中断抢占进程获取锁类A,可能导致AB-BA死锁。
当锁的状态发生变化时,检查下面的依赖规则:
- 如果锁类的状态变成硬中断安全,检查过去是否在获取它之后获取硬中断不安全的锁;
- 如果锁类的状态变成软中断安全,检查过去是否在获取它之后获取软中断不安全的锁;
- 如果锁类的状态变成硬中断不安全,检查过去是否在获取硬中断安全的锁之后获取它;
- 如果锁类的状态变成软中断不安全,检查过去是否在获取软中断安全的锁之后获取它;
内核有时需要获取同一个锁类的多个实例,上面的检查规则会导致误报“重复上锁”,需要使用spin_lock_nested()这类编程接口设置子类以区同类锁。
kernel/sched/sched.h
static inline void double_lock(spinlock_t *l1, spinlock_t *l2)
{
if (l1 > l2)
swap(l1, l2);
spin_lock(l1);
spin_lock_nested(l2, SINGLE_DEPTH_NESTING); /* 宏SINGLE_DEPTH_NESTING的值是1 */
}
3、代码分析
3.1 spin_lock_init()初始化
以自旋锁为例,自旋锁的结构体嵌入了一个数据类型为lockdep_map的成员dep_map,用来把锁实例映射到锁类。
typedef struct spinlock {
union {
struct raw_spinlock rlock;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
struct {
u8 __padding[LOCK_PADSIZE];
struct lockdep_map dep_map;
};
#endif
};
} spinlock_t;
typedef struct raw_spinlock {
arch_spinlock_t raw_lock;
#ifdef CONFIG_DEBUG_SPINLOCK
unsigned int magic, owner_cpu;
void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
} raw_spinlock_t;
数据类型lockdep_map的成员key是锁类的键值,同一个锁类的所有锁实例使用相同的键值,成员class_cache[0]指向锁类的主类(即子类号为0),class_cache[1]指向锁类的子类1。
/*
* Map the lock object (the lock instance) to the lock-class object.
* This is embedded into specific lock instances:
*/
struct lockdep_map {
struct lock_class_key *key;
struct lock_class *class_cache[NR_LOCKDEP_CACHING_CLASSES];
const char *name;
#ifdef CONFIG_LOCK_STAT
int cpu;
unsigned long ip;
#endif
};
使用函数spin_lock_init()初始化自旋锁的时候,定义一个数据类型为lock_class_key的静态局部变量,使用它的地址作为锁类的键值。
#define spin_lock_init(_lock) \
do { \
spinlock_check(_lock); \
raw_spin_lock_init(&(_lock)->rlock); \
} while (0)
#define raw_spin_lock_init(lock) \
do { \
static struct lock_class_key __key; \
\
__raw_spin_lock_init((lock), #lock, &__key); \
} while (0)
void __raw_spin_lock_init(raw_spinlock_t *lock, const char *name,
struct lock_class_key *key)
{
#ifdef CONFIG_DEBUG_LOCK_ALLOC
/*
* Make sure we are not reinitializing a held lock:
*/
debug_check_no_locks_freed((void *)lock, sizeof(*lock));
lockdep_init_map(&lock->dep_map, name, key, 0);
#endif
lock->raw_lock = (arch_spinlock_t)__ARCH_SPIN_LOCK_UNLOCKED;
lock->magic = SPINLOCK_MAGIC;
lock->owner = SPINLOCK_OWNER_INIT;
lock->owner_cpu = -1;
}
/*
* Initialize a lock instance's lock-class mapping info:
*/
void lockdep_init_map(struct lockdep_map *lock, const char *name,
struct lock_class_key *key, int subclass)
{
int i;
for (i = 0; i < NR_LOCKDEP_CACHING_CLASSES; i++)
lock->class_cache[i] = NULL;
#ifdef CONFIG_LOCK_STAT
lock->cpu = raw_smp_processor_id();
#endif
......
lock->name = name;
......
lock->key = key;
if (unlikely(!debug_locks))
return;
......
}
锁类的主要成员如下:
//lock_class:lockdep中的核心结构,维护了锁的before和after结构,就是锁之间的依赖关系.
//另外还通过链表结构维护,可以进行遍历操作,通过hash表结构进行查找操作,里面还记录锁的ip,可以通过kallsym翻译成可读形式的符号.
struct lock_class {
//用来把锁类加入散列表,第一次申请锁的时候,需要把锁实例映射到锁类,根据锁实例的键值在散列表中查找锁类
struct hlist_node hash_entry;
/用来把锁类加入全局的锁类链表
struct list_head lock_entry;
//locks_after:记录曾经在获取本锁类之后获取的所有锁类
//locks_before:记录曾经在获取本锁类之前获取的所有锁类
struct list_head locks_after, locks_before;
//指向键值
const struct lockdep_subclass_key *key;
unsigned int subclass;
unsigned int dep_gen_id;
/*
* IRQ/softirq usage tracking bits:
*/
unsigned long usage_mask;
const struct lock_trace *usage_traces[XXX_LOCK_USAGE_STATES];
/*
* Generation counter, when doing certain classes of graph walking,
* to ensure that we check one node only once:
*/
int name_version;
const char *name;
#ifdef CONFIG_LOCK_STAT
unsigned long contention_point[LOCKSTAT_POINTS];
unsigned long contending_point[LOCKSTAT_POINTS];
#endif
} __no_randomize_layout;
在进程描述符中增加了以下成员:
struct task_struct {
#ifdef CONFIG_LOCKDEP
#define MAX_LOCK_DEPTH 48UL
u64 curr_chain_key;
//进程持有的锁的数量
int lockdep_depth;
unsigned int lockdep_recursion;
//表示进程持有的锁,按时间先后排列
struct held_lock held_locks[MAX_LOCK_DEPTH];
#endif
}
3.2 spin_lock()申请自旋锁
spin_lock()
↓
raw_spin_lock()
↓
_raw_spin_lock() @kernel/spinlock.c
↓
__raw_spin_lock() @include/linux/spinlock_api_smp.h
→ preempt_disable();
→ spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
↓
#define lock_acquire_exclusive(l, s, t, n, i) lock_acquire(l, s, t, 0, 1, n, i)
#define spin_acquire(l, s, t, i) lock_acquire_exclusive(l, s, t, NULL, i)
static int __lock_acquire(struct lockdep_map *lock, unsigned int subclass,
int trylock, int read, int check, int hardirqs_off,
struct lockdep_map *nest_lock, unsigned long ip,
int references, int pin_count)
→ __lock_acquire()
// __lock_acquire() 是 lockdep 死锁检测的核心,
//所有原理中描述的死锁错误都是在这里检测的。如果出错,最终会调用 print_xxx_bug() 函数。
→ __lock_acquire()
→ LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
//调用者必须确保在调用它之前禁用 IRQ,否则我们可能会得到一个想要获取锁的中断,这将再次进入 lockdep。
static int __lock_acquire(struct lockdep_map *lock, unsigned int subclass,
int trylock, int read, int check, int hardirqs_off,
struct lockdep_map *nest_lock, unsigned long ip,
int references, int pin_count)
{
struct task_struct *curr = current;
struct lock_class *class = NULL;
struct held_lock *hlock;
unsigned int depth;
int chain_head = 0;
int class_idx;
u64 chain_key;
......
//如果类不存在,则在哈希表中注册锁的类。,否则我们查找一下。
//我们将结果缓存在锁对象本身lock->class_cache中,因此哈希的实际查找应该针对每个锁对象进行一次。
if (unlikely(!class)) {
class = register_lock_class(lock, subclass, 0);
if (!class)
return 0;
}
......
//将锁添加到当前进程持有的锁列表中。(我们还没有增加深度,直到完成依赖性检查)
depth = curr->lockdep_depth;
......
//lock_classes是所有lock_class集合
//#define MAX_LOCKDEP_KEYS_BITS 13
//#define MAX_LOCKDEP_KEYS (1UL << MAX_LOCKDEP_KEYS_BITS)=8192
//struct lock_class lock_classes[MAX_LOCKDEP_KEYS];
class_idx = class - lock_classes;
//当前进程持有的锁数量不为0
if (depth) {
hlock = curr->held_locks + depth - 1;
//spin_lock时nest_lock=0
if (hlock->class_idx == class_idx && nest_lock) {
......
}
}
//当前进程要持有的锁held_lock结构体填充
hlock = curr->held_locks + depth;
hlock->class_idx = class_idx;
hlock->acquire_ip = ip;
hlock->instance = lock;
hlock->nest_lock = nest_lock;
hlock->irq_context = task_irq_context(curr);
hlock->trylock = trylock;
hlock->read = read;
hlock->check = check;
hlock->hardirqs_off = !!hardirqs_off;
hlock->references = references;
#ifdef CONFIG_LOCK_STAT
hlock->waittime_stamp = 0;
hlock->holdtime_stamp = lockstat_clock();
#endif
hlock->pin_count = pin_count;
/* Initialize the lock usage bit */
if (!mark_usage(curr, hlock, check))
return 0;
......
//检查死锁
if (!validate_chain(curr, hlock, chain_head, chain_key))
return 0;
curr->curr_chain_key = chain_key;
//当前进程要持有的锁数量加1
curr->lockdep_depth++;
check_chain_key(curr);
......
return 1;
}
static int validate_chain(struct task_struct *curr,
struct held_lock *hlock,
int chain_head, u64 chain_key)
{
if (!hlock->trylock && hlock->check &&
lookup_chain_cache_add(curr, hlock, chain_key)) {
//检查当前task_struct的held_locks栈是否有AA锁
int ret = check_deadlock(curr, hlock);
if (!ret)
return 0;
//标记递归读取
if (ret == 2)
hlock->read = 2;
//添加依赖关系
if (!chain_head && ret != 2) {
if (!check_prevs_add(curr, hlock))
return 0;
}
graph_unlock();
} else {
/* after lookup_chain_cache_add(): */
if (unlikely(!debug_locks))
return 0;
}
return 1;
}
static int
check_prevs_add(struct task_struct *curr, struct held_lock *next)
{
struct lock_trace *trace = NULL;
int depth = curr->lockdep_depth;
struct held_lock *hlock;
......
//遍历当前进程held_locks的栈,添加和next的关联关系
for (;;) {
int distance = curr->lockdep_depth - depth + 1;
hlock = curr->held_locks + depth - 1;
if (hlock->read != 2 && hlock->check) {
//死锁检查,添加prev和next的关联关系
int ret = check_prev_add(curr, hlock, next, distance,
&trace);
if (!ret)
return 0;
if (!hlock->trylock)
break;
}
depth--;
if (!depth)
break;
//如果我们进入另一个上下文,则停止搜索
if (curr->held_locks[depth].irq_context !=
curr->held_locks[depth-1].irq_context)
break;
}
return 1;
out_bug:
......
return 0;
}
static int
check_prev_add(struct task_struct *curr, struct held_lock *prev,
struct held_lock *next, int distance,
struct lock_trace **const trace)
{
struct lock_list *entry;
int ret;
......
//广度优先搜索,检查是否形成环形,有环则代表死锁
ret = check_noncircular(next, prev, trace);
if (unlikely(ret <= 0))
return 0;
//hardirq-safe(-read) 锁与 hardirq-unsafe 锁 死锁判断
//softirq-safe(-read) 锁与 softirq-unsafe 锁 死锁判断
if (!check_irq_usage(curr, prev, next))
return 0;
......
//所有验证都通过了,没有死锁,将新锁添加到之前锁的依赖列表中
//next lock添加到prev的locks_after链表上,不是直接挂到链表,而是新申请一个lock_list挂到locks_after上
ret = add_lock_to_list(hlock_class(next), hlock_class(prev),
&hlock_class(prev)->locks_after上,
next->acquire_ip, distance, *trace);
if (!ret)
return 0;
//prev lock添加到next的locks_before链表上
ret = add_lock_to_list(hlock_class(prev), hlock_class(next),
&hlock_class(next)->locks_before,
next->acquire_ip, distance, *trace);
if (!ret)
return 0;
return 2;
}
3.3 检查死锁的原理
假设当前申请锁类L2,函数validate_chain的检查过程如下:
- 调用函数check_deadlock检查重复上锁,即当前进程是否已经持有锁类L2,如果已经持有锁类L2,除非两次都申请读锁,否则存在死锁。
- 调用函数check_prevs_add,根据以前记录到的锁类依赖关系检查死锁。
假设当前申请锁类L2,函数check_prevs_add针对当前进程的数组held_locks中的每个锁类L1,调用函数check_prev_add检查,检查过程如下:
- 调用函数check_noncircular以检查AB-BA死锁。
- 检查锁类L1是否出现在锁类L2的链表locks_after中,如果出现,说明以前有过的申请顺序是L2-->L1,而现在的申请顺序是L1-->L2,存在死锁风险。
CPU1 CPU2
process1 process2
---- ----
[ L1 ]
process1的held_locks中有L1
[ L2 ]
process2的held_locks中有L2
[ L1 ]
process2的held_locks中有L2-->L1
L1出现在锁类L2的链表locks_after中
[ L2 ]
遍历process1的held_locks,
检查发现process1中L1-->L2
然后调用函数check_prev_add检查发现有L2-->L1(有环)
则process1持有L1,proecss2持有L2等L1,process1又去申请L2
*** DEADLOCK ***
- 递归检查,针对锁类L2的链表locks_after中的每个锁类L3,检查锁类L1是否出现在锁类L3的链表locks_after中,如果出现,说明存在死锁风险。
CPU1 CPU2 CPU3
process1 process2 process3
---- ---- ----
[ L1 ]
process1的held_locks中有L1
[ L2 ]
process2的held_locks中有L2
[ L3 ]
process3的held_locks中有L3
[ L3 ]
process2的held_locks中有L2-->L3
L3出现在锁类L2的链表locks_after中
[ L1 ]
process3的held_locks中有L3-->L1
L1出现在锁类L3的链表locks_after中
[ L2 ]
遍历process1的held_locks,
检查发现process1中L1 --> L2
然后调用函数check_prev_add通过广度优先遍历发现有L2->L3,L3->L1(有环)
则process1持有L1,proecss2持有L2等L3,proecss3持有L3等L1,process1又去申请L2
*** DEADLOCK ***
- 调用函数check_irq_usage,检查是否存在以下情况:“在获取硬中断安全的锁类之后获取硬中断不安全的锁类”或者“在获取软中断安全的锁类之后获取软中断不安全的锁类”。
- 如果锁类L1的链表locks_before中存在硬中断安全的锁类,并且锁类L2的链表locks_after中存在硬中断不安全的锁类,那么说明在获取硬中断安全的锁类之后获取硬中断不安全的锁类,存在死锁风险。
- 记录锁类的依赖关系:把锁类L2添加到锁类L1的链表locks_after中,把锁类L1添加到锁类L2的链表locks_before中。
4、使用实例
lockdep的日志非常人性化,我们可以通过错误提示就能知道发生了哪些错误,唯一难理解的是它的状态显式,它使用了’.-+?’,下面进行摘自内核文档Documentation/locking/lockdep-design.txt
modprobe/2287 is trying to acquire lock:
(&sio_locks[i].lock){-.-...}, at: [<c02867fd>] mutex_lock+0x21/0x24
but task is already holding lock:
(&sio_locks[i].lock){-.-...}, at: [<c02867fd>] mutex_lock+0x21/0x24
注意大括号内的符号,一共有6个字符,分别对应STATE和STATE-read这六种(因为目前每个STATE有3种不同含义)情况,各个字符代表的含义分别如下:
- ‘.’ 表示在进程上下文,在 irq 关闭时获得一把锁
- ‘-‘ 表示在中断上下文,获得一把锁
- ‘+’ 表示在 irq 打开时获得一把锁
- ‘?’ 表示在中断上下文,在 irq 打开时获得一把锁
Lockdep 每次都只检测并 report 第一次出错的地方。因为第一个报出来的可能会引发其他的风险提示,就像编译错误一样。并且,这只是一个 warning info, 在实时运行的系统中,LOG 可能一下子就被冲掉了。可以把 lockdep warning 转化为 BUG_ON(),使机器在遇到死锁风险就主动重启来引起开发人员的关注,从而不放过每一个可能存在的漏洞。
下面是参考文档中魅族实际开发遇到的lockdep报的死锁风险log:
(0)[1132:system_server]======================================================
(0)[1132:system_server][ INFO: HARDIRQ-safe -> HARDIRQ-unsafe lock order detected ]
(0)[1132:system_server]3.18.22-eng-01315-gea95810-cIb68b198-dirty #2 Tainted: G W
(0)[1132:system_server]------------------------------------------------------
(0)[1132:system_server]system_server/1132 [HC0[0]:SC0[0]:HE0:SE1] is trying to acquire:
(0)[1132:system_server]lockdep: [ffffffc0013a6b18] (resume_reason_lock){+.+...}
(0)[1132:system_server]lockdep: , at:
(0)[1132:system_server][<ffffffc00011a2e0>] log_wakeup_reason+0x40/0x17c
(0)[1132:system_server]
and this task is already holding:
(0)[1132:system_server]lockdep: [ffffffc001401440] (__spm_lock){-.....}
(0)[1132:system_server]lockdep: , at:
(0)[1132:system_server][<ffffffc000492164>] spm_go_to_sleep+0x200/0x948
(0)[1132:system_server]which would create a new lock dependency:
(0)[1132:system_server] (__spm_lock){-.....} -> (resume_reason_lock){+.+...}
(0)[1132:system_server]
but this new dependency connects a HARDIRQ-irq-safe lock:
(0)[1132:system_server] (__spm_lock){-.....}
... which became HARDIRQ-irq-safe at:
(0)[1132:system_server] [<ffffffc00010b834>] mark_lock+0x180/0x770
(0)[1132:system_server] [<ffffffc00010e868>] __lock_acquire+0xaf8/0x243c
(0)[1132:system_server] [<ffffffc000110b08>] lock_acquire+0xe8/0x1a8
(0)[1132:system_server] [<ffffffc000c73eb4>] _raw_spin_lock_irqsave+0x54/0x84
(0)[1132:system_server] [<ffffffc00048f880>] spm_irq0_handler+0x2c/0x12c
(0)[1132:system_server] [<ffffffc00011f948>] handle_irq_event_percpu+0xc0/0x338
(0)[1132:system_server] [<ffffffc00011fc08>] handle_irq_event+0x48/0x78
(0)[1132:system_server] [<ffffffc000122d68>] handle_fasteoi_irq+0xe0/0x1a4
(0)[1132:system_server] [<ffffffc00011eee0>] generic_handle_irq+0x30/0x4c
(0)[1132:system_server] [<ffffffc00011effc>] __handle_domain_irq+0x100/0x2a4
(0)[1132:system_server] [<ffffffc000081568>] gic_handle_irq+0x54/0xe0
(0)[1132:system_server] [<ffffffc000085290>] el0_irq_naked+0x14/0x24
(0)[1132:system_server]
to a HARDIRQ-irq-unsafe lock:
(0)[1132:system_server] (resume_reason_lock){+.+...}
... which became HARDIRQ-irq-unsafe at:
(0)[1132:system_server]... [<ffffffc00010b834>] mark_lock+0x180/0x770
(0)[1132:system_server] [<ffffffc00010e65c>] __lock_acquire+0x8ec/0x243c
(0)[1132:system_server] [<ffffffc000110b08>] lock_acquire+0xe8/0x1a8
(0)[1132:system_server] [<ffffffc000c73e48>] _raw_spin_lock+0x38/0x50
(0)[1132:system_server] [<ffffffc00011a258>] wakeup_reason_pm_event+0x54/0x9c
(0)[1132:system_server] [<ffffffc0000c4d88>] notifier_call_chain+0x84/0x2d4
(0)[1132:system_server] [<ffffffc0000c5400>] __blocking_notifier_call_chain+0x40/0x74
(0)[1132:system_server] [<ffffffc0000c5444>] blocking_notifier_call_chain+0x10/0x1c
(0)[1132:system_server] [<ffffffc000115ed4>] pm_notifier_call_chain+0x1c/0x48
(0)[1132:system_server] [<ffffffc000117b68>] pm_suspend+0x36c/0x70c
(0)[1132:system_server] [<ffffffc000115e40>] state_store+0xb0/0xe0
(0)[1132:system_server] [<ffffffc0003b1f28>] kobj_attr_store+0x10/0x24
(0)[1132:system_server] [<ffffffc000266f88>] sysfs_kf_write+0x50/0x64
(0)[1132:system_server] [<ffffffc0002662c8>] kernfs_fop_write+0x110/0x180
(0)[1132:system_server] [<ffffffc0001f6570>] vfs_write+0x98/0x1b8
(0)[1132:system_server] [<ffffffc0001f678c>] SyS_write+0x4c/0xb0
(0)[1132:system_server] [<ffffffc0000854ac>] el0_svc_naked+0x20/0x28
(0)[1132:system_server]
other info that might help us debug this:
(0)[1132:system_server] Possible interrupt unsafe locking scenario:
(0)[1132:system_server] CPU0 CPU1
(0)[1132:system_server] ---- ----
(0)[1132:system_server] lock(resume_reason_lock);
(0)[1132:system_server] local_irq_disable();
(0)[1132:system_server] lock(__spm_lock);
(0)[1132:system_server] lock(resume_reason_lock);
(0)[1132:system_server] <Interrupt>
(0)[1132:system_server] lock(__spm_lock);
(0)[1132:system_server] *** DEADLOCK ***
从上面的 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。再来看看可能发生死锁的情景:
(0)[1132:system_server] Possible interrupt unsafe locking scenario:
(0)[1132:system_server] CPU0 CPU1
(0)[1132:system_server] ---- ----
(0)[1132:system_server] lock(resume_reason_lock);
(0)[1132:system_server] local_irq_disable();
(0)[1132:system_server] lock(__spm_lock);
(0)[1132:system_server] lock(resume_reason_lock);
(0)[1132:system_server] <Interrupt>
(0)[1132:system_server] lock(__spm_lock);
(0)[1132:system_server] *** DEADLOCK ***
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 死锁。
分析到这里,自然知道死锁风险点和正确使用锁的规则了,按照这个规则去修复代码,避免死锁就可以了。解决办法:
- 分析 resume_reason_lock 是否在其他地方中断上下文有使用这把锁。
- 如果没有,直接把获取这把锁的地方 wakeup_reason_pm_event+0x54/0x9c 从 spin_lock 改成 spin_lock_irqsave 就可以了。保持嵌套获取锁前后的状态一致。