linux內核同步機制中的概念介紹和方法

    Linux設備驅動中必須解決的一個問題是多個進程對共享資源的併發訪問,併發訪問會導致競態,linux提供了多種解決競態問題的方式,這些方式適合不同的應用場景。

 

Linux內核是多進程、多線程的操作系統,它提供了相當完整的內核同步方法。內核同步方法列表如下:

=========================

內核中採用的同步技術:    

中斷屏蔽

原子操作  (分爲整數原子操作和位 原子操作)

信號量  (semaphore)

RCU  (read-copy-update)

SMP系統中的同步機制 :
自旋鎖 (spin lock)

讀寫自旋鎖

順序鎖         (seqlock 只包含在2.6內核中)

讀寫信號量  (rw_semaphore)

大內核鎖BKL(Big Kernel Lock)

Seq鎖()。

==========================

一、概念介紹

 

併發與競態:

          併發(concurrency)指的是多個執行單元同時、並行被執行,而併發的執行單元對共享資源(硬件資源和軟件上的全局變量、靜態變量等)的訪問則很容易導致競態(race conditions)。

linux中,主要的競態發生在如下幾種情況:

1、對稱多處理器(SMP)多個CPU                特點是多個CPU使用共同的系統總線,因此可訪問共同的外設和存儲器。

2、CPU內進程與搶佔它的進程

3、中斷(硬中斷、軟中斷、Tasklet、底半部)與進程之間

只要併發的多個執行單元存在對共享資源的訪問,競態就有可能發生

如果中斷處理程序訪問進程正在訪問的資源,則競態也會會發生。

多箇中斷之間本身也可能引起併發而導致競態(中斷被更高優先級的中斷打斷)。 

解決競態問題的途徑是保證對共享資源的互斥訪問,所謂互斥訪問就是指一個執行單元在訪問共享資源的時候,其他的執行單元都被禁止訪問。 

訪問共享資源的代碼區域被稱爲臨界區,臨界區需要以某種互斥機制加以保護,如:中斷屏蔽,原子操作,自旋鎖,和信號量都是linux設備驅動中可採用的互斥途徑。 

臨界區和競爭條件:

所謂臨界區(critical regions)就是訪問和操作共享數據的代碼段,爲了避免在臨界區中併發訪問,編程者必須保證這些代碼原子地執行——也就是說,代碼在執行結束前不可被打斷,就如同整個臨界區是一個不可分割的指令一樣,如果兩個執行線程有可能處於同一個臨界區中,那麼就是程序包含一個bug,如果這種情況發生了,我們就稱之爲競爭條件(race conditions),避免併發和防止競爭條件被稱爲同步 

死鎖:

死鎖的產生需要一定條件:要有一個或多個執行線程和一個或多個資源,每個線程都在等待其中的一個資源,但所有的資源都已經被佔用了,所有線程都在相互等待,但它們永遠不會釋放已經佔有的資源,於是任何線程都無法繼續,這便意味着死鎖的發生。

=========================================================================================================================

詳細的同步方法如下:

一、中斷屏蔽

CPU範圍內避免競態的一種簡單方法是在進入臨界區之前屏蔽系統的中斷由於linux內核的進程調度等操作都依賴中斷來實現,內核搶佔進程之間的併發也就得以避免了。

中斷屏蔽的使用方法:

[html] view plaincopy
  1. local_irq_disable()//屏蔽中斷  
  2. //臨界區  
  3. local_irq_enable()//開中斷  
  4. 特點:由於<span style="font-family:Times New Roman;">linux</span>系統的異步<span style="font-family:Times New Roman;">IO</span>,進程調度等很多重要操作都依賴於中斷,在屏蔽中斷期間所有的中斷都無法得到處理,因此<strong>長時間的屏蔽是很危險</strong>的,有可能造成數據丟失甚至系統崩潰,這就要求在屏蔽中斷之後,當前的內核執行路徑應當儘快地執行完臨界區的代碼。  

中斷屏蔽只能禁止本CPU內的中斷,因此,並不能解決多CPU引發的競態,所以單獨使用中斷屏蔽並不是一個值得推薦的避免競態的方法,它一般和自旋鎖配合使用。

 

 二、原子操作

