http://blog.chinaunix.net/uid-20543672-id-3252604.html
css
自旋鎖:若是內核配置爲SMP系統,自旋鎖就按SMP系統上的要求來實現真正的自旋等待,可是對於UP系統,自旋鎖僅作搶佔和中斷操做,沒有實現真正的「自旋」。若是配置了CONFIG_DEBUG_SPINLOCK,那麼自旋鎖按照SMP系統來編譯。html
可是爲何在UP系統中不須要真正的「帶有自旋的」自旋鎖呢?其實在理解了自旋鎖的概念和由來,這個問題就迎刃而解了。因此我從新查找了關於自旋鎖的資料,認真研究了自旋鎖的實現和相關內容。linux
衆所周知,自旋鎖最初就是爲了SMP系統設計的,實如今多處理器狀況下保護臨界區。因此在SMP系統中,自旋鎖的實現是完整的原本面目。可是對於UP系統,自旋鎖能夠說是SMP版本的閹割版。由於只有在SMP系統中的自旋鎖才須要真正「自旋」。編程
自旋鎖的實現是爲了保護一段短小的臨界區操做代碼,保證這個臨界區的操做是原子的,從而避免併發的競爭冒險。在Linux內核中,自旋鎖一般用於包含內核數據結構的操做,你能夠看到在許多內核數據結構中都嵌入有spinlock,這些大部分就是用於保證它自身被操做的原子性,在操做這樣的結構體時都經歷這樣的過程:上鎖-操做-解鎖。數據結構
若是內核控制路徑發現自旋鎖「開着」(能夠獲取),就獲取鎖並繼續本身的執行。相反,若是內核控制路徑發現鎖由運行在另外一個CPU上的內核控制路徑「鎖着」,就在原地「旋轉」,反覆執行一條緊湊的循環檢測指令,直到鎖被釋放。 自旋鎖是循環檢測「忙等」,即等待時內核無事可作(除了浪費時間),進程在CPU上保持運行,因此它保護的臨界區必須小,且操做過程必須短。不過,自旋鎖一般很是方便,由於不少內核資源只鎖1毫秒的時間片斷,因此等待自旋鎖的釋放不會消耗太多CPU的時間。併發
從保證臨界區訪問原子性的目的來考慮,自旋鎖應該阻止在代碼運行過程當中出現的任何併發干擾。這些「干擾」包括:函數
1、中斷,包括硬件中斷和軟件中斷(僅在中斷代碼可能訪問臨界區時須要)
這種干擾存在於任何系統中,一箇中斷的到來致使了中斷例程的執行,若是在中斷例程中訪問了臨界區,原子性就被打破了。因此若是在某種中斷例程中存在訪問某個臨界區的代碼,那麼就必須用spinlock保護。對於不一樣的中斷類型(硬件中斷和軟件中斷)對應於不一樣版本的自旋鎖實現,其中包含了中斷禁用和開啓的代碼。可是若是你保證沒有中斷代碼會訪問臨界區,那麼使用不帶中斷禁用的自旋鎖API便可。
2、內核搶佔(僅存在於可搶佔內核中)
在2.6之後的內核中,支持內核搶佔,而且是可配置的。這使UP系統和SMP相似,會出現內核態下的併發。這種狀況下進入臨界區就須要避免因搶佔形成的併發,因此解決的方法就是在加鎖時禁用搶佔(preempt_disable(); ),在開鎖時開啓搶佔(preempt_enable();注意此時會執行一次搶佔調度)。
3、其餘處理器對同一臨界區的訪問(僅SMP系統)
在SMP系統中,多個物理處理器同時工做,致使可能有多個進程物理上的併發。這樣就須要在內存加一個標誌,每一個須要進入臨界區的代碼都必須檢查這個標誌,看是否有進程已經在這個臨界區中。這種狀況下檢查標誌的代碼也必須保證原子和快速,這就要求必須精細地實現,正常狀況下每一個構架都有本身的彙編實現方案,保證檢查的原子性。
有些人會覺得自旋鎖的自旋檢測能夠用for實現,這種想法「Too young, too simple, sometimes naive」!你能夠在理論上用C去解釋,可是若是用for,起碼會有以下兩個問題:
(1)你如何保證在SMP下其餘處理器不會同時訪問同一個的標誌呢?(也就是標誌的獨佔訪問)
(2)必須保證每一個處理器都不會去讀取高速緩存而是真正的內存中的標誌(能夠實現,編程上能夠用volitale)
要根本解決這個問題,須要在芯片底層實現物理上的內存地址獨佔訪問,而且在實現上使用特殊的彙編指令訪問。請看參考資料中對於自旋鎖的實現分析。以arm爲例,從存在SMP的ARM構架指令集開始(V六、V7),採用LDREX和STREX指令實現真正的自旋等待。
根據上的介紹,咱們很容易知道自旋鎖的組成:
-
中斷控制(僅在中斷代碼可能訪問臨界區時須要)
-
搶佔控制(僅存在於可搶佔內核中須要)
-
自旋鎖標誌控制 (僅SMP系統須要)
中斷控制是按代碼訪問臨界區的不一樣而在編程時選用不一樣的變體,有些API中有,有些沒有。
而搶佔控制和自旋鎖標誌控制依據內核配置(是否支持內核搶佔)和硬件平臺(是否爲SMP)的不一樣而在編譯時肯定。若是不須要,相應的控制代碼就編譯爲空函數。 對於非搶佔式內核,由自旋鎖所保護的每一個臨界區都有禁止內核搶佔的API,可是爲空操做。因爲UP系統不存在物理上的並行,因此能夠閹割掉自旋的部分,剩下搶佔和中斷操做部分便可。
- 到這裏其實就能夠解釋爲何我開始的實驗現象和預想的徹底不一樣了:
- 因爲UP系統(在不配置CONFIG_DEBUG_SPINLOCK的狀況下),根本就沒有自旋鎖控制的部分,屢次得到自旋鎖是可能的(這種編程原本就是錯誤的,只是我想看錯誤的現象而已)。
- 對於其中的一點疑惑:
-
- 一、在有禁用中斷的版本中,既然已經禁用了中斷,在本處理器上就不會被打斷,禁用搶佔是否多餘?
-
- (1)禁用了中斷能夠避免由於中斷引發的搶佔調度,可是若是在自旋鎖保護的臨界區中存在 preempt_disable();和 preempt_enable();對。這樣在preempt_enable();就會引起搶佔調度。
-
- (2)避免SMP系統中別的處理器執行調度程序使得本處理器的進程會被調度出去。?????
-
- 對於這個問題我不是很肯定,還有深刻研究調度系統後纔會有準確的答案。
不管是搶佔式UP、非搶佔式UP仍是SMP系統,只要在某類中斷代碼可能訪問臨界區,就須要控制中斷,保證操做的原子性。因此這個和模塊代碼中臨界區的訪問還有關係,是否可能在中斷中操做臨界區,只有程序員才知道。因此自旋鎖API中有針對不一樣中斷類型的自旋鎖變體:
- 不會在任何中斷例程中操做臨界區:
-
- static inline void spin_lock(spinlock_t*lock)
-
- static inline void spin_unlock(spinlock_t*lock)
-
- 若是在軟件中斷中操做臨界區:
-
- static inline void spin_lock_bh(spinlock_t*lock)
-
- static inline void spin_unlock_bh(spinlock_t*lock)
-
- bh表明bottom half,也就是中斷中的底半部,因內核中斷的底半部通常經過軟件中斷(tasklet等)來處理而得名。
-
- 若是在硬件中斷中操做臨界區:
-
- static inline void spin_lock_irq(spinlock_t*lock)
-
- static inline void spin_unlock_irq(spinlock_t*lock)
-
- 若是在控制硬件中斷的時候須要同時保存中斷狀態:
-
- spin_lock_irqsave(lock, flags)
-
- static inline void spin_unlock_irqrestore(spinlock_t*lock, unsigned long flags)
這些狀況描訴彷佛有點簡單,我在網上找到了一篇使用規則((轉)自旋鎖(spinlock)解釋得經典,透徹),很是詳細。我稍做修改,轉載以下:
-
得到自旋鎖和釋放自旋鎖有好幾個版本,所以讓讀者知道在什麼樣的狀況下使用什麼版本的得到和釋放鎖的宏是很是必要的。
若是被保護的共享資源只在進程上下文訪問和軟中斷(包括tasklet、timer)上下文訪問,那麼當在進程上下文訪問共享資源時,可能被軟中斷打斷,從而可能進入軟中斷上下文來對被保護的共享資源訪問,所以對於這種狀況,對共享資源的訪問必須使用spin_lock_bh和spin_unlock_bh來保護。固然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也能夠,它們失效了本地硬中斷,失效硬中斷隱式地也失效了軟中斷。可是使用spin_lock_bh和spin_unlock_bh是最恰當的,它比其餘兩個快。
若是被保護的共享資源只在兩個或多個tasklet或timer上下文訪問,那麼對共享資源的訪問僅須要用spin_lock和spin_unlock來保護,沒必要使用_bh版本,由於當tasklet或timer運行時,不可能有其餘tasklet或timer在當前CPU上運行。
若是被保護的共享資源只在一個tasklet或timer上下文訪問,那麼不須要任何自旋鎖保護,由於同一個tasklet或timer只能在一個CPU上運行,即便是在SMP環境下也是如此。實際上tasklet在調用tasklet_schedule標記其須要被調度時已經把該tasklet綁定到當前CPU,所以同一個tasklet決不可能同時在其餘CPU上運行。timer也是在其被使用add_timer添加到timer隊列中時已經被幫定到當前CPU,因此同一個timer毫不可能運行在其餘CPU上。固然同一個tasklet有兩個實例同時運行在同一個CPU就更不可能了。
若是被保護的共享資源只在一個軟中斷(tasklet和timer除外)上下文訪問,那麼這個共享資源須要用spin_lock和spin_unlock來保護,由於一樣的軟中斷能夠同時在不一樣的CPU上運行。
若是被保護的共享資源在兩個或多個軟中斷上下文訪問,那麼這個共享資源固然更須要用spin_lock和spin_unlock來保護,不一樣的軟中斷可以同時在不一樣的CPU上運行。
若是被保護的共享資源在軟中斷(包括tasklet和timer)或進程上下文和硬中斷上下文訪問,那麼在軟中斷或進程上下文訪問期間,可能被硬中斷打斷,從而進入硬中斷上下文對共享資源進行訪問,所以,在進程或軟中斷上下文須要使用spin_lock_irq和spin_unlock_irq來保護對共享資源的訪問。
而在中斷處理句柄中使用什麼版本,需依狀況而定,若是隻有一箇中斷處理句柄訪問該共享資源,那麼在中斷處理句柄中僅須要spin_lock和spin_unlock來保護對共享資源的訪問就能夠了。由於在執行中斷處理句柄期間,不可能被同一CPU上的軟中斷或進程打斷。
可是若是有不一樣的中斷處理句柄訪問該共享資源,那麼須要在中斷處理句柄中使用spin_lock_irq和spin_unlock_irq來保護對共享資源的訪問。
在使用spin_lock_irq和spin_unlock_irq的狀況下,徹底能夠用spin_lock_irqsave和spin_unlock_irqrestore取代,那具體應該使用哪個也須要依狀況而定,若是能夠確信在對共享資源訪問前中斷是使能的,那麼使用spin_lock_irq更好一些。由於它比spin_lock_irqsave要快一些,可是若是你不能肯定是否中斷使能,那麼使用spin_lock_irqsave和spin_unlock_irqrestore更好,由於它將恢復訪問共享資源前的中斷標誌而不是直接使能中斷。
固然,有些狀況下須要在訪問共享資源時必須中斷失效,而訪問完後必須中斷使能,這樣的情形使用spin_lock_irq和spin_unlock_irq最好。
spin_lock用於阻止在不一樣CPU上的執行單元對共享資源的同時訪問以及不一樣進程上下文互相搶佔致使的對共享資源的非同步訪問,而中斷失效和軟中斷失效倒是爲了阻止在同一CPU上軟中斷或中斷對共享資源的非同步訪問。
以上是我對自旋鎖的理解和使用上的總結,對與自旋鎖的實現,其實網上已經有之類文章了,我不廢話。因爲自旋鎖涉及到內核搶佔,全部最好仍是學習如下搶佔的相關知識。參考資料以下:
分析Linux中Spinlock在ARM及X86平臺上的實現
ARM的SWP和LDREX STREX指令