最大處理吞吐量自然需要並行並充分利用可用的處理資源。在高效能的並行應用中,僅允許單一執行緒在任何時刻執行的關鍵區段 (critical section,以下簡稱 CS) 相當常見。現代作業系統提供簡單的鎖定機制,以強制執行單一執行緒的限制。兩種常見的鎖類型為 spinlock 與 mutex。每種鎖都能對 CS 形成屏障,從而維護程式的正確性。然而,spinlock 與 mutex 在內部的實作方式截然不同,這在多種常見的工作負載下導致顯著的效能差異。
spinlock 在單處理器且不支援搶佔式排程的作業系統上,其實沒作用,只有在多處理器環境下才有其價值。最基本的 spinlock 是所有執行緒無序搶奪同一鎖的那種,但由於所有處理器上的執行緒都在等待同一個變數,這會導致所有爭鎖處理器的快取 (cache) 被頻繁刷新。為了解決這個問題,設計者引入新的 spinlock 機制:讓每個執行緒在本地處理器的變數上自旋,避免快取的廣泛刷新。這類 spinlock 的第一個實作是陣列鎖,但陣列鎖需靜態分配固定大小,若爭鎖的執行緒較少,便造成空間浪費。於是出現第二種實作 —— 佇列鎖。佇列可依需求動態擴展,解決陣列鎖空間利用率低的問題。
雖然這類新型 spinlock 避免快取刷新,同時也減少空間浪費,但卻引發飢餓問題。排在佇列後方的執行緒,因等待時間過長,可能導致反應能力下降。為此,引入了具有超時機制的鎖,以避免執行緒長時間自旋導致系統效能降低。總的來說,只要 spinlock 的佇列過長,就有可能產生飢餓現象。那麼,有沒有辦法限制佇列長度呢?答案是肯定的,這就是「複合鎖」。複合鎖綜合超時鎖與佇列鎖的優勢,並引入新的競爭機制 —— 類似 access token 的概念。也就是說,只有獲得 access token 的執行緒才可進入佇列爭奪 spinlock。access token 數量有限,因此成功限制佇列長度。若執行緒拿到 access token 後,在佇列中等待超時,將自動放棄 access token 並拋出例外,由呼叫者決定後續處理。
然而,這樣的設計帶來一個問題:本來是爭奪 spinlock,現在變成爭奪 access token,這是否失去原意?實際上,爭奪 access token 是為了讓 spinlock 的競爭過程更公平,進而避免飢餓。既然目標是避免飢餓,那麼打破先來先服務的順序是必要的。access token 的競爭過程實際上就是一種打破先來先服務的策略,並藉由簡單的退避演算法來控制爭奪 access token 的開銷,避免對系統造成過大負擔。若單純為了解決飢餓問題,timedlock 已相當實用;但複合鎖進一步強化公平性,這種公平性正是透過打破先來先服務所實作的。
在服務台排隊問題上,超市的處理實不理想,常常發生隊頭擁塞的情況。試想,目前方結帳發生糾紛,導致後方的我們只能乾等,看著其他結帳隊伍快速前進。這種排隊方式雖然簡單,卻將「排隊排程」的任務交給顧客自己。顧客需自行判斷排哪條佇列較快,如觀察購物量、有無衣物 (卡片糾紛風險) 、是否有需稱重商品、有無打折商品、顧客年齡、收銀員熟練度等,這完全是一場賭博。
相比之下,銀行或餐廳的排隊就合理許多,顧客抽號碼,排入單一佇列,由空閒的服務台叫號,屬於「單一佇列、多服務台」的排程系統,有效避免隊頭擁塞。顧客只需持票等待,無需實體排隊,票號形成虛擬佇列,沒叫號前可自由活動。
尤其在業務處理快速的情況下,一旦離開場館,剛踏出門就被叫號,只得匆忙返回,反倒不如不離開。若等待時間較長,則可完成其他任務,若收益高於體力成本,離場即有意義。
聯想到 semaphore。它就像是「單一佇列、多服務台」的系統,初始值即服務台數。當一執行緒獲服務,相當於服務台減一 (down
) ,而服務完成後則服務台數加一 (up
) 。「服務」即進入臨界區。
問題是,Linux semaphore 採用 sleep-wait,對我的場景來說切換開銷過高,perf
顯示大量時間耗在 schedule
與 wake_up
。頻繁切換導致 cache 失效,性能下滑。雖然 CFS 排程機制可調整 jitter,但損耗仍不可忽視。
Linux 的 ticket lock,所有爭鎖者與持鎖者共用同一變數,導致多處理器 cache coherence 開銷巨大。嘗試設計「本地接力 spinlock」,讓每個爭鎖者只觸碰自己專屬的變數,且避免快取行 (cache line) 衝突,釋放鎖時僅寫入下一個爭鎖者的本地變數,降低 cache coherence 需求。
結合 spinlock 與超市排隊經驗,萌生出一個可讓多 CPU 同時持有鎖的 自旋佇列構想。後來發現,這不正是 semaphore?但 Linux 的 sleep-wait semaphore 不足以滿足需求,這裡需要 spin-wait 版本,因為資料包傳送非常快速。semaphore 理應適用,但不願 sleep,故採用多 spinlock 機制。結果顯示,spin-wait semaphore效果優異。
為凸顯主體概念,以下簡化 semaphore 實作,忽略細節:
註:上述簡化程式碼忽略 semaphore 內部的 spinlock,實際情況中需防止多服務台同時操作同一等待者。
以下為改造版,自旋等待 semaphore:
所有函式與結構體均加上 spin_
前綴,標示自旋版本的 semaphore。
以上程式碼實作出通用自旋等待佇列,而 spinlock 則是其中「單服務台」的特例。
鎖的開銷是巨大的,尤其是在多核處理器環境下更為明顯。
引入多處理的目的是提升並行處理能力,以增強系統效能。然而,由於共享臨界區 (critical section) 的存在,同一時間內只能允許一個執行緒存取 (特別是寫操作) ,導致本應並行執行的工作流在這裡被序列化 (serialization) 。形象地來看,這就像是一條寬闊的馬路上出現了一個瓶頸,而這種序列化是無法避免的,因為它是鎖機制的本質。問題在於,執行緒該如何應對這個瓶頸?顯然,誰都無法繞開它,關鍵在於當它們同時到達瓶頸時該怎麼辦。
一種最直接但粗暴的做法是「鬥毆式搶鎖」,這也是最簡單的自旋鎖 (spinlock) 設計方式。然而,一種更為溫和的策略則是:既然無法立即取得鎖,那就主動讓出 CPU (yield),進入睡眠狀態 (sleep) ,等待鎖釋放後再恢復執行。這就是睡眠等待 (sleep-wait) 機制,而相對應的方式則是持續忙碌等待 (spin-wait) 。問題是,執行緒本身該如何在這兩種方式之間做出最佳選擇?事實上,這種選擇的權限不在執行中的執行緒,而是在設計程式時的開發者手中。
不應簡單比較 sleep-wait 和 spin-wait 哪種效能更好,因為鎖本身就是一種效能損耗,無論哪種方式都會影響系統效能,只是影響方式不同:
為何仍然要引入 spin-wait?因為若始終使用 sleep-wait,當執行緒 A 進入睡眠,系統切換到執行緒 B,而此時鎖被釋放,系統無法保證執行緒 A 立即獲得 CPU,即使它被喚醒,仍需付出額外的切換開銷。而若鎖的釋放時間極短,那麼這樣的切換代價可能得不償失。因此,在某些情況下,持續忙碌等待 (spin-wait) 可能更為合理。
與中斷 (interrupt) 機制相比,鎖的開銷更大。若能夠透過關閉中斷 (disable interrupts) 來實作無鎖操作,那麼這是值得考慮的選項。但是,關閉中斷也會帶來副作用,例如增加系統延遲,因此需要權衡關閉中斷與鎖的開銷,評估其可行性。
自 Linux 核心初始版本開始,就已經引入自旋鎖。自旋鎖的行為模式是在等待鎖的過程中「原地打轉」,這導致 CPU 週期的浪費,但這是一種必要的取捨。系統設計本質上是一場權衡 (trade-off) ,雖然不是零和博弈,但始終沒有完美方案,只能努力尋找折衷點。
若不使用自旋鎖,應該怎麼做?很顯然,選擇的方法就是將執行緒切換至其他工作,等待持鎖者釋放鎖後再喚醒。然而,這樣會帶來額外開銷:
因此,若忙碌等待所浪費的 CPU 週期小於兩次執行緒切換的開銷,那麼自旋等待就是合理的。這也是為什麼自旋鎖適用於短時間持有鎖的臨界區,因為:
Linux 的自旋鎖經歷二個世代的演化:
Ticket 自旋鎖的設計非常巧妙,它將一個 32 位變數拆分為高 16 位 (next ticket) 和低 16 位 (current ticket) :
這樣的設計確保了 FIFO (先進先出) 公平性,避免了第一代自旋鎖中存在的隨機爭搶問題。
目前在使用自旋鎖時,一般會先進行一次 try-lock 嘗試:
然而,這種 try-lock 只會返回成功或失敗,但沒有提供額外資訊。例如,它可以返回:
這樣,呼叫者就能根據這些資訊決定是否應該:
此外,這些統計資訊還可以用於動態改進自旋鎖,例如:
雖然 Linux 核心的自旋鎖機制已經相當成熟,但仍有許多可以改進的空間,尤其是在大規模多核心系統下,如何更有效率地管理 CPU 資源,將是未來鎖機制設計的關鍵方向。
很多時候,當一個行程剛進入睡眠狀態等待 mutex 時,mutex 可能已經被釋放。若能夠即時感知到 mutex 被釋放,將是最理想的情況。解決這個問題的方式之一,是使用自旋忙碌等待 (spin-waiting) 而非阻塞等待 (blocking wait),這樣的理解是否正確?
最初,自旋鎖 (spinlock) 是為了解決頻繁睡眠與喚醒帶來的效能問題。然而,在即時系統 (real-time systems) 中,自旋鎖可能導致其他行程長時間延遲,進而影響整體吞吐量 (throughput) 。因此,為了避免這種情況,即時系統又回到了睡眠/喚醒 (sleep/wakeup) 機制,因為即時系統的首要目標不是節省開銷,而是確保執行穩定性。睡眠/喚醒機制確保了爭搶鎖的行程不會影響到其他行程,從而維持系統吞吐量。然而,以 mutex 實作的自旋鎖仍然存在一定的代價,因為它必須支援優先權繼承 (priority inheritance protocol, PIP) 或優先權提升 (priority boosting),以避免優先權反轉 (priority inversion) 與死鎖 (deadlock)。
在可搶占 (preemptive) 核心環境下,若高優先權的行程準備執行,它將會搶占低優先權行程。若低優先權行程持有某個鎖,而此鎖正被高優先權行程等待,則會導致高優先權行程被阻塞,尤其是在單 CPU 環境或行程與 CPU 綁定的情況下。此外,若此時一個中等優先權的行程介入並搶占了低優先權行程,則可能導致高優先權行程的阻塞時間過長,這在即時系統中是不可接受的。因此,Linux 核心引入優先權繼承協定,但該協定的實作需要額外的資料結構與演算法管理,這會增加系統開銷,抵消部分因睡眠/喚醒機制提升吞吐量而獲得的效益。因此,在不同方案之間取得平衡成為關鍵問題。
Linux 核心的自適應自旋鎖設計確保:
這段程式碼展示 Linux 核心如何判斷是否應該繼續自旋:
mutex_spin_on_owner
的邏輯如下:
這段程式碼說明 Linux 核心如何透過兩層自旋來改進鎖爭搶的過程。
以下改寫自 spinlock 前世今生
軟體通常會極致地配合硬體,盡最大努力最佳化效能。圍繞著快取所做的最佳化非常多,而我們就以一個點作為切入,探究 spinlock 的前世今生。spinlock
是 Linux 核心中常見的互斥操作,適用於不可睡眠的場景,可以說是最基礎的同步操作。不知你是否研究過 spinlock 的進化史?從最初的 wild spinlock
到 ticket spinlock
,再到今天的 qspinlock
,可謂是一波三折,大起大落!那麼,spinlock
究竟是怎麼一步一步發生蛻變的?又是為什麼要發生這些變化?背後究竟是什麼力量推動著自身的變革呢?
Linux 核心原始程式碼就像是一部歷史,以下藉由探討快取切入來 spinlock 的前世今生,嘗試探索 spinlock 和快取之間的恩怨情仇。
首先來介紹一下這位主角,spinlock (自旋鎖) 是一種互斥的操作,用在不能進行睡眠的行程間對共享資料進行互斥的存取。任一時刻只能有一個行程能夠取得鎖,其它無法取得 spinlock 的行程則會在原地自旋,持續等待直到獲得鎖。若讓你實作一個 spinlock,會不會覺得這件事看起來好像挺簡單的?
看起來是不是很簡單。上面的例子中 spinlock 的 locked
成員是 1 代表 locked。若是 0 代表鎖是釋放狀態。spin_lock()
保證在不能獲得鎖的情況下一直 spin 等待。不過這裡有個小問題需要修改下,在 spin_lock()
中判斷 locked
是 0 的同時需要將 locked
成 1,這整個過程必須保證是 atomic 的操作。所以我們需要一道 atomic 指令。我們假設 test_and_set()
就是一道 atomic 指令,測試一個變數的值,然後置為 1。返回值是變數的舊值。這一系列操作是由硬體提供的 atomic 指令實作的。
我們假設系統有 8 個 CPU。假設 8 個 CPU 同時申請 spinlock
,CPU0 成功持有鎖。想一下 CPU1–CPU7 在做什麼?由於 test_and_set
是無條件寫入 1 的操作。所以 CPU1–CPU7 一直在寫變數。根據多核 cache coherency protocol 中的 MESI protocol,我們知道修改一個變數時,這個變數必須在快取行中,且狀態是 E 或 M。所以當 8 個 CPU 同時申請 spinlock 時會發生以下狀況。
invalid
訊息給其他 CPU,然後修改變數為 1。invalid
訊息給 CPU1,然後修改變數。invalid
訊息給 CPU2,然後修改變數。這就無形之中增加了頻寬壓力,使效能下降。我們既要 spin 又想避免 cache thrashing 導致的頻寬壓力和效能損失。我們知道快取行有一種狀態是 shared
,可以在多個 CPU 之間共享,而不用發送 invalid
訊息,而且 CPU1–CPU7 一直在 spin,修改變數也沒有意義,只有 CPU0 unlock
的時候,修改才有價值。所以我們對 spin_lock()
修改如下。
這樣 CPU1–CPU7 會一直保持 Shared
狀態,沒有 cache thrashing。這種最佳化在 Linux 核心程式碼中經常可見,例如這裡,當你看到這種奇怪的寫法時,不要覺得奇怪。當 spin_unlock()
時,這種 Shared
狀態會被打破。但是這也足以在一定程度上提升效能。
好景不常,我們發現了新的問題。某些等待的 CPU 可能會有 starvation 現象。例如,CPU1–CPU7 在原地 spin,CPU0 釋放鎖的時候,CPU1–CPU7 哪個 CPU 的 cache 先看到 locked
的值,就會先獲得鎖。所以不排隊的機制可能導致部分 CPU "餓死"。為了解決這個問題,我們從 wild spinlock
跨度到 ticket spinlock
。
歷史推進,我們導入 FIFO 排隊機制,按申請順序取得鎖,確保公平性。
spin_lock()
中的 xadd()
是一個會將變數加 1 的 atomic 操作,並返回變數之前的值。在這一版實作中,我們可以確定當 owner
等於 next
時,代表鎖是釋放狀態。否則,代表鎖被其他人持有。next
就像是排隊拿票的機制,每來一個申請者,next
就加 1,代表票號;owner
代表目前持鎖的票號。
看起來一切都完美了,但是現實往往是殘酷的。接下來又遇到什麼問題了呢?
我們依然以上面 8 個 CPU 的系統為例說明。CPU0–CPU7 依次申請 spinlock
。假設初始狀態,spinlock
變數的結構體沒有快取到任何 CPU 的快取內。CPU0 獲取 spinlock
,此時的 cache 狀態如下。
然後 CPU1 申請鎖,並更新 spinlock 變數。所以會 invalid
CPU0 的 cache。接著 spinlock 變數被快取到 CPU1 的 cache,然後 CPU1 開始一直 spin,持續讀取 spinlock 的值。
接著 CPU2 申請鎖,並更新 spinlock 變數。所以會 invalid
CPU1 的 cache,然後更新 next
的值。而 CPU1 又會讀取 spinlock 的值,所以 spinlock 變數對應的快取行的狀態最終是 Shared
,並且同時快取在 CPU1 和 CPU2 的 cache 中。
後面 CPU3–CPU7 依次申請 spinlock
,就是重複上面的操作:在更新 next
之前,先 invalid
其他 CPU 的 cache,然後其他 CPU 再從目前 CPU 獲取更新後的 spinlock 值。cache line 的狀態更新為 Shared
,然後繼續 spin。
當 CPU0 spin_unlock()
的時候,會先 invalid
CPU1–CPU7 對應的 cache line,然後更新 owner
的值。
CPU1–CPU7 讀取 owner
的值時,又會從 CPU0 獲取最新資料,並快取到各自的 cache 中。
不知道你是否發現了問題:隨著 CPU 數量的增加,總線頻寬壓力變得非常大。而且延遲也會隨之上升,效能逐漸下降。更重要的是,當 CPU0 釋放鎖後,CPU1–CPU7 中其實只有一個 CPU 能獲得鎖 —— 根本沒必要影響所有 CPU 的快取,只需要影響接下來按 FIFO 順序該獲得鎖的那個 CPU 就行。
這說明 ticket spinlock
並不是 scalable 的 (最初的 wild spinlock
也有相同問題) 。於是,歷史又往前邁出了一步。
我們來到了 qspinlock
的時代,qspinlock
的出現正是為了解決 ticket spinlock
上述的問題。
我們先來思考一下造成這個問題的根本原因,就是每個 CPU 都在 spin 同一個共享變數 spinlock 上,導致頻寬壓力大、延遲高、效能差。所以,只要每個 CPU spin 的不是同一個變數,而是各自不同的變數,就可以有效避免這種情況。
這就需要我們換一種排隊方式,例如使用單向鏈結串列。單向鏈結串列一樣可以保證 FIFO 順序,每次解鎖時,也只需要通知鏈結串列中頭部的 CPU 即可。這,其實就是 MCS lock 的實作原理。而 qspinlock
的實作,就是建立在 MCS lock 的理論基礎上。
接下來,我們先探究一下 MCS lock 是如何實作的。
mcs_spinlock
中 next
成員就是構建單向鏈結串列的基礎,所有 spin
自旋的 CPU 都可以藉助 mcs_spinlock
結構體構建立單向鏈結串列關係。mcs_spinlock
結構體需要幾個呢?我們知道 spin_lock()
期間,搶佔是關閉的,所以最多只可能存在 CPU 數量減 1 個 CPU 自旋等待,所以我們只需要 CPU 個數的 mcs_spinlock
結構體 (spinlock
可能會出現巢狀,例如 softirq
可以中斷行程,hardirq
可以中斷 softirq
,NMI
可以中斷 hardirq
。這意味著一個 CPU 可能出現 4 個不同的 spinlock 自旋等待,預期 mcs_spinlock
結構體需要 4 * CPU
數量。後面為了簡化問題不考慮單核 spinlock 巢狀情況)
這就很適合使用 percpu
變數。spin
等鎖的操作只需要將所屬自己 CPU 的 mcs_spinlock
結構體加入單向鏈結串列尾部,然後 spin
,直到自己的 mcs_spinlock
的 locked
成員置 1 (locked
初始值是 0) 。unlock
的操作也很簡單,只需要將解鎖的 CPU 對應的 mcs_spinlock
結構體的 next
指標的 locked
成員設 1,相當於通知下一個 CPU 退出迴圈。
我們繼續以上面的 8 個 CPU 的系統為例說明。首先 CPU0 申請 spinlock 時,發現鏈結串列是空的,且鎖是釋放狀態。所以 CPU0 獲得鎖。
CPU1 繼續申請 spinlock
,需要 spin
等待。所以將 CPU1 對應的 mcs_spinlock
結構體加入鏈結串列尾部。然後 spin
等待 CPU1 對應的 mcs_spinlock
結構體 locked
成員被置 1。
當 CPU2 繼續申請鎖時,發現鏈結串列不為空,說明有 CPU 在等待鎖。所以也將 CPU2 對應的 mcs_spinlock
結構體加入鏈表尾部。
當 CPU0 釋放鎖的時候,發現 CPU0 對應的 mcs_spinlock
結構體的 next
域不為 NULL
,說明有等待的 CPU。然後將 next
域指向的 mcs_spinlock
結構體的 locked
成員置 1,通知下個獲得鎖的 CPU 退出自旋。MCS lock
頭指標可以選擇不更新,等到 CPU2 釋放鎖時再更新為 NULL
。
以上只是一個簡單的參考,MCS lock 的具體程式碼細節可參考 kernel/locking/mcs_spinlock.h。如何將 mcs_spinlock
頭指標塞進 spinlock 4 個位元組的身軀,成為實作 qspinlock
的關鍵。不過這部分可以參考 Linux 核心原始程式碼,掌握了其原理,看程式碼實作應該易如反掌。透過以上步驟,我們可以看到每個 CPU 都 spin
在自己的私有變數上,因此不會存在 ticket spinlock
的問題。
從 wild spinlock
到 qspinlock
,程式碼的複雜度增加的不是一點半點,而是很多。qspinlock
的實作檔案足足超過 500 行,而 wild spinlock
僅僅幾十行。
在這篇文章中,我給出了一個拯救 panic 的方法,其目的更多事惡作劇性質。但仍有不足,請看下面這段程式碼中的註解
若持有自旋鎖的 task 被 schedule 出去,由於該 task 不會再回來了,那麼只要另一個 task 在搶鎖,系統馬上就會發生 deadlock。
需求自然就出來了,我能不能在呼叫 schedule 之前便利系統目前所有自旋鎖的持有情況,將自己持有的自旋鎖給 unlock 呢?
有需求就有方案。當然可以。
大秀 hook 手藝的時候來了。雖然作為手藝人用手工的方式拼接指令可以實作任何功能,但現在的目標已經不僅僅是秀手藝了,而是實作上述的需求,所以我盡量先使用 stap/kprobe,ftrace 這些場面還不是太宏大的工具。
需求如圖所示:
很顯然,直接的思路就是使用 kretprobe 了,撰寫以下程式碼:
請忽略具體的 spinlock 統計邏輯,現在僅僅關注框架,我敢說,這個玩法對於一般的 wrap
函數,簡直就是模板:
wrap
函數。所謂的 wrap 函式其實很簡單:
kretprobe
的處理流程如下:
然而,它偏偏不能用於 _raw_spin_lock
/_raw_spin_unlock
的 wrap
!因為會死鎖!
kprobe
/kretprobe
框架內部使用 spinlock 來進行同步,在 ret_handler
執行的時候,它已經持有了該 spinlock
,而屬於 kretprobe
的 ret_handler
本身同樣也需要該 spinlock
,因此會 deadlock。啦啦啦,這就是為什麼我喜歡純手工活兒的原因了,因為它可控啊!
來來來,試試 ftrace
,相比於 kretprobe
而言,它更簡單,我覺得它應該沒問題。若再不行,就只能上純手工藝了。
先來試試框架,下面是一個什麼都不做的框架程式碼:
當我加載這個模組的時候,系統像什麼都沒有發生一樣平靜,而我用 crash
看 _raw_spin_lock
/_raw_spin_unlock
的時候,顯然它們已經被 hook 了,這意味著,wrap
起作用了。
下圖展示了 ftrace
實作 wrap
的原理:
這個已經和手工做法幾乎無異了。可以拿這個 ftrace
和上面的 kretprobe
對比一下,感受一下雷同和差異:
kretprobe
一般也是用來 probe 函式而不是指令,這一點和 ftrace
一致。ftrace
機制上更加直接,而 kretprobe
則更加 trick 一點。ftrace
;若要表演,我會照抄一套 kretprobe
的機制;若純自己玩,我選擇純手工藝。好了,現在把 spinlock 統計邏輯放進去,程式碼簡單,自己感受:
值得注意的是,wrap
函式中不能再呼叫任何會使用 spinlock 的函式 (避免循環巢狀) ,因此一開始用 atomics 實作一個自己的自旋鎖:
然而系統很快就卡死了:
於是,只能退而求其次,採用寬鬆的約束了:
所以,我這個 spinlock 快照記錄機制,它是不準的。
以上就是一個簡單的 spinlock 快照機制的程式碼和說明,它可以展示:
task
在爭搶哪一把 spinlock
。task
所持有。這個機制有什麼用呢?
回到本文的開頭,若想讓 panic
被 schedule
出去而不是當機,我在擔心 current
持有鎖怎麼辦,我希望有一個辦法讓我知道 current
是否持有 spinlock
,以及持有了哪些 spinlock
,然後將它們 unlock
。
現在有辦法了:
強調一點,本文介紹的把戲無法揪出所有的 spinlock 狀態,因為 Linux 核心中 spinlock 的 lock/unlock 操作並不全是透過 _raw_spin_lock
/_raw_spin_unlock
入口的,比如 spin_lock_irqsave
/spin_unlock_irqrestore
就不是,它們有自己的入口。因此你需要把所有這些入口都用 ftrace
hook 起來,才能全都捕捉到。