Linux驅動程序開發005 - 內核同步技術

序言
就像我們在操作系統裏學習的那樣,如果多個程序(進程或線程)同時訪問臨界區數據就會發生競爭。存在競爭條件的程序會產生不可預料的結果。消除競爭的方法一般就是同步的訪問臨界區數據(原子訪問)。Linux內核提供了多種技術用來實現內核同步操作。下面我們就分別介紹。

內核同步技術
Linux內核是多進程、多線程的操作系統,它提供了相當完整的內核同步方法。作爲一個總結,我們先列出內核同步方法列表,這樣我們可以從總體上對內核同步技術有個瞭解,然後我們這分別對每個同步技術做詳細介紹。
同步技術 同步技術描述
自旋鎖  
讀寫自旋鎖  
 信號量  
讀寫信號量  
  原子操作  
 內存屏障  
  完成變量  
 大內核鎖  
seq鎖  

  • 自旋鎖
鎖機制是一種廣泛使用的同步技術,Linux內核中最常見的鎖就是自旋鎖(spin lock)。自旋鎖被設計工作在多個處理器上(SMP),它只能被一個CPU上的一個進程(線程)所持有。它也可以工作在支持搶佔的單處理器上。如果另一個進程或線程試圖獲取一個被持有的自旋鎖,那麼它就會在該鎖上自旋(循環的執行一小段代碼)直到該鎖被釋放。從這個意義上說,自旋鎖是忙等待的,這就會特別浪費處理器的時間,因此自旋鎖不應該被長時間持有。對於單處理器並且不可搶佔的內核來說,自旋鎖什麼也不作。
需要強調的是,自旋鎖別設計用於多處理器的同步機制,對於單處理器,內核在編譯時不會引入自旋鎖機制,對於可搶佔的內核,它僅僅被用於設置內核的搶佔機制是否開啓的一個開關,也就是說加鎖和解鎖實際變成了禁止或開啓內核搶佔功能。如果內核不支持搶佔,那麼自旋鎖根本就不會編譯到內核中。
內核中使用spinlock_t類型來表示自旋鎖,它定義在<linux/spinlock_types.h>:

typedef struct {
    raw_spinlock_t raw_lock;
#if defined(CONFIG_PREEMPT) && defined(CONFIG_SMP)
    unsigned int break_lock;
#endif
} spinlock_t;


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

typedef struct {
    unsigned int slock;
} raw_spinlock_t;


slock表示了自旋鎖的狀態,“1”表示自旋鎖處於解鎖狀態(UNLOCK),“0”表示自旋鎖處於上鎖狀態(LOCKED)。
break_lock表示當前是否由進程在等待自旋鎖,顯然,它只有在支持搶佔的SMP內核上才起作用。

自旋鎖的實現是一個複雜的過程,說它複雜不是因爲需要多少代碼或邏輯來實現它,其實它的實現代碼很少。自旋鎖的實現跟體系結構關係密切,核心代碼基本也是由彙編語言寫成,與體協結構相關的核心代碼都放在相關的<asm/>目錄下,比如<asm/spinlock.h>。對於我們驅動程序開發人員來說,我們沒有必要了解這麼spinlock的內部細節,如果你對它感興趣,請參考閱讀Linux內核源代碼。對於我們驅動的spinlock接口,我們只需包括<linux/spinlock.h>頭文件。在我們詳細的介紹spinlock的API之前,我們先來看看自旋鎖的一個基本使用格式:

#include <linux/spinlock.h>
spinlock_t lock = SPIN_LOCK_UNLOCKED;

spin_lock(&lock);
....
spin_unlock(&lock);


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

#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宏的工作,因此,下面的兩行代碼是等價的。

DEFINE_SPINLOCK (lock);
spinlock_t lock = SPIN_LOCK_UNLOCKED;


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

spinlock_t *lock
......
spin_lock_init(lock);


  • 獲取鎖