定義:原子操作指的是在執行過程中不會被別的代碼路徑所中斷的操作。(原子原本指的是不可分割的微粒,所以原子操作也就是不能夠被分割的指令)

(它保證指令原子的方式執行而不能被打斷)

原子操作是不可分割的,在執行完畢不會被任何其它任務或事件中斷。在單處理器系統(UniProcessor)中,能夠在單條指令中完成的操作都可以認爲是"原子操作",因爲中斷只能發生於指令之間。這也是某些CPU指令系統中引入了test_and_settest_and_clear等指令用於臨界資源互斥的原因。但是,在對稱多處理器(Symmetric Multi-Processor)結構中就不同了,由於系統中有多個處理器在獨立地運行,即使能在單條指令中完成的操作也有可能受到干擾。我們以decl (遞減指令)爲例,這是一個典型的"讀-改-寫"過程,涉及兩次內存訪問。

通俗理解:

      原子操作,顧名思義,就是說像原子一樣不可再細分。一個操作是原子操作,意思就是說這個操作是以原子的方式被執行,要一口氣執行完,執行過程不能夠被OS的其他行爲打斷,是一個整體的過程,在其執行過程中,OS的其它行爲是插不進來的。

分類:linux內核提供了一系列函數來實現內核中的原子操作,分爲整型原子操作位原子操作共同點是:在任何情況下操作都是原子的,內核代碼可以安全的調用它們而不被打斷。

 

原子整數操作:

針對整數的原子操作只能對atomic_t類型的數據進行處理,在這裏之所以引入了一個特殊的數據類型,而沒有直接使用C語言的int型,主要是出於兩個原因:

第一、讓原子函數只接受atomic_t類型的操作數,可以確保原子操作只與這種特殊類型數據一起使用,同時,這也確保了該類型的數據不會被傳遞給其它任何非原子函數;

第二、使用atomic_t類型確保編譯器不對相應的值進行訪問優化——這點使得原子操作最終接收到正確的內存地址,而不是一個別名,最後就是在不同體系結構上實現原子操作的時候,使用atomic_t可以屏蔽其間的差異。

原子整數操作最常見的用途就是實現計數器。

另一點需要說明原子操作只能保證操作是原子的,要麼完成,要麼不完成,不會有操作一半的可能,但原子操作並不能保證操作的順序性,即它不能保證兩個操作是按某個順序完成的。如果要保證原子操作的順序性,請使用內存屏障指令。

atomic_tATOMIC_INIT(i)定義如下:

[html] view plaincopy
  1. typedef struct { volatile int counter; } atomic_t;  
  2. #define ATOMIC_INIT(i)  { (i) }  

=========================整數相關函數: ===========================================          

[html] view plaincopy
  1. ATOMIC_INIT(int i)                                         //聲明一個atomic_t變量並且初始化爲i  
  2.          int atomic_read(atomic_t *v)                      //原子地讀取整數變量v  
  3.          void atomic_set(atomic_t *v, int i)               //原子地設置v爲i  
  4.          void atomic_add(int i, atomic_t *v)         //v+i  
  5.          void atomic_sub(int i, atomic_t *v)         //v-i  
  6.          void atomic_inc(atomic_t *v)                //v+1  
  7.          void atomic_dec(atomic_t *v)                //v-1  
  8.          int atomic_sub_and_test(int i, atomic_t *v)       //v-i, 結果等於0,返回真,否則返回假   
  9.            int atomic_add_negative(int i, atomic_t *v)       //原子地給v+i,結果是負數,返回真,否則返回假  
  10.            int atomic_dec_and_test(atomic_t *v)              //v-1, 結果是0,返回真,否則返回假  
  11.            int atomic_inc_and_test(atomic_t *v)              //v+1,如果是0,返回真,否則返回假  

在你編寫代碼的時候,能使用原子操作的時候,就儘量不要使用複雜的加鎖機制,對多數體系結構來講,原子操作與更復雜的同步方法相比較,給系統帶來的開銷小,對高速緩存行的影響也小,但是,對於那些有高性能要求的代碼,對多種同步方法進行測試比較,不失爲一種明智的作法。

 

原子位操作:

