鎖型別及其規則¶
簡介¶
核心提供多種鎖定原語,可分為三類
睡眠鎖
CPU 本地鎖
自旋鎖
本文件從概念上描述了這些鎖型別,並提供了它們的巢狀規則,包括 PREEMPT_RT 下使用的規則。
鎖的類別¶
睡眠鎖¶
睡眠鎖只能在可搶佔任務上下文中獲取。
儘管實現允許從其他上下文呼叫 try_lock(),但也有必要仔細評估 unlock() 以及 try_lock() 的安全性。 此外,還有必要評估這些原語的除錯版本。 簡而言之,除非沒有其他選擇,否則不要從其他上下文獲取睡眠鎖。
睡眠鎖型別
mutex
rt_mutex
semaphore
rw_semaphore
ww_mutex
percpu_rw_semaphore
在 PREEMPT_RT 核心上,這些鎖型別會轉換為睡眠鎖
local_lock
spinlock_t
rwlock_t
CPU 本地鎖¶
local_lock
在非 PREEMPT_RT 核心上,local_lock 函式是圍繞搶佔和中斷停用原語的包裝器。與其他鎖定機制相反,停用搶佔或中斷是純 CPU 本地併發控制機制,不適合於 CPU 間的併發控制。
自旋鎖¶
raw_spinlock_t
位自旋鎖
在非 PREEMPT_RT 核心上,這些鎖型別也是自旋鎖
spinlock_t
rwlock_t
自旋鎖隱式地停用搶佔,並且鎖/解鎖函式可以具有應用進一步保護的字尾
_bh()
停用/啟用下半部(軟中斷)
_irq()
停用/啟用中斷
_irqsave/restore()
儲存並停用/恢復中斷停用狀態
所有者語義¶
除了訊號量之外,上述鎖型別都具有嚴格的所有者語義
獲取鎖的上下文(任務)必須釋放它。
rw_semaphores 有一個特殊的介面,允許非所有者為讀者釋放鎖。
rtmutex¶
RT-mutex 是支援優先順序繼承 (PI) 的互斥鎖。
由於搶佔和中斷停用部分,PI 在非 PREEMPT_RT 核心上存在限制。
PI 顯然不能搶佔程式碼的停用搶佔或停用中斷區域,即使在 PREEMPT_RT 核心上也是如此。 相反,PREEMPT_RT 核心在可搶佔任務上下文中執行大多數此類程式碼區域,特別是中斷處理程式和軟中斷。 此轉換允許透過 RT-mutex 實現 spinlock_t 和 rwlock_t。
semaphore¶
semaphore 是一種計數訊號量實現。
訊號量通常用於序列化和等待,但新的用例應改用單獨的序列化和等待機制,例如互斥鎖和完成量。
semaphores 和 PREEMPT_RT¶
PREEMPT_RT 不會更改訊號量實現,因為計數訊號量沒有所有者的概念,因此阻止 PREEMPT_RT 為訊號量提供優先順序繼承。 畢竟,無法提升未知所有者。 因此,阻塞訊號量可能會導致優先順序反轉。
rw_semaphore¶
rw_semaphore 是一種多讀者單寫者鎖定機制。
在非 PREEMPT_RT 核心上,該實現是公平的,因此可以防止寫者飢餓。
rw_semaphore 預設情況下符合嚴格的所有者語義,但存在允許非所有者為讀者釋放的專用介面。 這些介面獨立於核心配置工作。
rw_semaphore 和 PREEMPT_RT¶
PREEMPT_RT 核心將 rw_semaphore 對映到單獨的基於 rt_mutex 的實現,從而改變了公平性
由於 rw_semaphore 寫者無法將其優先順序授予多個讀者,因此被搶佔的低優先順序讀者將繼續持有其鎖,從而甚至使高優先順序寫者飢餓。 相比之下,由於讀者可以將其優先順序授予寫者,因此被搶佔的低優先順序寫者的優先順序將被提升,直到它釋放鎖,從而防止該寫者使讀者飢餓。
local_lock¶
local_lock 為受停用搶佔或中斷保護的關鍵部分提供了一個命名範圍。
在非 PREEMPT_RT 核心上,local_lock 操作對映到搶佔和中斷停用和啟用原語
local_lock(&llock)
preempt_disable()
local_unlock(&llock)
preempt_enable()
local_lock_irq(&llock)
local_irq_disable()
local_unlock_irq(&llock)
local_irq_enable()
local_lock_irqsave(&llock)
local_irq_save()
local_unlock_irqrestore(&llock)
local_irq_restore()
local_lock 的命名範圍比常規原語有兩個優點
鎖名稱允許靜態分析,並且也是保護範圍的明確文件,而常規原語是無範圍且不透明的。
如果啟用了 lockdep,則 local_lock 會獲得一個鎖對映,該鎖對映允許驗證保護的正確性。 這可以檢測到諸如從中斷或軟中斷上下文呼叫使用 preempt_disable() 作為保護機制的函式的情況。 除此之外,lockdep_assert_held(&llock) 的工作方式與其他任何鎖定原語一樣。
local_lock 和 PREEMPT_RT¶
PREEMPT_RT 核心將 local_lock 對映到每個 CPU 的 spinlock_t,從而改變了語義
所有 spinlock_t 更改也適用於 local_lock。
local_lock 用法¶
在停用搶佔或中斷是適當的併發控制形式以在非 PREEMPT_RT 核心上保護每個 CPU 的資料結構的情況下,應使用 local_lock。
由於 PREEMPT_RT 特定的 spinlock_t 語義,local_lock 不適合於在 PREEMPT_RT 核心上防止搶佔或中斷。
raw_spinlock_t 和 spinlock_t¶
raw_spinlock_t¶
raw_spinlock_t 是所有核心(包括 PREEMPT_RT 核心)中的嚴格自旋鎖實現。 僅在真正的關鍵核心程式碼、低階中斷處理以及需要停用搶佔或中斷(例如,安全訪問硬體狀態)的地方使用 raw_spinlock_t。 當關鍵部分很小,從而避免 RT-mutex 開銷時,有時也可以使用 raw_spinlock_t。
spinlock_t¶
spinlock_t 的語義隨著 PREEMPT_RT 的狀態而變化。
在非 PREEMPT_RT 核心上,spinlock_t 對映到 raw_spinlock_t 並且具有完全相同的語義。
spinlock_t 和 PREEMPT_RT¶
在 PREEMPT_RT 核心上,spinlock_t 對映到基於 rt_mutex 的單獨實現,這改變了語義
搶佔未停用。
用於 spin_lock / spin_unlock 操作的硬中斷相關字尾(_irq、_irqsave / _irqrestore)不會影響 CPU 的中斷停用狀態。
軟中斷相關字尾 (_bh()) 仍然停用軟中斷處理程式。
非 PREEMPT_RT 核心停用搶佔以獲得此效果。
PREEMPT_RT 核心使用每個 CPU 的鎖進行序列化,從而保持啟用搶佔。 該鎖停用軟中斷處理程式,並防止由於任務搶佔而導致的重入。
PREEMPT_RT 核心保留所有其他 spinlock_t 語義
持有 spinlock_t 的任務不會遷移。 非 PREEMPT_RT 核心透過停用搶佔來避免遷移。 PREEMPT_RT 核心改為停用遷移,這可確保即使任務被搶佔,指向每個 CPU 變數的指標仍然有效。
任務狀態在獲取自旋鎖期間保持不變,確保任務狀態規則適用於所有核心配置。 非 PREEMPT_RT 核心保持任務狀態不變。 但是,如果任務在獲取期間阻塞,PREEMPT_RT 必須更改任務狀態。 因此,它會在阻塞之前儲存當前任務狀態,並且相應的鎖喚醒會恢復它,如下所示
task->state = TASK_INTERRUPTIBLE lock() block() task->saved_state = task->state task->state = TASK_UNINTERRUPTIBLE schedule() lock wakeup task->state = task->saved_state其他型別的喚醒通常會無條件地將任務狀態設定為 RUNNING,但這在這裡不起作用,因為任務必須保持阻塞狀態,直到鎖可用為止。 因此,當非鎖喚醒嘗試喚醒阻塞等待自旋鎖的任務時,它會改為將儲存的狀態設定為 RUNNING。 然後,當鎖獲取完成時,鎖喚醒會將任務狀態設定為儲存的狀態,在這種情況下將其設定為 RUNNING
task->state = TASK_INTERRUPTIBLE lock() block() task->saved_state = task->state task->state = TASK_UNINTERRUPTIBLE schedule() non lock wakeup task->saved_state = TASK_RUNNING lock wakeup task->state = task->saved_state這可確保不會丟失真正的喚醒。
rwlock_t¶
rwlock_t 是一種多讀者單寫者鎖定機制。
非 PREEMPT_RT 核心將 rwlock_t 實現為自旋鎖,並且 spinlock_t 的字尾規則相應地適用。 該實現是公平的,因此可以防止寫者飢餓。
rwlock_t 和 PREEMPT_RT¶
PREEMPT_RT 核心將 rwlock_t 對映到單獨的基於 rt_mutex 的實現,從而改變了語義
所有 spinlock_t 更改也適用於 rwlock_t。
由於 rwlock_t 寫者無法將其優先順序授予多個讀者,因此被搶佔的低優先順序讀者將繼續持有其鎖,從而甚至使高優先順序寫者飢餓。 相比之下,由於讀者可以將其優先順序授予寫者,因此被搶佔的低優先順序寫者的優先順序將被提升,直到它釋放鎖,從而防止該寫者使讀者飢餓。
PREEMPT_RT 注意事項¶
RT 上的 local_lock¶
local_lock 到 PREEMPT_RT 核心上的 spinlock_t 的對映有一些含義。 例如,在非 PREEMPT_RT 核心上,以下程式碼序列按預期工作
local_lock_irq(&local_lock);
raw_spin_lock(&lock);
並且與以下程式碼完全等效
raw_spin_lock_irq(&lock);
在 PREEMPT_RT 核心上,此程式碼序列會中斷,因為 local_lock_irq() 對映到既不停用中斷也不停用搶佔的每個 CPU 的 spinlock_t。 以下程式碼序列在 PREEMPT_RT 和非 PREEMPT_RT 核心上都能完美地工作
local_lock_irq(&local_lock);
spin_lock(&lock);
local 鎖的另一個注意事項是每個 local_lock 都有一個特定的保護範圍。 因此,以下替換是錯誤的
func1()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock_1, flags);
func3();
local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock_1, flags);
}
func2()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock_2, flags);
func3();
local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock_2, flags);
}
func3()
{
lockdep_assert_irqs_disabled();
access_protected_data();
}
在非 PREEMPT_RT 核心上,這可以正常工作,但在 PREEMPT_RT 核心上,local_lock_1 和 local_lock_2 是不同的,並且無法序列化 func3() 的呼叫者。 此外,lockdep 斷言將在 PREEMPT_RT 核心上觸發,因為由於 spinlock_t 的 PREEMPT_RT 特定語義,local_lock_irqsave() 不會停用中斷。 正確的替換是
func1()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock, flags);
func3();
local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock, flags);
}
func2()
{
local_irq_save(flags); -> local_lock_irqsave(&local_lock, flags);
func3();
local_irq_restore(flags); -> local_unlock_irqrestore(&local_lock, flags);
}
func3()
{
lockdep_assert_held(&local_lock);
access_protected_data();
}
spinlock_t 和 rwlock_t¶
PREEMPT_RT 核心上 spinlock_t 和 rwlock_t 語義的更改有一些含義。 例如,在非 PREEMPT_RT 核心上,以下程式碼序列按預期工作
local_irq_disable();
spin_lock(&lock);
並且與以下程式碼完全等效
spin_lock_irq(&lock);
同樣適用於 rwlock_t 和 _irqsave() 字尾變體。
在 PREEMPT_RT 核心上,此程式碼序列會中斷,因為 RT-mutex 需要完全可搶佔的上下文。 而是使用 spin_lock_irq() 或 spin_lock_irqsave() 及其解鎖對應項。 如果中斷停用和鎖定必須保持分離,PREEMPT_RT 會提供一種 local_lock 機制。 獲取 local_lock 會將任務固定到 CPU,從而允許獲取每個 CPU 的中斷停用鎖之類的東西。 但是,僅應在絕對必要時使用此方法。
典型的場景是執行緒上下文中每個 CPU 變數的保護
struct foo *p = get_cpu_ptr(&var1);
spin_lock(&p->lock);
p->count += this_cpu_read(var2);
這是非 PREEMPT_RT 核心上的正確程式碼,但在 PREEMPT_RT 核心上,這會中斷。spinlock_t 語義的 PREEMPT_RT 特定更改不允許獲取 p->lock,因為 get_cpu_ptr() 隱式停用搶佔。 以下替換適用於兩個核心
struct foo *p;
migrate_disable();
p = this_cpu_ptr(&var1);
spin_lock(&p->lock);
p->count += this_cpu_read(var2);
migrate_disable() 確保任務固定在當前 CPU 上,這反過來保證了對 var1 和 var2 的每個 CPU 的訪問在任務保持可搶佔的同時停留在同一個 CPU 上。
migrate_disable() 替換對於以下場景無效
func()
{
struct foo *p;
migrate_disable();
p = this_cpu_ptr(&var1);
p->val = func2();
這會中斷,因為 migrate_disable() 不能防止來自搶佔任務的重入。 這種情況的正確替換是
func()
{
struct foo *p;
local_lock(&foo_lock);
p = this_cpu_ptr(&var1);
p->val = func2();
在非 PREEMPT_RT 核心上,這透過停用搶佔來防止重入。 在 PREEMPT_RT 核心上,這透過獲取底層每個 CPU 的自旋鎖來實現。
RT 上的 raw_spinlock_t¶
獲取 raw_spinlock_t 會停用搶佔,並且可能還會停用中斷,因此關鍵部分必須避免獲取常規 spinlock_t 或 rwlock_t,例如,關鍵部分必須避免分配記憶體。 因此,在非 PREEMPT_RT 核心上,以下程式碼可以完美地工作
raw_spin_lock(&lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);
但是此程式碼在 PREEMPT_RT 核心上會失敗,因為記憶體分配器是完全可搶佔的,因此無法從真正的原子上下文中呼叫。 但是,在保持正常的非原始自旋鎖時呼叫記憶體分配器是完全可以的,因為它們不會在 PREEMPT_RT 核心上停用搶佔
spin_lock(&lock);
p = kmalloc(sizeof(*p), GFP_ATOMIC);
位自旋鎖¶
PREEMPT_RT 無法替換位自旋鎖,因為單個位太小,無法容納 RT-mutex。 因此,位自旋鎖的語義在 PREEMPT_RT 核心上得以保留,因此 raw_spinlock_t 的注意事項也適用於位自旋鎖。
某些位自旋鎖已使用條件 (#ifdef'ed) 程式碼更改在用法站點替換為常規 spinlock_t 以用於 PREEMPT_RT。 相比之下,spinlock_t 替換不需要用法站點更改。 而是,標頭檔案和核心鎖定實現中的條件允許編譯器透明地進行替換。
鎖型別巢狀規則¶
最基本的規則是
相同鎖類別(睡眠、CPU 本地、自旋)的鎖型別可以任意巢狀,只要它們遵守常規鎖排序規則以防止死鎖即可。
睡眠鎖型別不能巢狀在 CPU 本地和自旋鎖型別中。
CPU 本地和自旋鎖型別可以巢狀在睡眠鎖型別中。
自旋鎖型別可以巢狀在所有鎖型別中
這些約束既適用於 PREEMPT_RT,也適用於其他情況。
PREEMPT_RT 將 spinlock_t 和 rwlock_t 的鎖類別從自旋更改為睡眠,並將 local_lock 替換為每個 CPU 的 spinlock_t 意味著它們不能在持有原始自旋鎖時獲取。 這導致以下巢狀順序
睡眠鎖
spinlock_t、rwlock_t、local_lock
raw_spinlock_t 和位自旋鎖
如果違反這些約束,Lockdep 將會發出抱怨,無論是 PREEMPT_RT 還是其他情況。