Linux死锁检测:lockdep

死锁形成是因为以下四个必要条件,理论上只要破坏其中一个条件就可以避免死锁,但是在实际应用过程中一般会采用lock ordering破环循环等待条件。

  1. 互斥条件:一个资源每次只能被一个进程使用。

  2. 请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。

  3. 不剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺。

  4. 循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。

但是对于大型系统而言,因为长调用链,多核调度,中断等一系列原因很难保证系统上锁遵循lock ordering,所以linux引用lockdep机制检查死锁。
在这里插入图片描述

在申请一把锁时将其他所有锁都申请,强制保证lock ordering,只用一把大锁不是更好?

lockdep检测单位:class

lockdep检查的是lock class,而不是具体的lock instance,这样可以将instance每个具体的场景都映射到class中,根据lockdep design所说,依靠这种方式,lockdep可以不依赖特定的multi-cpu /multi-task test case而检查死锁的风险。

lockdep在lock instance初始化时(相同class的instance在同一个地方显式初始化),会有一个静态局部变量__key作为lock.key,用于在register_lock_class中注册class,其key address作为class在hash表中的索引值,保证class的唯一性。

# define raw_spin_lock_init(lock)				\
do {								\
	static struct lock_class_key __key;			\
								\
	__raw_spin_lock_init((lock), #lock, &__key);		\
} while (0)

static inline struct lock_class *
register_lock_class(struct lockdep_map *lock, unsigned int subclass, int force)
{
	... ...
	/*在classhash_table hash表中查找class(lock->key->subkeys + subclass),其中subclass是nest lock用于不同的class*/
	class = look_up_lock_class(lock, subclass);
	
	... ...
	/通过lock.key确定class在hash表中的位置/
	key = lock->key->subkeys + subclass;
	hash_head = classhashentry(key);
	
	... ...
	/*申请class,并初始化*/
	class = lock_classes + nr_lock_classes++;
	debug_atomic_inc(nr_unused_locks);
	class->key = key;
	class->name = lock->name;
	class->subclass = subclass;
	
	... ...
	/*将class添加到hash中*/
	list_add_tail_rcu(&class->hash_entry, hash_head);
	... ...
}

lockdep检测

lockdep会通过usage_mask在class中统计lock所处的情形,总共有4 * nSTATEs + 1中情形。然后在上锁的过程中除了检查锁在当前的情形是否会产生死锁外,还会检查lock new_mask和之前的usage_mask组合是否产生死锁。

4 是指:

4usage_mask
ever held in STATE context(state,used,w)
ever held as readlock in STATE context(state,used,r)
ever held with STATE enabled(state,enable,w)
ever held as readlock with STATE enabled(state,enable,r)

state 是指:

  1. hardirq –> 硬中断

  2. softirq –> 软中断

  3. reclaim_fs –> fs 回收

1 是指:

ever used

首先会通过mark_irqflags检查现在lock new_mask是否会和lock之前所在的情形(usage_mask)产生冲突;其次会通过validate_chain检查lock和chain及其usage_mask产生循环上锁的情形。

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)
{
	/*先从cacha中查找lock class,如果找不到,通过register_lock_class查找或申请class*/
	if (subclass < NR_LOCKDEP_CACHING_CLASSES)
		class = lock->class_cache[subclass];

	if (unlikely(!class)) {
		class = register_lock_class(lock, subclass, 0);
	... ...
	}
	
	... ...
	/*进程持有锁的深度*/
	depth = curr->lockdep_depth;
	
	... ...
	/*nest lock有两种形式:
	1.整个hash表用一把锁,每个bucket共用一个class,下面是检查之后bucket使用锁时,只递增bucket.hlock.references
	2.另一中是有不同的subclass(normal,whole,partition),但是这种nest_lock对应的会选择同一个lock.key下的不同class
	*/
	class_idx = class - lock_classes + 1;

	if (depth) {
		hlock = curr->held_locks + depth - 1;
		if (hlock->class_idx == class_idx && nest_lock) {
			if (hlock->references)
				hlock->references++;
			else
				hlock->references = 2;

			return 1;
		}
	}
	
	... ...
	/*向进程申请hlock,并初始化*/
	hlock = curr->held_locks + depth;
	hlock->class_idx = class_idx;
	hlock->instance = lock;
	hlock->nest_lock = nest_lock;
	hlock->trylock = trylock;
	hlock->read = read;
	hlock->check = check;
	hlock->hardirqs_off = !!hardirqs_off;
	hlock->references = references;
	... ...
	
	/*
	功能分析:mark_irqflags将lock instance所处的(state,enable/uesed,r/w)信息归总到usage_mask,并通过mark_lock_irq检查是否会产生死锁
	函数分析:mark_lock_irq中valid_state检查AA型死锁,check_usage_XX检查 AB-BA型死锁
	1.excl_bit是和new_bit相同状态下irq有冲突的写锁。如果lock是uesed(0),其locks_after不能有相同状态下enable的锁(excl_bit),否则可能在该锁区域内发生中断,然后再锁上lock,和现在的lock ordering相反造成(AB-BA)死锁。同理当lock enable(1)时,其locks_before不能有used的锁。
	2.valid_state用于检查lock本身也不能有excl_bit的情形,否则会造成AA死锁
	3.死锁检查范围(linux4.4.0中只有STRICT_READ_CHECKS置上,所有的情形才都能检测的到):
		-对于写锁,对于AA型死锁自身的读写锁都会检查;对于AB-BA型死锁,检查和其他读写锁的乱序
		-对于读锁lock,对于AA性死锁只会检查自身写锁;对于AB-BA型死锁,会检查写锁(因为将所有属性都归纳到class,所以不知道这把锁是在读lock还是写lock之后,虽然在读lock之后没问题,但是在写lock之后就会存在问题)
	*/
	if (check && !mark_irqflags(curr, hlock))
		return 0;
	if (!mark_lock(curr, hlock, LOCK_USED))
		return 0;
    /*
    1.chain_key:chain是后续validate_chain的基本单位,chain_key是其唯一标志,如果chain_key相同表示是同一条chain(lockdep design说这是lockdep 100%proof的条件之一),就不会在通过validate_chain验证,保证performance。
    注:1.chain_key是利用之前chain的key和class_id计算hash,如果chain没有(chain_head置上),其chain_key默认是0
            2.不在同一个irq_context中视为一个新chain的开始(chain_head)
            3.curr.curr_chain_key指向当前进程所持有的chain,每个hlock都有一个prev_chain_key表征其所在的chain。chain与进程无关,只要chain_key相同就是一个chain
    */
    ... ...
    /*第一种nest lock首个bucket上锁时要检查整个hash锁要锁上*/
    if (nest_lock && !__lock_is_held(nest_lock))
        return print_lock_nested_lock_not_held(curr, hlock, ip);
    /*
    功能分析:前面mark_irqflags是检查lock在之前出现情形下是否会出现死锁,现在是检查当前chain情景下是否会出现死锁
    函数分析:
    1.lookup_chain_cache检查当前hash中是否存在chain_key对应的chain,如果存在返回0表示不需要验证;如果不存在为其申请chain,并把其chain存在chain_hlocks[chain.base]中,返回1表示需要验证chain是否会产生死锁
    2.check_deadlock验证lock是否会导致进程当前的held_locks产生死锁
    3.check_prevs_add检查将lock加到chain中之后是否会因为chain上class之前出现的情形导致死锁
    	-如果是chain_head;递归读,或者第一种nest lock,将不会将lock加到locks_before/locks_after中并检查
    	-check_prev_add检查chain之前的因为irq产生的锁场景是否会产生死锁,如果不是try_lock只需要将进程一个prev添加关系就好了,因为prev和其之前锁的关系已经建立好了,通过bfs遍历整个graph时其他锁的关系自然会遍历到。
    		1.check_noncircular确定lock.locks_after上没有prev
    		2.check_prev_add_irq通过check_usage检查prev.locks_before和lock.locks_after是否存在(used,enable)这样的锁组合可能因为中断和当前的lock order冲突而导致死锁。
    		注:1.(读,写)出现死锁的逻辑是:bit检查只能确定有(next:enable_w,prev:uesed_r)的可能性,因为class丢失了instance的具体使用场景,所以当前的顺序可能是(prev: w; next: enable_w),这两者组合产生死锁。
    		       2.这里只检查了(写,写),(读,写)两种情况,如果以(读,写)出现死锁的逻辑来说,其他两种情况也应该都检查?
    		3.add_lock_to_list将next加到prev.locks_after链表中,将prev加到next.locks_before链表中
    */
    if (!validate_chain(curr, lock, hlock, chain_head, chain_key))
		return 0;
    
    /*更新进程所在的chain,已经其held_locks的深度*/
    curr->curr_chain_key = chain_key;
    curr->lockdep_depth++;
    ... ...
}

lockdep其他接口

lockdep_assert_held是一种防御型编程,表示在某种操作之前lock要被锁住。lockdep_pin_lock是上层为了检查在长调用链的应用场景中,底层是否有先unlock之后再回到上层的时候在lock。

/*如果lock没被锁上将报warn*/
#define lockdep_assert_held(l)	do {				\
		WARN_ON(debug_locks && !lockdep_is_held(l));	\
} while (0)


static int
__lock_release(struct lockdep_map *lock, int nested, unsigned long ip)
{
    ... ...
    prev_hlock = NULL;
    /*从后向前遍历进程held_locks数组*/
    for (i = depth-1; i >= 0; i--) {
        hlock = curr->held_locks + i;
        /*只处理一条chain*/
        if (prev_hlock && prev_hlock->irq_context != hlock->irq_context)
            break;
        /*看是否能找到lock*/
        if (match_held_lock(hlock, lock))
            goto found_it;
        prev_hlock = hlock;
    }
    ... ...
found_it:
    /*在__lock_pin_lock中递增pin_count,如果中途将其释放将会报warn*/
    WARN(hlock->pin_count, "releasing a pinned lock\n");
    /*递减referces,如果还存在nest lock,直接返回1*/
    if (hlock->references) {
		hlock->references--;
        if (hlock->references) {
            		return 1;
		}
	}
    /*如果lock被完全释放,更新lockdep_depth和chain*/
    curr->lockdep_depth = i;
    curr->curr_chain_key = hlock->prev_chain_key
    /*如果不是栈式释放锁,将会产生新的chain,所以需要重新验证是否会产生死锁*/
    for (i++; i < depth; i++) {
        hlock = curr->held_locks + i;
        if (!__lock_acquire(hlock->instance,
                            hlock_class(hlock)->subclass, hlock->trylock,
                            hlock->read, hlock->check, hlock->hardirqs_off,
                            hlock->nest_lock, hlock->acquire_ip,
                            hlock->references, hlock->pin_count))
            return 0;
    }

}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值