針對位這一級數據進行操作的函數,是對普通的內存地址進行操作的。它的參數是一個指針和一個位號。 

=================================位操作相關函數:====================================

[html] view plaincopy
  1. void set_bit(int nr, void *addr)                  //原子地設置addr所指對象的第nr位  
  2. void clear_bit(int nr, void *addr)                  //清空addr第nr位  
  3. void change_bit(int nr, void *addr)                 //反轉addr第nr位  
  4. int test_and _set_bit(int nr, void *addr)           //設置addr第nr位,並返回原先的位的值  
  5. int test_and_clear_bit(int nr, void *addr)          //清除addr第nr位,並返回原先的值  
  6. int test_and_change_bit(int nr, void *addr)         //反轉addr第nr,並返回原先的值  
  7. int test_bit(int nr, void *addr)                            //原子地返回addr的第nr位  

爲方便其間,內核還提供了一組與上述操作對應的非原子位函數,非原子位函數與原子位函數的操作完全相同,但是,非原子位函數不保證原子性,且其名字前綴多兩個下劃線。例如,與test_bit()對應的非原子形式是_test_bit(),如果你不需要原子性操作(比如,如果你已經用鎖保護了自己的數據),那麼這些非原子的位函數相比原子的位函數可能會執行得更快些。

 使用示例:

創建和初始化原子變量                    
atomic_t my_counter ATOMIC_INIT(0);    //聲明一個atomic_t類型的變量my_counter 並且初始化爲0
或者  
atomic_set( &my_counter, 0 );  

[html] view plaincopy
  1. <span style="font-size:12px;color:#000000;">簡單的算術原子函數                      
  2. val = atomic_read( &my_counter );      
  3. atomic_add( 1, &my_counter );      
  4. atomic_inc( &my_counter );      
  5. atomic_sub( 1, &my_counter );      
  6. atomic_dec( &my_counter );    
  7. </span>  

三、自旋鎖(spin lock

自旋鎖的引入:

如果每個臨界區都能像增加變量這樣簡單就好了,可惜現實不是這樣,而是臨界區可以跨越多個函數,例如:先得從一個數據結果中移出數據,對其進行格式轉換和解析,最後再把它加入到另一個數據結構中,整個執行過程必須是原子的,在數據被更新完畢之前,不能有其他代碼讀取這些數據,顯然,簡單的原子操作是無能爲力的(在單處理器系統(UniProcessor)中,能夠在單條指令中完成的操作都可以認爲是"原子操作",因爲中斷只能發生於指令之間),這就需要使用更爲複雜的同步方法——鎖來提供保護。 

自旋鎖的介紹:

Linux內核中最常見的鎖是自旋鎖(spin lock),自旋鎖最多隻能被一個可執行線程持有,如果一個執行線程試圖獲得一個被爭用(已經被持有)的自旋鎖,那麼該線程就會一直進行忙循環旋轉等待鎖重新可用,要是鎖未被爭用,請求鎖的執行線程便能立刻得到它,繼續執行,在任意時間,自旋鎖都可以防止多於一個的執行線程同時進入理解區,注意同一個鎖可以用在多個位置例如,對於給定數據的所有訪問都可以得到保護和同步。

一個被爭用的自旋鎖使得請求它的線程在等待鎖重新可用時自旋(特別浪費處理器時間),所以自旋鎖不應該被長時間持有,事實上,這點正是使用自旋鎖的初衷,在短期間內進行輕量級加鎖,還可以採取另外的方式來處理對鎖的爭用:讓請求線程睡眠,直到鎖重新可用時再喚醒它,這樣處理器就不必循環等待,可以去執行其他代碼,這也會帶來一定的開銷——這裏有兩次明顯的上下文切換,被阻塞的線程要換出和換入。因此,持有自旋鎖的時間最好小於完成兩次上下文切換的耗時,當然我們大多數人不會無聊到去測量上下文切換的耗時,所以我們讓持有自旋鎖的時間應儘可能的短就可以了,信號量可以提供上述第二種機制,它使得在發生爭用時,等待的線程能投入睡眠,而不是旋轉。

自旋鎖可以使用在中斷處理程序中(此處不能使用信號量,因爲它們會導致睡眠),在中斷處理程序中使用自旋鎖時,一定要在獲取鎖之前,首先禁止本地中斷(在當前處理器上的中斷請求),否則,中斷處理程序就會打斷正持有鎖的內核代碼,有可能會試圖去爭用這個已經持有的自旋鎖,這樣以來,中斷處理程序就會自旋,等待該鎖重新可用,但是鎖的持有者在這個中斷處理程序執行完畢前不可能運行,這正是我們在前一章節中提到的雙重請求死鎖,注意,需要關閉的只是當前處理器上的中斷,如果中斷髮生在不同的處理器上,即使中斷處理程序在同一鎖上自旋,也不會妨礙鎖的持有者(在不同處理器上)最終釋放鎖。 

自旋鎖的簡單理解:

理解自旋鎖最簡單的方法是把它作爲一個變量看待,該變量把一個臨界區或者標記爲我當前正在運行,請稍等一會或者標記爲我當前不在運行,可以被使用。如果A執行單元首先進入例程,它將持有自旋鎖,當B執行單元試圖進入同一個例程時,將獲知自旋鎖已被持有,需等到A執行單元釋放後才能進入 

自旋鎖的API函數: 

其實介紹的幾種信號量和互斥機制,其底層源碼都是使用自旋鎖,可以理解爲自旋鎖的再包裝。所以從這裏就可以理解爲什麼自旋鎖通常可以提供比信號量更高的性能。
自旋鎖是一個互斥設備,他只能會兩個值:鎖定解鎖。它通常實現爲某個整數之中的單個位。
測試並設置的操作必須以原子方式完成。
任何時候,只要內核代碼擁有自旋鎖,在相關CPU上的搶佔就會被禁止。

適用於自旋鎖的核心規則:
1)任何擁有自旋鎖的代碼都必須使原子的,除服務中斷外(某些情況下也不能放棄CPU,如中斷服務也要獲得自旋鎖。爲了避免這種鎖陷阱,需要在擁有自旋鎖時禁止中斷),不能放棄CPU(如休眠,休眠可發生在許多無法預期的地方)。否則CPU將有可能永遠自旋下去(死機)。
2)擁有自旋鎖的時間越短越好。


