Linux 死鎖檢測模組 Lockdep 簡介

Lotte發表於2016-07-27

(伯樂線上注:本文來自作者 白浩文 的自薦投稿。)

死鎖概念

死鎖是指多個程式(執行緒)因為長久等待已被其他程式佔有的的資源而陷入阻塞的一種狀態。當等待的資源一直得不到釋放,死鎖會一直持續下去。死鎖一旦發生,程式本身是解決不了的,只能依靠外部力量使得程式恢復執行,例如重啟,開門狗復位等。

Linux 提供了檢測死鎖的機制,主要分為 D 狀態死鎖和 R 狀態死鎖。

  • D 狀態死鎖程式等待 I/O 資源無法得到滿足,長時間(系統預設配置 120 秒)處於 TASK_UNINTERRUPTIBLE 睡眠狀態,這種狀態下程式不響應非同步訊號(包括 kill -9)。如:程式與外設硬體的互動(如 read),通常使用這種狀態來保證程式與裝置的互動過程不被打斷,否則裝置可能處於不可控的狀態。對於這種死鎖的檢測 Linux 提供的是 hung task 機制,MTK 也提供 hang detect 機制來檢測 Android 系統 hang 機問題。觸發該問題成因比較複雜多樣,可能因為 synchronized_irq、mutex lock、記憶體不足等。D 狀態死鎖只是區域性多程式間互鎖,一般來說只是 hang 機、凍屏,機器某些功能沒法使用,但不會導致沒喂狗,而被狗咬死。
  • R 狀態死鎖程式長時間(系統預設配置 60 秒)處於 TASK_RUNNING 狀態壟斷 CPU 而不發生切換,一般情況下是程式關搶佔或關中斷後長時候執行任務、死迴圈,此時往往會導致多 CPU 間互鎖,整個系統無法正常排程,導致喂狗執行緒無法執行,無法喂狗而最終看門狗復位的重啟。該問題多為原子操作,spinlock 等 CPU 間併發操作處理不當造成。本文所介紹的 Lockdep 死鎖檢測工具檢測的死鎖型別就是 R 狀態死鎖。

常見錯誤

  • AA: 重複上鎖
  • ABBA: 曾經使用 AB 順序上鎖,又使用 BA 上鎖
  • ABBCCA: 這種型別是 ABBA 的擴充套件。AB 順序 , AB 順序,CA 順序。這種鎖人工很難發現。
  • 多次 unlock

AB-BA 死鎖的形成

假設有兩處程式碼(比如不同執行緒的兩個函式 thread_P 和 thread_Q)都要獲取兩個鎖(分別為 lockA 和 lockB),如果 thread_P 持有 lockA 後再去獲取 lockB,而此時恰好由 thread_Q 持有 lockB 且它也正在嘗試獲取 lockA,那麼此時就是處於死鎖的狀態,這是一個最簡單的死鎖例子,也即所謂的 AB-BA 死鎖。

下面接合時間軸來觀察死鎖發生的時機:

ABBA 死鎖示意圖 1ABBA 死鎖示意圖 1

X 軸表示程式 P 執行的時間軸,Y 軸表示程式 Q 執行的時間軸。

這幅圖依據兩個程式併發時間點不同而給出了 6 種執行線路:

  1. Q 獲得 B,然後獲得 A;然後釋放 B,然後釋放 A;此時 P 執行時,它可以獲得全部資源
  2. Q 獲得 B,然後獲得 A;此時 P 執行並阻塞在對 A 的請求上;Q 釋放 B 和 A,當 P 恢復執行時,它可以獲得全部資源
  3. Q 獲得 B,然後 P 執行獲得 A;此時 Q 阻塞在對 A 的請求上;P 阻塞在對 B 的請求上,大家都在互相等待各自的資源而死鎖
  4. P 獲得 A,然後 Q 執行獲得 B;此時 P 阻塞在對 B 的請求上;Q 阻塞在對 A 的請求上,大家都在互相等待各自的資源而死鎖
  5. P 獲得 A,然後獲得 B;此時 Q 執行並阻塞在對 B 的請求上;P 釋放 A 和 B,當 Q 恢復執行時,它可以獲得全部資源
  6. P 獲得 A,然後獲得 B;然後釋放 A,然後釋放 B;此時 Q 執行時,它可以獲得全部資源