內核提供了三個函數用於獲取一個自旋鎖。
spin_lock:獲取指定的自旋鎖。
spin_lock_irq:禁止本地中斷並獲取自旋鎖。
spin_lock_irqsace:保存本地中斷狀態,禁止本地中斷並獲取自旋鎖,返回本地中斷狀態。

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

另外兩個同自旋鎖獲取相關的函數是:
spin_trylock():嘗試獲取自旋鎖,如果獲取失敗則立即返回非0值,否則返回0。
spin_is_locked():判斷指定的自旋鎖是否已經被獲取了。如果是則返回非0,否則,返回0。
  • 釋放鎖
同獲取鎖相對應,內核提供了三個相對的函數來釋放自旋鎖。
spin_unlock:釋放指定的自旋鎖。
spin_unlock_irq:釋放自旋鎖並激活本地中斷。
spin_unlock_irqsave:釋放自旋鎖,並恢復保存的本地中斷狀態。

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

// 靜態初始化
rwlock_t rwlock = RW_LOCK_UNLOCKED;
//動態初始化
rwlock_t *rwlock;
...
rw_lock_init(rwlock);


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

read_lock(&rwlock);
...
read_unlock(&rwlock);


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

write_lock(&rwlock);
...
write_unlock(&rwlock);


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

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

RW_LOCK_UNLOCKED
rw_lock_init(rwlock_t *)
read_lock(rwlock_t *)
read_unlock(rwlock_t *)
read_lock_irq(rwlock_t *)
read_unlock_irq(rwlock_t *)
read_lock_irqsave(rwlock_t *, unsigned long)
read_unlock_irqsave(rwlock_t *, unsigned long)
write_lock(rwlock_t *)
write_unlock(rwlock_t *)
write_lock_irq(rwlock_t *)
write_unlock_irq(rwlock_t *)
write_lock_irqsave(rwlock_t *, unsigned long)
write_unlock_irqsave(rwlock_t *, unsigned long)
rw_is_locked(rwlock_t *)


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

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

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

struct semaphore {
    atomic_t count;
    int sleepers;
    wait_queue_head_t wait;
};


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

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

static DECLARE_MUTEX(my_sem);
......

if (down_interruptible(&my_sem))

{
    return -ERESTARTSYS;
}
......
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則初始化信號量爲鎖狀態。
  • 使用信號量
信號量初始化完成後我們就可以使用它了

down_interruptible(struct semaphore *);
down(struct semaphore *)
down_trylock(struct semaphore *)
up(struct semaphore *)


down函數會嘗試獲取指定的信號量,如果信號量已經被使用了,則進程進入不可中斷的睡眠狀態。down_interruptible則會使進程進入可中斷的睡眠狀態。關於進程狀態的詳細細節,我們在內核的進程管理裏在做詳細介紹。

down_trylock嘗試獲取信號量, 如果獲取成功則返回0,失敗則會立即返回非0。

當退出臨界區時使用up函數釋放信號量,如果信號量上的睡眠隊列不爲空,則喚醒其中一個等待進程。

  • 讀寫信號量
類似於自旋鎖,信號量也有讀寫信號量。讀寫信號量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_trylock和down_write_trylock會嘗試着獲取信號量,如果獲取成功則返回1,否則返回0。奇怪爲什麼返回值與信號量的對應函數相反,使用是一定要小心這點。

後記
由於文本大小的限制,這裏只能介紹這些技術了。自旋鎖和信號量都是常用的內核技術,下面我們總結一下自旋鎖和信號量的特點,然後說明什麼時候使用自旋鎖,什麼時候使用信號量。
  • SMP系統
自旋鎖主要是應用於SMP(CONFIG_SMP)的系統上的,爲了能使代碼工作在非SMP系統上,請慎重選擇使用的自旋鎖函數。
  • 睡眠
信號量是睡眠的而自旋鎖是非睡眠的,因此在不能睡眠的條件下請使用自旋鎖(如中斷上下文)。如果持鎖期間需要睡眠請使用信號量
  • 持鎖時間
由於自旋鎖是忙等待,很浪費CPU時間,因此如果是短期持鎖可以使用自旋鎖。如果是長期持鎖請使用信號量。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章