需要強調的是,自旋鎖設計用於多處理器的同步機制,對於單處理器(對於單處理器並且不可搶佔的內核來說,自旋鎖什麼也不作),內核在編譯時不會引入自旋鎖機制,對於可搶佔的內核,它僅僅被用於設置內核的搶佔機制是否開啓的一個開關,也就是說加鎖和解鎖實際變成了禁止或開啓內核搶佔功能。如果內核不支持搶佔,那麼自旋鎖根本就不會編譯到內核中。
內核中使用spinlock_t類型來表示自旋鎖,它定義在<linux/spinlock_types.h>

[html] view plaincopy
  1. typedef struct {  
  2.     raw_spinlock_t raw_lock;  
  3. #if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP)  
  4.     unsigned int break_lock;  
  5. #endif  
  6. } spinlock_t;  
  7.    

對於不支持SMP的內核來說,struct raw_spinlock_t什麼也沒有,是一個空結構。對於支持多處理器的內核來說,struct raw_spinlock_t定義爲

[html] view plaincopy
  1. typedef struct {  
  2.     unsigned int slock;  
  3. } raw_spinlock_t;<span style="color:#2a2a2a;"><span style="font-family:Times New Roman;"> </span></span>  

slock表示了自旋鎖的狀態,“1”表示自旋鎖處於解鎖狀態(UNLOCK),“0”表示自旋鎖處於上鎖狀態(LOCKED)。
break_lock
表示當前是否由進程在等待自旋鎖,顯然,它只有在支持搶佔的SMP內核上才起作用。
    
自旋鎖的實現是一個複雜的過程,說它複雜不是因爲需要多少代碼或邏輯來實現它,其實它的實現代碼很少。自旋鎖的實現跟體系結構關係密切,核心代碼基本也是由彙編語言寫成,與體協結構相關的核心代碼都放在相關的<asm/>目錄下,比如<asm/spinlock.h>。對於我們驅動程序開發人員來說,我們沒有必要了解這麼spinlock的內部細節,如果你對它感興趣,請參考閱讀Linux內核源代碼。對於我們驅動的spinlock接口,我們只需包括<linux/spinlock.h>頭文件。在我們詳細的介紹spinlockAPI之前,我們先來看看自旋鎖的一個基本使用格式:

 使用示例:

[html] view plaincopy
  1. #include <linux/spinlock.h>  
  2. spinlock_t mr_lock = SPIN_LOCK_UNLOCKED;  
  3. spin_lock(&mr_lock);  
  4. /*  
  5. 臨界區  
  6. */  
  7. spin_unlock(&mr_lock);  

從使用上來說,spinlockAPI還很簡單的,一般我們會用的的API如下表,其實它們都是定義在<linux/spinlock.h>中的宏接口,真正的實現在<asm/spinlock.h>

  1. #include <linux/spinlock.h>
    SPIN_LOCK_UNLOCKED
    DEFINE_SPINLOCK
    spin_lock_init( spinlock_t *)
    spin_lock(spinlock_t *)
    spin_unlock(spinlock_t *)
    spin_lock_irq(spinlock_t *)
    spin_unlock_irq(spinlock_t *)
    spin_lock_irqsace(spinlock_t *
    unsigned long flags)
    spin_unlock_irqsace(spinlock_t *, unsigned long flags)
    spin_trylock(spinlock_t *)
    spin_is_locked(spinlock_t *)
     

初始化

spinlock有兩種初始化形式,一種是靜態初始化,一種是動態初始化。對於靜態的spinlock對象,我們用 SPIN_LOCK_UNLOCKED來初始化,它是一個宏。當然,我們也可以把聲明spinlock和初始化它放在一起做,這就是 DEFINE_SPINLOCK宏的工作,因此,下面的兩行代碼是等價的。

[html] view plaincopy
  1. DEFINE_SPINLOCK (lock);  
  2. spinlock_t lock = SPIN_LOCK_UNLOCKED;  

spin_lock_init函數一般用來初始化動態創建的spinlock_t對象,它的參數是一個指向spinlock_t對象的指針。當然,它也可以初始化一個靜態的沒有初始化的spinlock_t對象。

[html] view plaincopy
  1. spinlock_t *lock  
  2. ......  
  3. spin_lock_init(lock);  

獲取鎖

內核提供了多個函數用於獲取一個自旋鎖。

[html] view plaincopy
  1. spin_try_lock()        試圖獲得某個特定的自旋鎖,如果該鎖已經被爭用,該方法會立刻返回一個非0值,而不會自旋等待鎖被釋放,如果成果獲得了這個鎖,那麼就返回0.  
  2. spin_is_locked()          //方法和spin_try_lock()是一樣的功效,該方法只做判斷,並不生效.<span style="color:#2a2a2a;">如果是則返回非<span style="font-family:Times New Roman;">0</span>,否則,返回<span style="font-family:Times New Roman;">0</span></span>  
  3. spin_lock();              //獲取指定的自旋鎖  
  4. spin_lock_irq();          //禁止本地中斷獲取指定的鎖  
  5. spin_lock_irqsave();      //保存本地中斷的狀態,禁止本地中斷,並獲取指定的鎖  


自旋鎖是可以使用在中斷處理程序中的,這時需要使用具有關閉本地中斷功能的函數,我們推薦使用 spin_lock_irqsave,因爲它會保存加鎖前的中斷標誌,這樣就會正確恢復解鎖時的中斷標誌。如果spin_lock_irq在加鎖時中斷是關閉的,那麼在解鎖時就會錯誤的開啓中斷。

釋放鎖

同獲取鎖相對應,內核提供了三個相對的函數來釋放自旋鎖。

[html] view plaincopy
  1. <span style="color:#000000;">spin_unlock:釋放指定的自旋鎖。  
  2. spin_unlock_irq:釋放自旋鎖並激活本地中斷。  
  3. spin_unlock_irqsave:釋放自旋鎖,並恢復保存的本地中斷狀態。</span>  


四、讀寫自旋鎖