下面這種情況是任何時間點都不會出現死鎖的

ABBA 死鎖示意圖 2ABBA 死鎖示意圖 2

lockdep 死鎖檢測模組

介紹了最簡單的 ABBA 死鎖的形成,回到正題,回到 kernel, 裡面有千千萬萬鎖,錯綜複雜,也不可能要求所有開發人員熟悉 spin_lock, spin_lock_irq, spin_lock_irqsave, spin_lock_nested 的區別。所以,在鎖死發生前,還是要做好預防勝於治療,防患於未然的工作,儘量提前發現並且提前在開發階段發現和解決這其中潛在的死鎖風險,而不是等到最後真正出現死鎖時給使用者帶來糟糕的體驗。應運而生的就是 lockdep 死鎖檢測模組,在 2006 年已經引入核心(https://lwn.net/Articles/185666/)。

1. 相關核心配置選項

  • CONFIG_PROVE_LOCKINGThis feature enables the kernel to report locking related deadlocks before they actually occur. For more details, see Documentation/locking/lockdep-design.txt.
  • CONFIG_DEBUG_LOCK_ALLOCDetect incorrect freeing of live locks.
  • CONFIG_DEBUG_LOCKDEPThe lock dependency engine will do additional runtime checks to debug itself, at the price of more runtime overhead.
  • CONFIG_LOCK_STATLock usage statistics. For more details, see Documentation/locking/lockstat.txt
  • CONFIG_DEBUG_LOCKING_API_SELFTESTSThe kernel to run a short self-test during bootup in start_kernel(). The self-test checks whether common types of locking bugs are detected by debugging mechanisms or not. For more details, see lib/locking-selftest.c

2. 基本實現

lockdep 操作的基本單元並非單個的鎖例項,而是鎖類(lock-class),事實上,也沒必要跟蹤千千萬萬的鎖,完全可以用同一方式對待同一類鎖的行為。比如,struct inode 結構體中的自旋鎖 i_lock 欄位就代表了這一類鎖,而具體每個 inode 節點的鎖只是該類鎖中的一個例項。

對於每個鎖的初始化,這段程式碼建立了一個靜態變數 (__key),並使用它的地址作為識別鎖的型別。因此,系統中的每個鎖 ( 包括 rwlocks 和 mutexes ) 都被分配一個特定的 key 值,並且都是靜態宣告的,同一類的鎖會對應同一個 key 值。這裡用得是雜湊表來儲存。

Lockdep 為每個鎖類維護了兩個連結串列:

  • before 鏈:鎖類 L 前曾經獲取的所有鎖類,也就是鎖類 L 前可能獲取的鎖類集合。
  • after 鏈:鎖類 L 後曾經獲取的所有鎖類。

Lockdep 邏輯:

當獲取 L 時,檢查 after 鏈中的鎖類是否已經被獲取,如果存在則報重複上鎖。聯合 L 的 after 鏈,和已經獲取的鎖的 before 鏈。遞迴檢查是否某個已經獲取的鎖中包含 L after 鎖。為了加速,lockdep 檢查鎖類順序關係,計算出 64bit 的 hash key。當新的 lock 順序出現則計算 hash key 並放入表中。當獲取鎖時,則直接掃描表,用於加速。

也由於上述的設計邏輯,不可避免會存在誤報。例如,同一類(對應相同 key 值)的多個鎖同時持有時,Lockdep 會誤報“重複上鎖”的警報。此時,你就需要使用 spin_lock_nested 這類 API 設定不同的子類來區分同類鎖,消除警報。

隨便找一個程式碼例子:

1)初始化

2)獲取鎖

3. 檢查規則

1)概述

Lockdep 操作的基本單元並非單個的鎖例項,而是鎖類(lock-class)。比如,struct inode 結構體中的自旋鎖 i_lock 欄位就代表了這一類鎖,而具體每個 inode 節點的鎖只是該類鎖中的一個例項。

lockdep 跟蹤每個鎖類的自身狀態,也跟蹤各個鎖類之間的依賴關係,通過一系列的驗證規則,以確保鎖類狀態和鎖類之間的依賴總是正確的。另外,鎖類一旦在初次使用時被註冊,那麼後續就會一直存在,所有它的具體例項都會關聯到它。

