死鎖檢測lockdep實現原理

死鎖在編程中是再常見不過的錯誤了,和內存泄露一樣是很難避免的問題,Ingo Molnar發明了lockdep用來檢測死鎖,它將問題產生的場景進行了歸納總結,避開了對鎖進行單個追蹤的方式來調試問題而是使用另外一種smart的方式,它不再處理單個鎖而是處理鎖類.他不僅適用於普通的自旋鎖,還可以用在mutex,rwlock,rcu中.
目前這個功能的調試只適合在開發環境下,不適合在生產環境下,它在持鎖和釋放鎖的過程中做了非常多的檢查工作,並且它的實現中使用了一些全局表的操作,非常耗性能的.可以在開發階段設置死鎖觸發的場景下進行重啓,當然這需要有能力進行kdump,服務器環境下還是通過syslog記錄死鎖的堆棧.它的堆棧提示非常友好,基本上不需要使用crash這類工具進行深度分析就可以很快還原問題,推薦開發階段使用.

死鎖場景

死鎖場景有:AA鎖,ABBA鎖。
其中AA鎖場景又分爲簡單重複上鎖和上下文切換引起的上鎖,前者不必多說,而後者可能是鎖使用場景可能有軟中斷和進程上下文,但是它使用的普通的spin_lock/spin_unlock而不是spin_lock_bh/spin_unlock_bh版本的,在進程上下文臨界區中被中斷打斷,中斷退出後進入軟中斷,此時就形成了AA死鎖.
而ABBA鎖是獲取鎖順序不一致導致的死鎖,如下:

thread_P() {
    spin_lock(&lockA);
    spin_lock(&lockB);

    spin_unlock(&lockA);
    spin_unlock(&lockB);
}

thread_Q() {
    spin_lock(&lockB);
    spin_lock(&lockA);

    spin_unlock(&lockB);
    spin_unlock(&lockA);
}

lockdep的原理

1.lockdep不再單獨追蹤每個鎖,而是一類鎖,例如inode->i_lock,內核中有很多的inode對象,但是他們共享同樣一個lock_class對象,通過在spin_lock系列的api內部改造來使得開發者對此無感. inode對象通常都會使用inode_init_always或類似的接口來進行初始化,當第一次執行的時候會創建一個局部靜態變量,後續對象初始化的時候會沿用靜態變量,實現公用一個鎖類.
2.每個鎖類維護了一個before和after鏈表
before 鏈:鎖類 L 前曾經獲取的所有鎖類,也就是鎖類 L 前可能獲取的鎖類集合.
after 鏈:鎖類 L 後曾經獲取的所有鎖類.
當嘗試獲取鎖L,它會檢查在當前獲取的鎖棧中是否已經持有過該鎖;此外還檢查它的before鏈和after鏈中是否有重疊,也就是一個鎖既在before中,也在after中
3.持有鎖的上下文是多樣的,但是有些上下文是不能混用的,就如上面所說的軟中斷上下文和進程上下文中使用spin_lock/spin_lock_bh兩個版本的鎖api造成的死鎖問題,在這裏同樣會檢查鎖的狀態.
鎖類有 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的實現

下面主要通過數據結構之間的聯繫來展示它的實現過程
lockdep datastructure
lock_class:lockdep中的核心結構,維護了鎖的before和after結構,就是鎖之間的依賴關係.另外還通過鏈表結構維護,可以進行遍歷操作,通過hash表結構進行查找操作,裏面還記錄鎖的ip,可以通過kallsym翻譯成可讀形式的符號.
lock_list:lock_class的before/after鏈表上掛的數據結構,主要是關聯鎖類lock_class信息,形成一對多的關係,遍歷before/after鏈表時找到lock_class對象.
held_lock:進程的鎖棧上記錄,每一個獲取鎖的操作就會在current進程信息中添加一個held_lock信息,目前最多支持48層鎖棧,之後會嘗試將鎖棧上的鎖加入到lock_class的before/class鏈表中,其中會進行檢查.其中當中斷髮生時,會保存進程上下文,之後會進入中斷處理,所以在中斷上下文中使用的也是進程上下文的進程held_lock記錄數組,在處理的時候會判斷是否從進程上下文切換到中斷上下文,當切換的時候只會負責相同上下文的held_lock信息.
lock_chain:記錄當前的lock是否已經和鎖L進行了關聯,lock_chain_get_class可以通過該數據結構的可以找到lock_class.如果已經關聯過了,則略過.否則會添加依賴關係,在此過程中會檢查是否會產生死鎖.
lockdep_map:每個鎖額外的數據結構,可能是多個鎖實例指向同一個鎖類數據結構

檢查規則

以下摘自內核的Documentation/locking/lockdep-design.txt

單鎖狀態檢查(Single-lock state rules):
1.一個軟中斷不安全(softirq-unsafe)的鎖類同樣也是硬中斷不安全(hardirq-unsafe)的。
2.對於任何一個鎖類,它不可能同時是hardirq-safe和hardirq-unsafe,也不可能同時是softirq-safe和softirq-unsafe,即這兩對對應狀態是互斥的。
多鎖依賴規則(Multi-lock dependency rules):
1.同一個鎖類不能被獲取兩次,因爲這會導致遞歸死鎖。
2.不能以不同的順序獲取兩個鎖類,即如此這樣是不行的:

<L1> -> <L2>
<L2> -> <L1>

因爲這會非常容易的導致本文最先提到的AB-BA死鎖。
3,同一個鎖實例在任何兩個鎖類之間不能出現這樣的情況:

<hardirq-safe>   ->  <hardirq-unsafe>
<softirq-safe>   ->  <softirq-unsafe>

這意味着,如果同一個鎖實例,在某些地方是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.

validate_state

鎖類lock_class的成員usage_mask中記錄着迄今爲止鎖使用的上下文,當嘗試持有鎖的時候會檢查是否有不同上下文混用的情況.
通過mark_lock接口在lock_acquire路徑中進行更新它,除了4*n+1種狀態還有讀/寫兩種方向用於讀寫鎖嵌套的場景.
下面是usage_mask中成員的註釋:

     * bit 0 - write/read                          
     * bit 1 - used_in/enabled                                                                              
     * bit 2+  state

我們通過enum lock_usage_bit的成員可以看到,每個STATE都是由4種情況組成,即低2位代表4中情況,第3和4位代表3種狀態.validate_state主要檢查不同STATE上下文的混用,也就是irq-safe/irq-unsafe這種操作.

    LOCK_USED_IN_SOFTIRQ, LOCK_USED_IN_SOFTIRQ_READ, LOCK_ENABLED_SOFTIRQ, LOCK_ENABLED_SOFTIRQ_READ, LOCK_USED_IN_RECLAIM_FS, 
    LOCK_USED_IN_RECLAIM_FS_READ, LOCK_ENABLED_RECLAIM_FS, LOCK_ENABLED_RECLAIM_FS_READ, LOCK_USED, LOCK_USAGE_STATES}

validate_chain

1.遍歷當前進程held_lock數組,和當前要持有的held_lock信息進行比對.
2.在比對過程中,查找兩個held_lock是否可能產生環,會遍歷其中一個held_lock的before/after鏈,並迭代鏈上的lock_class形成一棵樹來查找是否有符合的lock_class.
3.它主要檢查兩種情形:鎖之間的依賴關係是否會形成死鎖,鎖的STATE是否合法,即硬中斷和軟中斷的幾種狀態是否進行了混用.

	validate_chain
		lookup_chain_cache  //即將獲得的lock和上一次的lock是否已經通過lock_chain關聯,沒有關聯就進行下面的關聯,否則直接返回
		check_deadlock	//檢查當前task_struct的held_locks棧是否有AA鎖
		check_prevs_add	//添加依賴關係
			for (;;) {
				hlock = curr->held_locks + depth - 1;	//遍歷當前held_locks的棧,添加和next的關聯關係
				check_prev_add(hlock, next..)
				depth--;
			}

check_prev_add的邏輯:

check_prev_add
	check_noncircular(&this, hlock_class(prev), &target_entry);	//檢查是否形成環形
	check_prev_add_irq	//檢查上下文是否一致
    add_lock_to_list(hlock_class(prev), hlock_class(next),                    //next lock添加到prev的locks_after鏈表上
                   &hlock_class(prev)->locks_after);                                                                                                                                      
    add_lock_to_list(hlock_class(next), hlock_class(prev),                    //prev lock添加到next的locks_before鏈表上           
                   &hlock_class(next)->locks_before);

lockdep nest鎖:

同一類(對應相同 key 值)的多個鎖同時持有時,Lockdep 會誤報“重複上鎖”的警告。這類操作中典型的是dentry層級遍歷時,dentry共享同一個鎖類,當遍歷A目錄下的B,C,D文件,因爲在A層級已經拿到過鎖了,所以在B,C,D中嘗試拿鎖時lockdep會以爲是重複的AA鎖.此時就需要使用spin_lock_nested 這類 API 設置不同的子類來區分同類鎖,消除警告。

lockdep日誌的分析

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種不同含義)情況,各個字符代表的含義分別如下:

‘.’ acquired while irqs disabled and not in irq context
‘-‘ acquired in irq context
‘+’ acquired with irqs enabled
‘?’ acquired in irq context with irqs enabled.

參考:

內核文檔:Documentation/locking/lockdep-design.txt
魅族團隊的文檔:http://kernel.meizu.com/linux-dead-lock-detect-lockdep.html
lenky的博客:http://www.lenky.info/archives/2013/04/2253
United States Patent 8145903:Method and system for a kernel lock validator
http://www.freepatentsonline.com/8145903.html

但是目前很少見有人用它的,畢竟在用戶層的併發場景比較少,不像內核中公用相同的地址空間,不同的上下文切換,迄今爲止在工作中還沒有遇到使用lockdep的應用層框架,有時間了可以再去看一下它的使用場景.

應用層的lockdep:https://lwn.net/Articles/536363/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章