如果臨界區保護的數據是可讀可寫的,那麼只要沒有寫操作,對於讀是可以支持併發操作的。對於這種只要求寫操作是互斥的需求,如果還是使用自旋鎖顯然是無法滿足這個要求(對於讀操作實在是太浪費了)。爲此內核提供了另一種鎖-讀寫自旋鎖,讀自旋鎖也叫共享自旋鎖,寫自旋鎖也叫排他自旋鎖。

讀寫自旋鎖是一種比自旋鎖粒度更小的鎖機制,它保留了“自旋”的概念,但是在寫操作方面,只能最多有一個寫進程,在讀操作方面,同時可以有多個讀執行單元,當然,讀和寫也不能同時進行。
    
讀寫自旋鎖的使用也普通自旋鎖的使用很類似,首先要初始化讀寫自旋鎖對象:

[html] view plaincopy
  1. //靜態初始化  
  2. rwlock_t rwlock = RW_LOCK_UNLOCKED;  
  3. //動態初始化  
  4. rwlock_t *rwlock;  
  5. ...  
  6. rw_lock_init(rwlock);  


 在讀操作代碼裏對共享數據獲取讀自旋鎖:

[html] view plaincopy
  1. read_lock(&rwlock);  
  2. ...  
  3. read_unlock(&rwlock);  


 在寫操作代碼裏爲共享數據獲取寫自旋鎖:

[html] view plaincopy
  1. write_lock(&rwlock);  
  2. ...  
  3. write_unlock(&rwlock);  


 需要注意的是,如果有大量的寫操作,會使寫操作自旋在寫自旋鎖上而處於寫飢餓狀態(等待讀自旋鎖的全部釋放),因爲讀自旋鎖會自由的獲取讀自旋鎖。

讀寫自旋鎖的函數類似於普通自旋鎖,這裏就不一一介紹了,我們把它列在下面的表中。

[html] view plaincopy
  1. RW_LOCK_UNLOCKED  
  2. rw_lock_init(rwlock_t *)  
  3. read_lock(rwlock_t *)  
  4. read_unlock(rwlock_t *)  
  5. read_lock_irq(rwlock_t *)  
  6. read_unlock_irq(rwlock_t *)  
  7. read_lock_irqsave(rwlock_t *, unsigned long)  
  8. read_unlock_irqsave(rwlock_t *, unsigned long)  
  9. write_lock(rwlock_t *)  
  10. write_unlock(rwlock_t *)  
  11. write_lock_irq(rwlock_t *)  
  12. write_unlock_irq(rwlock_t *)  
  13. write_lock_irqsave(rwlock_t *, unsigned long)  
  14. write_unlock_irqsave(rwlock_t *, unsigned long)  
  15. rw_is_locked(rwlock_t *)  

五、順序瑣

順序瑣(seqlock)是對讀寫鎖的一種優化,若使用順序瑣,讀執行單元絕不會被寫執行單元阻塞,也就是說,讀執行單元可以在寫執行單元對被順序瑣保護的共享資源進行寫操作時仍然可以繼續讀,而不必等待寫執行單元完成寫操作,寫執行單元也不需要等待所有讀執行單元完成讀操作纔去進行寫操作。但是,寫執行單元與寫執行單元之間仍然是互斥的,即如果有寫執行單元在進行寫操作,其它寫執行單元必須自旋在哪裏,直到寫執行單元釋放了順序瑣。

如果讀執行單元在讀操作期間,寫執行單元已經發生了寫操作,那麼,讀執行單元必須重新讀取數據,以便確保得到的數據是完整的,這種鎖在讀寫同時進行的概率比較小時,性能是非常好的,而且它允許讀寫同時進行,因而更大的提高了併發性,

注意,順序瑣由一個限制,就是它必須被保護的共享資源不含有指針,因爲寫執行單元可能使得指針失效,但讀執行單元如果正要訪問該指針,將導致Oops

六、信號量

Linux中的信號量是一種睡眠鎖,如果有一個任務試圖獲得一個已經被佔用的信號量時,信號量會將其推進一個等待隊列,然後讓其睡眠,這時處理器能重獲自由,從而去執行其它代碼,當持有信號量的進程將信號量釋放後,處於等待隊列中的哪個任務被喚醒,並獲得該信號量。