2)狀態

鎖類有 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 檢測鎖的安全規則時,會在 log 中提示對應的狀態位資訊

比如:

注意大括號內的符號,一共有 6 個字元,分別對應 STATE 和 STATE-read 這六種(因為目前每個 STATE 有 3 種不同含義)情況,各個字元代表的含義分別如下:

  • ’.’ 表示在在程式上下文,在 irq 關閉時獲得一把鎖
  • ’-‘ 表示在中斷上下文,獲得一把鎖
  • ’+’ 表示在 irq 開啟時獲得一把鎖
  • ’?’ 表示在中斷上下文,在 irq 開啟時獲得一把鎖

3)單鎖狀態規則(Single-lock state rules)

  • 一個軟中斷不安全 (softirq-unsafe) 的鎖類也是硬中斷不安全 (hardirq-unsafe) 的鎖類。
  • 對於任何一個鎖類,它不可能同時是 hardirq-safe 和 hardirq-unsafe,也不可能同時是 softirq-safe 和 softirq-unsafe,即這兩對對應狀態是互斥的。

上面這兩條就是 lockdep 判斷單鎖是否會發生死鎖的檢測規則。

關於四個名稱的概念如下 :

  • ever held in hard interrupt context (hardirq-safe);
  • ever held in soft interrupt context (softirg-safe);
  • ever held in hard interrupt with interrupts enabled (hardirq-unsafe);
  • ever held with soft interrupts and hard interrupts enabled (softirq-unsafe);

4)多鎖依賴規則(Multi-lock dependency rules)

  • 同一個鎖類不能被獲取兩次,否則會導致遞迴死鎖(AA)。
  • 不能以不同的順序獲取兩個鎖類,即:

是不行的。因為這會非常容易的導致 AB-BA 死鎖。當然,下面這樣的情況也不行,即在中間插入了其它正常順序的鎖也能被 lockdep 檢測出來:

  • 同一個鎖例項在任何兩個鎖類之間,巢狀獲取鎖的狀態前後需要保持一致,即:

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

所以要注意巢狀獲取鎖前後的狀態需要保持一致,避免死鎖風險。

5) 出錯處理

當檢測到死鎖風險時,lockdep 會列印下面幾種型別的風險提示,更完整的 LOG 會在下面例子中展示。

  • [ INFO: possible circular locking dependency detected ] // 圓形鎖,獲取鎖的順序異常(ABBA)
  • [ INFO: %s-safe -> %s-unsafe lock order detected ] // 獲取從 safe 的鎖類到 unsafe 的鎖類的操作
  • [ INFO: possible recursive locking detected ] // 重複去獲取同類鎖(AA)
  • [ INFO: inconsistent lock state ] // 鎖的狀態前後不一致
  • [ INFO: possible irq lock inversion dependency detected ] // 巢狀獲取鎖的狀態前後需要保持一致,即 [hardirq-safe] -> [hardirq-unsafe],[softirq-safe] -> [softirq-unsafe] 會警報死鎖風險
  • [ INFO: suspicious RCU usage. ] // 可疑的 RCU 用法

4. 使用例項

Lockdep 每次都只檢測並 report 第一次出錯的地方。

只報一次死鎖風險列印提示就不報了,因為第一個報出來的可能會引發其他的風險提示,就像編譯錯誤一樣。並且,這只是一個 warning info, 在實時執行的系統中,LOG 可能一下子就被沖掉了。本著魅族手機對使用者體驗極致的追求,不允許任何一個死鎖風險在開發階段僥倖存在,我們會把 lockdep warning 轉化為 BUG_ON(),使機器在遇到死鎖風險就主動重啟來引起開發人員的關注,從而不放過每一個可能存在的漏洞。

下面是實際開發中遇到 lockdep 報的死鎖風險 LOG:

從上面的 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。再來看看可能發生死鎖的情景:

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 就可以了。保持巢狀獲取鎖前後的狀態一致。

參考資料

  1. 《Operating systems : internals and design principles / William Stallings. — 7th ed.》
  2. 核心文件 lockdep-design.txt
  3. 死鎖檢測模組 lockdep 簡介
  4. Method and system for a kernel lock validator
  5. The kernel lock validator

相關文章