【轉】自旋鎖spin_lock和raw_spin_lock


本文轉自http://blog.csdn.net/droidphone/article/details/7395983

 

本文不打算詳細探究spin_lock的詳細實現機制,只是最近對raw_spin_lock的出現比較困擾,搞不清楚什么時候用spin_lock,什么時候用raw_spin_lock,因此有了這篇文章。

 

/*****************************************************************************************************/
聲明:本博內容均由http://blog.csdn.net/droidphone原創,轉載請注明出處,謝謝!
/*****************************************************************************************************/

1.  臨界區(Critical Section)

我們知道,臨界區是指某個代碼區間,在該區間中需要訪問某些共享的數據對象,又或者是總線,硬件寄存器等,通常這段代碼區間的范圍要控制在盡可能小的范圍內。臨界區內需要對這些數據對象和硬件對象的訪問進行保護,保證在退出臨界區前不會被臨界區外的代碼對這些對象進行修改。出現以下幾種情形時,我們需要使用臨界區進行保護:
  • (1)  在可以搶占(preemption)的系統中,兩個線程同時訪問同一個對象;
  • (2)  線程和中斷同時訪問同一個對象;
  • (3)  在多核系統中(SMP),可能兩個CPU可能同時訪問同一個對象;

2.  自旋鎖(spin_lock)

針對單處理器系統,對第一種情況,只要臨時關閉系統搶占即可,我們可以使用以下方法:
[cpp]  view plain  copy
 
  1. preempt_disable();  
  2. .....  
  3. // 訪問共享對象的代碼  
  4. ......  
  5. preempt_enable();  

 

同樣地,針對單處理器系統,第二種情況,只要臨時關閉中斷即可,我們可以使用以下方法:

 

[cpp]  view plain  copy
 
  1. local_irq_disable();  
  2. ......  
  3. // 訪問共享對象的代碼  
  4. ......  
  5. local_irq_enable();  

 

那么,針對多處理器的系統,以上的方法還成立么?答案很顯然:不成立。

對於第一種情況,雖然搶占被禁止了,可是另一個CPU上還有線程在運行,如果這個線程也正好要訪問該共享對象,上面的代碼段顯然是無能為力了。

對於第二種情況,雖然本地CPU的中斷被禁止了,可是另一個CPU依然可以產生中斷,如果他的中斷服務程序也正好要訪問該共享對象,上面的代碼段也一樣無法對共享對象進行保護。

實際上,在linux中,上面所說的三種情況都可以用自旋鎖(spin_lock)解決。基本的自旋鎖函數對是:
  • spin_lock(spinlock_t *lock);
  • spin_unlock(spinlock_t *lock);
對於單處理器系統,在不打開調試選項時,spinlock_t實際上是一個空結構,把上面兩個函數展開后,實際上就只是調用preempt_disable()和preempt_enable(),對於單處理器系統,關掉搶占后,其它線程不能被調度運行,所以並不需要做額外的工作,除非中斷的到來,不過內核提供了另外的變種函數來處理中斷的問題。
 
對於多處理器系統,spinlock_t實際上等效於內存單元中的一個整數,內核保證spin_lock系列函數對該整數進行原子操作,除了調用preempt_disable()和preempt_enable()防止線程被搶占外,還必須對spinlock_t上鎖,這樣,如果另一個CPU的代碼要使用該臨界區對象,就必須進行自旋等待。
 
對於中斷和普通線程都要訪問的對象,內核提供了另外兩套變種函數:
  • spin_lock_irq(spinlock_t *lock);
  • spin_unlock_irq(spinlock_t *lock);
和:
  • spin_lock_irqsave(lock, flags);
  • spin_lock_irqrestore(lock, flags);
我們可以按以下原則使用上面的三對變種函數(宏):
  • 如果只是在普通線程之間同時訪問共享對象,使用spin_lock()/spin_unlock();
  • 如果是在中斷和普通線程之間同時訪問共享對象,並且確信退出臨界區后要打開中斷,使用spin_lock_irq()/spin_unlock_irq();
  • 如果是在中斷和普通線程之間同時訪問共享對象,並且退出臨界區后要保持中斷的狀態,使用spin_lock_irqsave()/spin_unlock_irqrestore();
其實變種還不止這幾個,還有read_lock_xxx/write_lock_xxx、spin_lock_bh/spin_unlock_bh、spin_trylock_xxx等等。但常用的就上面幾種。

3.  raw_spin_lock

在2.6.33之后的版本,內核加入了raw_spin_lock系列,使用方法和spin_lock系列一模一樣,只是參數有spinlock_t變為了raw_spinlock_t。而且在內核的主線版本中,spin_lock系列只是簡單地調用了raw_spin_lock系列的函數,但內核的代碼卻是有的地方使用spin_lock,有的地方使用raw_spin_lock。是不是很奇怪?要解答這個問題,我們要回到2004年,MontaVista Software, Inc的開發人員在郵件列表中提出來一個Real-Time Linux Kernel的模型,旨在提升Linux的實時性,之后Ingo Molnar很快在他的一個項目中實現了這個模型,並最終產生了一個Real-Time preemption的patch。
該模型允許在臨界區中被搶占,而且申請臨界區的操作可以導致進程休眠等待,這將導致自旋鎖的機制被修改,由原來的整數原子操作變更為信號量操作。當時內核中已經有大約10000處使用了自旋鎖的代碼,直接修改spin_lock將會導致這個patch過於龐大,於是,他們決定只修改哪些真正不允許搶占和休眠的地方,而這些地方只有100多處,這些地方改為使用raw_spin_lock,但是,因為原來的內核中已經有raw_spin_lock這一名字空間,用於代表體系相關的原子操作的實現,於是linus本人建議:
  • 把原來的raw_spin_lock改為arch_spin_lock;
  • 把原來的spin_lock改為raw_spin_lock;
  • 實現一個新的spin_lock;
寫到這里不知大家明白了沒?對於2.6.33和之后的版本,我的理解是:
  • 盡可能使用spin_lock;
  • 絕對不允許被搶占和休眠的地方,使用raw_spin_lock,否則使用spin_lock;
  • 如果你的臨界區足夠小,使用raw_spin_lock;
對於沒有打上Linux-RT(實時Linux)的patch的系統,spin_lock只是簡單地調用raw_spin_lock,實際上他們是完全一樣的,如果打上這個patch之后,spin_lock會使用信號量完成臨界區的保護工作,帶來的好處是同一個CPU可以有多個臨界區同時工作,而原有的體系因為禁止搶占的原因,一旦進入臨界區,其他臨界區就無法運行,新的體系在允許使用同一個臨界區的其他進程進行休眠等待,而不是強占着CPU進行自旋操作。寫這篇文章的時候,內核的版本已經是3.3了,主線版本還沒有合並Linux-RT的內容,說不定哪天就會合並進來,也為了你的代碼可以兼容Linux-RT,最好堅持上面三個原則。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM