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