6.2自旋锁
文章目錄
- 自旋鎖
- 自旋鎖API函數
- 死鎖兩種情況
- 被自旋鎖保護的臨界區調用引起睡眠和阻塞的API 函數
- 中斷造成的死鎖
- 下半部競爭處理函數
- 自旋鎖使用的注意事項
自旋鎖
原子操作只能對整形變量或者位進行保護,但是,在實際的使用環境中怎么可能只有整形變量或位這么簡單的臨界區。舉個最簡單的例子,設備結構體變量就不是整型變量,我們對于結構體中成員變量的操作也要保證原子性,在線程 A 對結構體變量使用期間,應該禁止其他的線程來訪問此結構體變量,這些工作原子操作都不能勝任,需要本節要講的鎖機制,在 Linux內核中就是自旋鎖。
當一個線程要訪問某個共享資源的時候首先要先獲取相應的鎖,鎖只能被一個線程持有,只要此線程不釋放持有的鎖,那么其他的線程就不能獲取此。對于自旋鎖而言,如果自旋鎖正在被線程 A 持有,線程 B 想要獲取自旋鎖,那么線程 B 就會處于忙循環-旋轉-等待狀態,線程 B 不會進入休眠狀態或者說去做其他的處理,而是會一直傻傻的在那里“轉圈圈”的等待鎖可用。比如現在有個公用電話亭,一次肯定只能進去一個人打電話,現在電話亭里面有人正在打電話,相當于獲得了自旋鎖。此時你到了電話亭門口,因為里面有人,所以你不能進去打電話,相當于沒有獲取自旋鎖,這個時候你肯定是站在原地等待,你可能因為無聊的等待而轉圈圈消遣時光,反正就是哪里也不能去,要一直等到里面的人打完電話出來。終于,里面的人打完電話出來了,相當于釋放了自旋鎖,這個時候你就可以使用電話亭打電話了,相當于獲取到了自旋鎖。
自旋鎖的“自旋”也就是“原地打轉”的意思,“原地打轉”的目的是為了等待自旋鎖可以用,可以訪問共享資源。把自旋鎖比作一個變量 a,變量 a=1 的時候表示共享資源可用,當 a=0的時候表示共享資源不可用。現在線程 A 要訪問共享資源,發現 a=0(自旋鎖被其他線程持有),那么線程 A 就會不斷的查詢 a 的值,直到 a=1。從這里我們可以看到自旋鎖的一個缺點:那就等待自旋鎖的線程會一直處于自旋狀態,這樣會浪費處理器時間,降低系統性能,所以自旋鎖的持有時間不能太長。所以自旋鎖適用于短時期的輕量級加鎖,如果遇到需要長時間持有鎖的場景那就需要換其他的方法了,這個我們后面會講解。
Linux 內核使用結構體 spinlock_t 表示自旋鎖,結構體定義如下所示:
在使用自旋鎖之前,肯定要先定義一個自旋鎖變量,定義方法如下所示:
spinlock_t lock; //定義自旋鎖
定義好自旋鎖變量以后就可以使用相應的 API 函數來操作自旋鎖。
自旋鎖API函數
最基本的自旋鎖 API 函數如表 47.3.2.1 所示:
| DEFINE_SPINLOCK(spinlock_t lock) | 定義并初始化一個自選變量。 |
| int spin_lock_init(spinlock_t *lock) | 初始化自旋鎖。 |
| void spin_lock(spinlock_t *lock) | 獲取指定的自旋鎖,也叫做加鎖。 |
| void spin_unlock(spinlock_t *lock) | 釋放指定的自旋鎖。 |
| int spin_trylock(spinlock_t *lock) | 嘗試獲取指定的自旋鎖,如果沒有獲取到就返回 0 |
| int spin_is_locked(spinlock_t *lock) | 檢查指定的自旋鎖是否被獲取,如果沒有被獲取就返回非 0,否則返回 0。 |
死鎖兩種情況
被自旋鎖保護的臨界區調用引起睡眠和阻塞的API 函數
上圖的自旋鎖API函數適用于SMP或支持搶占的單CPU下線程之間的并發訪問,也就是用于線程與線程之間,**被自旋鎖保護的臨界區一定不能調用任何能夠引起睡眠和阻塞的API 函數,**否則的話會可能會導致死鎖現象的發生。自旋鎖會自動禁止搶占,也就說當線程 A得到鎖以后會暫時禁止內核搶占。如果線程 A 在持有鎖期間進入了休眠狀態,那么線程 A 會自動放棄 CPU 使用權。線程 B 開始運行,線程 B 也想要獲取鎖,但是此時鎖被 A 線程持有,而且內核搶占還被禁止了!線程 B 無法被調度出去,那么線程 A 就無法運行,鎖也就無法釋放,好了,死鎖發生了!
中斷造成的死鎖
上圖API 函數用于線程之間的并發訪問,如果此時中斷也要插一腳,中斷也想訪問共享資源,那該怎么辦呢?首先可以肯定的是,中斷里面可以使用自旋鎖,但是在中斷里面使用自旋鎖的時候,在獲取鎖之前一定要先禁止本地中斷(也就是本 CPU 中斷,對于多核 SOC來說會有多個 CPU 核),否則可能導致鎖死現象的發生。
前,線程 A 是不可能執行的,線程 A 說“你先放手”,中斷說“你先放手”,場面就這么僵持著,死鎖發生!
最好的解決方法就是獲取鎖之前關閉本地中斷,Linux 內核提供了相應的 API 函數,如表47.3.2.2 所示:
| void spin_lock_irq(spinlock_t *lock) | 禁止本地中斷,并獲取自旋鎖。 |
| void spin_unlock_irq(spinlock_t *lock) | 激活本地中斷,并釋放自旋鎖。 |
| void spin_lock_irqsave(spinlock_t *lock,unsigned long flags) | 保存中斷狀態,禁止本地中斷,并獲取自旋鎖。 |
| void spin_unlock_irqrestore(spinlock_t*lock, unsigned long flags) | 將中斷狀態恢復到以前的狀態,并且激活本地中斷,釋放自旋鎖。 |
使用 spin_lock_irq/spin_unlock_irq 的時候需要用戶能夠確定加鎖之前的中斷狀態,但實際上內核很龐大,運行也是“千變萬化”,我們是很難確定某個時刻的中斷狀態,因此不推薦使用spin_lock_irq/spin_unlock_irq。建議使用 spin_lock_irqsave/spin_unlock_irqrestore,因為這一組函數會保存中斷狀態,在釋放鎖的時候會恢復中斷狀態。一般在線程中使用 spin_lock_irqsave/
spin_unlock_irqrestore,在中斷中使用 spin_lock/spin_unlock,示例代碼如下所示:
下半部競爭處理函數
下半部(BH)也會競爭共享資源,有些資料也會將下半部叫做底半部。關于下半部后面的章節會講解,如果要在下半部里面使用自旋鎖,可以使用表 47.3.2.3 中的 API 函數:
| void spin_lock_bh(spinlock_t *lock) | 關閉下半部,并獲取自旋鎖。 |
| void spin_unlock_bh(spinlock_t *lock) | 打開下半部,并釋放自旋鎖。 |
自旋鎖使用的注意事項
①、因為在等待自旋鎖的時候處于“自旋”狀態,因此鎖的持有時間不能太長,一定要短,否則的話會降低系統性能。如果臨界區比較大,運行時間比較長的話要選擇其他的并發處理方式,比如稍后要講的信號量和互斥體。
②、自旋鎖保護的臨界區內不能調用任何可能導致線程休眠的 API 函數,否則的話可能導致死鎖。
③、不能遞歸申請自旋鎖,因為一旦通過遞歸的方式申請一個你正在持有的鎖,那么你就必須“自旋”,等待鎖被釋放,然而你正處于“自旋”狀態,根本沒法釋放鎖。結果就是自己把自己鎖死了!
④、在編寫驅動程序的時候我們必須考慮到驅動的可移植性,因此不管你用的是單核的還是多核的 SOC,都將其當做多核 SOC 來編寫驅動程序。
總結
- 上一篇: shell编程快速入门(一)
- 下一篇: new的三种用法