Linux 核心鎖定

最後更新: 2024-11-26 05:37:18 +0800 +0800
tags: Kernel | Linux | Locking
原文以英文撰寫。您正在閱讀AI翻譯版本。

核心鎖定始於執行上下文。在程序上下文呼叫端之間正確的鎖,當相同資料從 softirq、硬體中斷或不可遮罩中斷(NMI)抵達時可能死結。原語及其後綴必須符合所有存取受保護狀態的上下文。

自旋鎖語意 #

使用下列方式宣告與初始化一般自旋鎖:

c
static DEFINE_SPINLOCK(queue_lock);

struct queue {
    spinlock_t lock;
};

spin_lock_init(&queue->lock);

在非 PREEMPT_RT 的 SMP 核心上,spin_lock() 會停用本機 CPU 的搶佔,並自旋直到取得獨佔擁有權。它不會停用本機中斷或 softirq。持有者不得睡眠、明確排程、執行阻塞配置,或呼叫可能取得睡眠鎖的程式碼。

在單處理器的非 RT 建置中,因為沒有其他 CPU 可以競爭,部分實作可以編譯為搶佔與編譯器控制。正確性不能依賴該最佳化;相同來源仍必須對 SMP 有效。

PREEMPT_RT 會變更 spinlock_t 與 rwlock_t 語意,使它們使用以 RT mutex 為基礎的實作並保持可搶佔。需要在 RT 與非 RT 核心上真正自旋且不可搶佔的鎖時,使用 raw_spinlock_t,但原始臨界區必須保持短小,且不能呼叫在 RT 上會變成睡眠的程式碼。替換是在鎖定標頭檔與核心程式碼中實作;應用程式程式碼不應使用設定條件重現它。

對應程序、Softirq 與 Hardirq 上下文 #

請選擇能防止同一 CPU 重新進入的最窄變體:

存取資料的來源程序上下文取得中斷側取得
僅程序上下文spin_lock()不適用
程序上下文與 softirq/taskletspin_lock_bh()softirq 中的 spin_lock()
程序上下文與 hardirqspin_lock_irqsave()處理常式中的 spin_lock()
具有已知 IRQ 狀態的 hardirq 上下文spin_lock_irq() 或 spin_lock_irqsave()依呼叫端狀態

spin_lock_bh() 會在取得鎖前停用本機下半部。spin_lock_irq() 會停用本機可遮罩中斷。spin_lock_irqsave(lock, flags) 另外儲存先前的中斷狀態,而 spin_unlock_irqrestore(lock, flags) 還原完全相同的狀態:

c
unsigned long flags;

spin_lock_irqsave(&device->lock, flags);
update_device_state(device);
spin_unlock_irqrestore(&device->lock, flags);

在一個詞法取得/釋放配對中使用相同的 flags 變數,不要假設進入時中斷已啟用。停用本機中斷可防止持有者 CPU 上的中斷等待自己的鎖;自旋鎖本身則排除在其他 CPU 上執行的處理常式。

若資料只在 hardirq 實例之間共用,當中斷線路不會在同一 CPU 上重新進入時,處理常式可以使用 spin_lock()。程序上下文存取者仍必須使用停用 IRQ 的變體。

NMI 上下文需要獨立設計 #

local_irq_disable() 與 spin_lock_irqsave() 不會遮罩 NMI。若 NMI 處理常式取得被中斷程式碼在同一 CPU 上持有的鎖,NMI 會永久自旋,因為被中斷的持有者無法恢復。

因此,NMI 共用資料需要為 NMI 設計的協定:

  • 使用 NMI 安全原子作業的無鎖或每 CPU 記錄;
  • 寫入者永遠不會以造成死結的方式被 NMI 讀取者搶佔的序號計數器模式;
  • 可接受丟棄 NMI 側觀察時使用 raw_spin_trylock();
  • 只有在每條取得路徑都證明不會發生 NMI 重新進入時,才使用原始鎖。

「原始」表示鎖在 PREEMPT_RT 上保留自旋語意;它不會讓任意臨界區具備 NMI 安全性。

取得與釋放排序 #

鎖取得具有 acquire 語意,解鎖具有 release 語意。因此,臨界區內的作業相對於另一個使用相同鎖保護相同資料的 CPU,會依序排列:

c
spin_lock(&state->lock);
state->payload = payload;
state->ready = true;
spin_unlock(&state->lock);

後續成功取得鎖者會觀察到解鎖前受保護的寫入。部分讀取者刻意不使用鎖存取資料時,鎖定不是協定的替代品。這些讀取者可能需要 READ_ONCE()、release/acquire 發布、RCU 或明確屏障。Linux 記憶體屏障文件也區分鎖 acquire/release 排序與一般完整記憶體屏障;臨界區外需要排序的程式碼,必須使用文件說明的屏障原語。

鎖順序仍是獨立要求。若一條路徑先取得 A 再取得 B,另一條路徑不得先取得 B 再取得 A。IRQ 狀態變體可防止本機中斷反轉,但不能防止跨鎖循環。

架構實作 #

公開的 spinlock_t API 定義於一般鎖定標頭檔,並透過原始鎖定程式碼映射至架構作業。在 SMP 上,raw_spinlock_t 包含 arch_spinlock_t,以及可選的除錯與 lockdep 狀態。架構會選取適合其原子指令與虛擬化環境的實作。

x86 使用佇列自旋鎖基礎設施。無競爭路徑在緊湊鎖字上運作;有競爭的呼叫端會形成佇列,而不是全部反覆使同一快取行失效。半虛擬化掛鉤可以取代部分等待與喚醒行為。架構程式碼提供排序與原子原語,一般程式碼提供 API 一致性、檢測與依設定而定的轉換。

來源配置不是 ABI。請從相同版本的 include/linux/spinlock*.h、kernel/locking/ 與 arch/<arch>/include/asm/ 驗證特定核心。

競爭與除錯 #

競爭會浪費 CPU 時間,並使鎖的快取行在處理器之間移動。保持自旋鎖區段有界,將配置與慢速 I/O 移至外部,分割不相關狀態,並避免為每 CPU 資料取得一個全域鎖。以多個鎖取代一個鎖可以減少競爭,但會增加排序與生命週期複雜度,因此變更設計前請先測量。

核心設施有助於建立事實:

  • CONFIG_LOCKDEP 與 lockdep 註解會偵測已觀察及可能的相依循環、錯誤 IRQ 使用與上下文不符。
  • CONFIG_DEBUG_SPINLOCK 加入擁有權與狀態檢查。
  • CONFIG_PROVE_LOCKING 啟用更廣泛的鎖定驗證。
  • lockstat 與 perf lock 回報競爭與取得行為。

除錯建置會改變時序與負擔。乾淨的 lockdep 執行可增加信心,但不能證明未執行路徑具有正確的生命週期或記憶體排序協定。

讀寫自旋鎖 #

rwlock_t 允許並行讀取者或單一寫入者:

c
static DEFINE_RWLOCK(routes_lock);
unsigned long flags;

read_lock_irqsave(&routes_lock, flags);
route = lookup_route(key);
read_unlock_irqrestore(&routes_lock, flags);

write_lock_irqsave(&routes_lock, flags);
replace_route(key, replacement);
write_unlock_irqrestore(&routes_lock, flags);

讀寫自旋鎖需要比普通自旋鎖更多的原子作業。它們不提供可攜式公平性保證;工作負載與實作可能讓寫入者延遲於讀取者之後,或讓讀取者延遲於寫入者之後。讀鎖不能安全升級為寫鎖。作業可能修改結構時,請釋放並在寫鎖下重試,或從一開始就取得寫鎖。

新的使用方式值得仔細檢視:

  • 所有存取都在可睡眠程序上下文,尤其持有者可能阻塞時,使用互斥鎖。
  • 讀取占優勢、讀取者可以容忍其存取模型,且更新可以延後回收至寬限期後時,使用 RCU。
  • 讀取者可以重試且寫入者可以序列化的小型快照,使用序列鎖/計數器。
  • 臨界區短小時使用普通自旋鎖;其較低的讀取側複雜度可能優於 rwlock_t。

選擇取決於上下文、持有時間、競爭、讀寫比例與物件生命週期,而不是並行讀取者的抽象吸引力。

參考資料 #

評論

在 GitHub 上閱讀或評論(發表評論需要登入)