信號量,或旗標,就是我們在操作系統裏學習的經典的P/V原語操作。
P
:如果信號量值大於0,則遞減信號量的值,程序繼續執行,否則,睡眠等待信號量大於0
V
:遞增信號量的值,如果遞增的信號量的值大於0,則喚醒等待的進程。


  
  信號量的值確定了同時可以有多少個進程可以同時進入臨界區,如果信號量的初始值始1,這信號量就是互斥信號量(MUTEX)。對於大於1的非0值信號量,也可稱爲計數信號量(counting semaphore)。對於一般的驅動程序使用的信號量都是互斥信號量。

類似於自旋鎖,信號量的實現也與體系結構密切相關,具體的實現定義在<asm/semaphore.h>頭文件中,對於x86_32系統來說,它的定義如下:

[html] view plaincopy
  1. struct semaphore {  
  2.    <span style="color:#ff9900;">atomic_t</span> count;  
  3.    int sleepers;  
  4.    wait_queue_head_t wait;  
  5. };  


 信號量的初始值countatomic_t類型的,這是一個原子操作類型,它也是一個內核同步技術,可見信號量是基於原子操作的。我們會在後面原子操作部分對原子操作做詳細介紹。

信號量的使用類似於自旋鎖,包括創建、獲取和釋放。我們還是來先展示信號量的基本使用形式:

[html] view plaincopy
  1. static DECLARE_MUTEX(my_sem);  
  2. ......  
  3. if (down_interruptible(&my_sem))  
  4. {  
  5.     return -ERESTARTSYS;  
  6. }  
  7. ......  
  8. up(&my_sem);  


 Linux內核中的信號量函數接口如下:

static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);
seam_init(struct semaphore *, int);
init_MUTEX(struct semaphore *);
init_MUTEX_LOCKED(struct semaphore *)
down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)

初始化信號量

信號量的初始化包括靜態初始化和動態初始化。靜態初始化用於靜態的聲明並初始化信號量。

static DECLARE_SEMAPHORE_GENERIC(name, count);
static DECLARE_MUTEX(name);

 

對於動態聲明或創建的信號量,可以使用如下函數進行初始化:

seam_init(sem, count);
init_MUTEX(sem);
init_MUTEX_LOCKED(struct semaphore *)

 

顯然,帶有MUTEX的函數始初始化互斥信號量。LOCKED則初始化信號量爲鎖狀態。

使用信號量

[html] view plaincopy
  1. down_interruptible(struct semaphore *);          
  2.                                    <span style="color:#3366ff;">使進程進入可中斷的睡眠狀態。關於進程狀態的詳細細節,我們在內核的進程管理裏在做詳細介紹。  
  3. </span>down(struct semaphore *)           嘗試獲取指定的信號量,如果信號量已經被使用了,則進程進入不可中斷的睡眠狀態。  
  4. down_trylock(struct semaphore *)   嘗試獲取信號量,如果獲取成功則返回0,失敗則會立即返回非0。  
  5. up(struct semaphore *)             釋放信號量,如果信號量上的睡眠隊列不爲空,則喚醒其中一個等待進程。  



七、讀寫信號量

類似於自旋鎖,信號量也有讀寫信號量。讀寫信號量API定義在<linux/rwsem.h>頭文件中,它的定義其實也是體系結構相關的,因此具體實現定義在<asm/rwsem.h>頭文件中,以下是x86的例子:

struct rw_semaphore {
    signed long       count;
    spinlock_t       wait_lock;
    struct list_head    wait_list;
};

 

首先要說明的是所有的讀寫信號量都是互斥信號量。讀鎖是共享鎖,就是同時允許多個讀進程持有該信號量,但寫鎖是獨佔鎖,同時只能有一個寫鎖持有該互斥信號量。顯然,寫鎖是排他的,包括排斥讀鎖。由於寫鎖是共享鎖,它允許多個讀進程持有該鎖,只要沒有進程持有寫鎖,它就始終會成功持有該鎖,因此這會造成寫進程寫飢餓狀態。

在使用讀寫信號量前先要初始化,就像你所想到的,它在使用上幾乎與讀寫自旋鎖一致。先來看看讀寫信號量的創建和初始化:

//靜態初始化
static DECLARE_RWSEM(rwsem_name);

