最近在內核頻繁使用了自旋鎖,自旋鎖如果使用不當,極易引起死鎖,在此總結一下。
自旋鎖是一個互斥設備,它只有兩個值:“鎖定”和“解鎖”。它通常實現為某個整數值中的某個位。希望獲得某個特定鎖得代碼測試相關的位。如果鎖可用,則“鎖定”被設置,而代碼繼續進入臨界區;相反,如果鎖被其他人獲得,則代碼進入忙循環(而不是休眠,這也是自旋鎖和一般鎖的區別)并重復檢查這個鎖,直到該鎖可用為止,這就是自旋的過程。“測試并設置位”的操作必須是原子的,這樣,即使多個線程在給定時間自旋,也只有一個線程可獲得該鎖。
自旋鎖最初是為了在多處理器系統(SMP)使用而設計的,但是只要考慮到并發問題,單處理器在運行可搶占內核時其行為就類似于SMP。因此,自旋鎖對于SMP和單處理器可搶占內核都適用。可以想象,當一個處理器處于自旋狀態時,它做不了任何有用的工作,因此自旋鎖對于單處理器不可搶占內核沒有意義,實際上,非搶占式的單處理器系統上自旋鎖被實現為空操作,不做任何事情。
自旋鎖有幾個重要的特性:1、被自旋鎖保護的臨界區代碼執行時不能進入休眠。2、被自旋鎖保護的臨界區代碼執行時是不能被被其他中斷中斷。3、被自旋鎖保護的臨界區代碼執行時,內核不能被搶占。從這幾個特性可以歸納出一個共性:被自旋鎖保護的臨界區代碼執行時,它不能因為任何原因放棄處理器。
考慮上面第一種情況,想象你的內核代碼請求到一個自旋鎖并且在它的臨界區里做它的事情,在中間某處,你的代碼失去了處理器。或許它已調用了一個函數(copy_from_user,假設)使進程進入睡眠。也或許,內核搶占發威,一個更高優先級的進程將你的代碼推到了一邊。此時,正好某個別的線程想獲取同一個鎖,如果這個線程運行在和你的內核代碼不同的處理器上(幸運的情況),那么它可能要自旋等待一段時間(可能很長),當你的代碼從休眠中喚醒或者重新得到處理器并釋放鎖,它就能得到鎖。而最壞的情況是,那個想獲取鎖得線程剛好和你的代碼運行在同一個處理器上,這時它將一直持有CPU進行自旋操作,而你的代碼是永遠不可能有任何機會來獲得CPU釋放這個鎖了,這就是悲催的死鎖。
考慮上面第二種情況,和上面第一種情況類似。假設我們的驅動程序正在運行,并且已經獲取了一個自旋鎖,這個鎖控制著對設備的訪問。在擁有這個鎖得時候,設備產生了一個中斷,它導致中斷處理例程被調用,而中斷處理例程在訪問設備之前,也要獲得這個鎖。當中斷處理例程和我們的驅動程序代碼在同一個處理器上運行時,由于中斷處理例程持有CPU不斷自旋,我們的代碼將得不到機會釋放鎖,這也將導致死鎖。
因此,如果我們有一個自旋鎖,它可以被運行在(硬件或軟件)中斷上下文中的代碼獲得,則必須使用某個禁用中斷的spin_lock形式的鎖來禁用本地中斷(注意,只是禁用本地CPU的中斷,不能禁用別的處理器的中斷),使用其他的鎖定函數遲早會導致系統死鎖(導致死鎖的時間可能不定,但是發生上述死鎖情況的概率肯定是有的,看處理器怎么調度了)。如果我們不會在硬中斷處理例程中訪問自旋鎖,但可能在軟中斷(例如,以tasklet的形式運行的代碼)中訪問,則應該使用spin_lock_bh,以便在安全避免死鎖的同時還能服務硬件中斷。
補充:
鎖定一個自旋鎖的函數有四個:
void spin_lock(spinlock_t *lock); ? ? ?
最基本得自旋鎖函數,它不失效本地中斷。
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);
在獲得自旋鎖之前禁用硬中斷(只在本地處理器上),而先前的中斷狀態保存在flags中
void spin_lockirq(spinlock_t *lock);
在獲得自旋鎖之前禁用硬中斷(只在本地處理器上),不保存中斷狀態
void spin_lock_bh(spinlock_t *lock);
在獲得鎖前禁用軟中斷,保持硬中斷打開狀態