死锁检测工具lockdep

2,352 阅读16分钟

常见的死锁有以下4种情况:

  1. 进程重复申请同一个锁,称为AA死锁。例如,重复申请同一个自旋锁;使用读写锁,第一次申请读锁,第二次申请写锁。
  2. 进程申请自旋锁时没有禁止硬中断,进程获取自旋锁以后,硬中断抢占,申请同一个自旋锁。
  3. 两个进程都要获取锁L1与L2,进程1持有L1,再去获取锁L2,此时进程2持有锁L2,尝试获取L1,那么进程1与进程2就会死锁,成为AB-BA死锁。
  4. 在一个处理器上进程1持有锁L1,再去获取锁L2,在另一个处理器上进程2持有锁L2,硬中断抢占进程2以后获取锁L1,这种AB-BA死锁很隐蔽。

避免AB-BA死锁最简单的方法就是定义锁的申请顺序,以破坏死锁的环形等待。但是如果一个系统拥有几百个甚至几千个锁,那么没法完全定义所有锁的申请顺序,更可行的办法是在开发阶段提前发现潜在的死锁风险。内核提供的死锁检测工具lockdep用来发现内核的死锁风险。

1、使用方法

死锁检测工具lockdep配置宏如下:

  1. CONFIG_LOCKDEP:在配置菜单中看不到这个配置宏,打开配置宏CONFIG_PROVE_LOCKING或CONFIG_DEBUG_LOCK_ALLOC会自动打开这个配置宏。
  2. CONFIG_PROVE_LOCKING:允许内核报告死锁问题。
  3. CONFIG_DEBUG_LOCK_ALLOC:检查内核是否错误地释放被持有的锁。
  4. CONFIG_DEBUG_LOCKINIG_API_SELFTESTS:内核在初始化的过程中运行一小段自我测试程序,自我测试程序检查调试机制是否可以发现常见的锁缺陷。

2、技术原理

死锁检测工具lockdep操作的基本对象是锁类,例如结构体里面的锁是一个锁类,结构体的每个实例里面的锁是锁类的一个实例。

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

2.1 锁类状态

lockdep为锁类定义了(4n+1)种使用历史状态,其中4表示:

  1. 该锁曾在STATE上下文被持有过
  2. 该锁曾在STATE上下文中被以读的形式持有过
  3. 该锁曾在开启STATE的情况下被持有过
  4. 该锁曾在开启STATE的情况下被以读的形式持有过

其中n是STATE状态的个数,STATE状态包括硬中断、软中断和reclaim_fs(__GFP_FS分配,表示允许向下调用到文件系统,如果文件系统持有锁以后使用锁以后使用标志位__GFP_FS申请内存,在内存严重不足的情况下,需要回收文件页,把修改过的文件页写回到存储设备,递归调用文件系统的函数,可能会导致死锁)。

其中的1表示该锁曾经被使用过。

2.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. 如果锁类的状态变成软中断不安全,检查过去是否在获取软中断安全的锁之后获取它;

内核有时需要获取同一个锁类的多个实例,上面的检查规则会导致误报“重复上锁”,需要使用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的检查过程如下:

  1. 调用函数check_deadlock检查重复上锁,即当前进程是否已经持有锁类L2,如果已经持有锁类L2,除非两次都申请读锁,否则存在死锁。
  2. 调用函数check_prevs_add,根据以前记录到的锁类依赖关系检查死锁。

假设当前申请锁类L2,函数check_prevs_add针对当前进程的数组held_locks中的每个锁类L1,调用函数check_prev_add检查,检查过程如下:

  1. 调用函数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 ***
  1. 调用函数check_irq_usage,检查是否存在以下情况:“在获取硬中断安全的锁类之后获取硬中断不安全的锁类”或者“在获取软中断安全的锁类之后获取软中断不安全的锁类”。
  • 如果锁类L1的链表locks_before中存在硬中断安全的锁类,并且锁类L2的链表locks_after中存在硬中断不安全的锁类,那么说明在获取硬中断安全的锁类之后获取硬中断不安全的锁类,存在死锁风险。
  1. 记录锁类的依赖关系:把锁类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 死锁。

分析到这里,自然知道死锁风险点和正确使用锁的规则了,按照这个规则去修复代码,避免死锁就可以了。解决办法:

  1. 分析 resume_reason_lock 是否在其他地方中断上下文有使用这把锁。
  2. 如果没有,直接把获取这把锁的地方 wakeup_reason_pm_event+0x54/0x9c 从 spin_lock 改成 spin_lock_irqsave 就可以了。保持嵌套获取锁前后的状态一致。

5、参考文档

1. Linux 死锁检测模块 Lockdep 简介

2. 《Linux内核深度解析》基于ARM64架构之lockdep章节

3. 死锁检测lockdep实现原理

4. 用crash分析内核死锁的一次实践