// 
動態初始化
static struct rw_semaphore rw_sem

init_rwsem(&rw_sem);

 

讀進程獲取信號量保護臨界區數據:

down_read(&rw_sem);
...
up_read(&rw_sem);

 

寫進程獲取信號量保護臨界區數據:

down_write(&rw_sem);
...
up_write(&rw_sem);

 

更多的讀寫信號量API請參考下表:

#include <linux/rwsem.h>

DECLARE_RWSET(name);
init_rwsem(struct
  rw_semaphore *);
void down_read(struct rw_semaphore *sem);
void down_write(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);

 

同自旋鎖一樣,down_read_trylockdown_write_trylock會嘗試着獲取信號量,如果獲取成功則返回1,否則返回0。奇怪爲什麼返回值與信號量的對應函數相反,使用是一定要小心這點。

 

九、自旋鎖和信號量區別

在驅動程序中,當多個線程同時訪問相同的資源時(驅動程序中的全局變量是一種典型的共享資源),可能會引發"競態",因此我們必須對共享資源進行併發控制。Linux內核中解決併發控制的最常用方法是自旋鎖與信號量(絕大多數時候作爲互斥鎖使用)。

  自旋鎖與信號量"類似而不類",類似說的是它們功能上的相似性,"不類"指代它們在本質和實現機理上完全不一樣,不屬於一類。

  自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環查看是否該自旋鎖的保持者已經釋放了鎖,"自旋"就是"在原地打轉"。而信號量則引起調用者睡眠,它把進程從運行隊列上拖出去,除非獲得鎖。這就是它們的"不類"

  但是,無論是信號量,還是自旋鎖,在任何時刻,最多只能有一個保持者,即在任何時刻最多只能有一個執行單元獲得鎖。這就是它們的"類似"

  鑑於自旋鎖與信號量的上述特點,一般而言,自旋鎖適合於保持時間非常短的情況,它可以在任何上下文使用;信號量適合於保持時間較長的情況,會只能在進程上下文使用。如果被保護的共享資源只在進程上下文訪問,則可以以信號量來保護該共享資源,如果對共享資源的訪問時間非常短,自旋鎖也是好的選擇。但是,如果被保護的共享資源需要在中斷上下文訪問(包括底半部即中斷處理句柄和頂半部即軟中斷),就必須使用自旋鎖。

區別總結如下:

1、由於爭用信號量的進程在等待鎖重新變爲可用時會睡眠,所以信號量適用於鎖會被長時間持有的情況。

2、相反,鎖被短時間持有時,使用信號量就不太適宜了,因爲睡眠引起的耗時可能比鎖被佔用的全部時間還要長。

3、由於執行線程在鎖被爭用時會睡眠,所以只能在進程上下文中才能獲取信號量鎖,因爲在中斷上下文中(使用自旋鎖)是不能進行調度的。

4、你可以在持有信號量時去睡眠(當然你也可能並不需要睡眠),因爲當其它進程試圖獲得同一信號量時不會因此而死鎖,(因爲該進程也只是去睡眠而已,而你最終會繼續執行的)。

5、在你佔用信號量的同時不能佔用自旋鎖,因爲在你等待信號量時可能會睡眠,而在持有自旋鎖時是不允許睡眠的。

6、信號量鎖保護的臨界區可包含可能引起阻塞的代碼,而自旋鎖則絕對要避免用來保護包含這樣代碼的臨界區,因爲阻塞意味着要進行進程的切換,如果進程被切換出去後,另一進程企圖獲取本自旋鎖,死鎖就會發生。

7、信號量不同於自旋鎖,它不會禁止內核搶佔(自旋鎖被持有時,內核不能被搶佔),所以持有信號量的代碼可以被搶佔,這意味着信號量不會對調度的等待時間帶來負面影響。

除了以上介紹的同步機制方法以外,還有BKL(大內核鎖),Seq鎖等。

BKL是一個全局自旋鎖,使用它主要是爲了方便實現從Linux最初的SMP過度到細粒度加鎖機制。

Seq鎖用於讀寫共享數據,實現這樣鎖只要依靠一個序列計數器。
發佈了17 篇原創文章 · 獲贊 1 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章