linux鎖機制

linux內核中,有很多同步機制。比較經典的有原子操作、spin_lock(忙等待的鎖)、mutex(互斥鎖)、semaphore(信號量)等。並且它們幾乎都有對應的rw_XXX(讀寫鎖),以便在能夠區分讀與寫的情況下,讓讀操作相互不互斥(讀寫、寫寫依然互斥)。而seqlockrcu應該可以不算在經典之列,它們是兩種比較有意思的同步機制。

atomic(原子操作):

所謂原子操作,就是該操作絕不會在執行完畢前被任何其他任務或事件打斷,也就說,它的最小的執行單位,不可能有比它更小的執行單位,因此這裏的原子實際是使用了物理學裏的物質微粒的概念。
原子操作需要硬件的支持,因此是架構相關的,其API和原子類型的定義都定義在內核源碼樹的include/asm/atomic.h文件中,它們都使用彙編語言實現,因爲C語言並不能實現這樣的操作。

原子操作主要用於實現資源計數,很多引用計數(refcnt)就是通過原子操作實現的。

原子類型定義如下:
typedefstruct { volatile int counter; }atomic_t;
volatile修飾字段告訴gcc不要對該類型的數據做優化處理,對它的訪問都是對內存的訪問,而不是對寄存器的訪問。

原子操作API包括:
atomic_read(atomic_t* v);

該函數對原子類型的變量進行原子讀操作,它返回原子類型的變量v的值。
atomic_set(atomic_t* v, int i);

該函數設置原子類型的變量v的值爲i
voidatomic_add(int i, atomic_t *v);

該函數給原子類型的變量v增加值i
atomic_sub(inti, atomic_t *v);

該函數從原子類型的變量v中減去i
intatomic_sub_and_test(int i, atomic_t *v);

該函數從原子類型的變量v中減去i,並判斷結果是否爲0,如果爲0,返回真,否則返回假。
voidatomic_inc(atomic_t *v);

該函數對原子類型變量v原子地增加1
voidatomic_dec(atomic_t *v);

該函數對原子類型的變量v原子地減1
intatomic_dec_and_test(atomic_t *v);

該函數對原子類型的變量v原子地減1,並判斷結果是否爲0,如果爲0,返回真,否則返回假。
intatomic_inc_and_test(atomic_t *v);

該函數對原子類型的變量v原子地增加1,並判斷結果是否爲0,如果爲0,返回真,否則返回假。
intatomic_add_negative(int i, atomic_t*v);

該函數對原子類型的變量v原子地增加I,並判斷結果是否爲負數,如果是,返回真,否則返回假。
intatomic_add_return(int i, atomic_t *v);

該函數對原子類型的變量v原子地增加i,並且返回指向v的指針。
intatomic_sub_return(int i, atomic_t *v);

該函數從原子類型的變量v中減去i,並且返回指向v的指針。

intatomic_inc_return(atomic_t * v);

該函數對原子類型的變量v原子地增加1並且返回指向v的指針。

intatomic_dec_return(atomic_t * v);

該函數對原子類型的變量v原子地減1並且返回指向v的指針。

原子操作通常用於實現資源的引用計數,在TCP/IP協議棧的IP碎片處理中,就使用了引用計數,碎片隊列結構structipq描述了一個IP碎片,字段refcnt就是引用計數器,它的類型爲atomic_t,當創建IP碎片時(在函數ip_frag_create中),使用atomic_set函數把它設置爲1,當引用該IP碎片時,就使用函數atomic_inc把引用計數加1,當不需要引用該IP碎片時,就使用函數ipq_put來釋放該IP碎片,ipq_put使用函數atomic_dec_and_test把引用計數減1並判斷引用計數是否爲0,如果是就釋放Ip碎片。函數ipq_killIP碎片從ipq隊列中刪除,並把該刪除的IP碎片的引用計數減1(通過使用函數atomic_dec實現)。



Spanlock(自旋鎖)

自旋鎖與互斥鎖有點類似,只是自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環在那裏看是否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是因此而得名。由於自旋鎖使用者一般保持鎖時間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠高於互斥鎖。

信號量和讀寫信號量適合於保持時間較長的情況,它們會導致調用者睡眠,因此只能在進程上下文使用(_trylock的變種能夠在中斷上下文使用),而自旋鎖適合於保持時間非常短的情況,它可以在任何上下文使用。如果被保護的共享資源只在進程上下文訪問,使用信號量保護該共享資源非常合適,如果對共巷資源的訪問時間非常短,自旋鎖也可以。但是如果被保護的共享資源需要在中斷上下文訪問(包括底半部即中斷處理句柄和頂半部即軟中斷),就必須使用自旋鎖。

自旋鎖保持期間是搶佔失效的,而信號量和讀寫信號量保持期間是可以被搶佔的。自旋鎖只有在內核可搶佔或SMP的情況下才真正需要,在單CPU且不可搶佔的內核下,自旋鎖的所有操作都是空操作。

跟互斥鎖一樣,一個執行單元要想訪問被自旋鎖保護的共享資源,必須先得到鎖,在訪問完共享資源後,必須釋放鎖。如果在獲取自旋鎖時,沒有任何執行單元保持該鎖,那麼將立即得到鎖;如果在獲取自旋鎖時鎖已經有保持者,那麼獲取鎖操作將自旋在那裏,直到該自旋鎖的保持者釋放了鎖。

無論是互斥鎖,還是自旋鎖,在任何時刻,最多只能有一個保持者,也就說,在任何時刻最多只能有一個執行單元獲得鎖。

自旋鎖的API有:

spin_lock_init(x)

該宏用於初始化自旋鎖x。自旋鎖在真正使用前必須先初始化。該宏用於動態初始化。

DEFINE_SPINLOCK(x)

該宏聲明一個自旋鎖x並初始化它。該宏在2.6.11中第一次被定義,在先前的內核中並沒有該宏。

SPIN_LOCK_UNLOCKED

該宏用於靜態初始化一個自旋鎖。

DEFINE_SPINLOCK(x)等同於spinlock_tx = SPIN_LOCK_UNLOCKED

spin_is_locked(x)

該宏用於判斷自旋鎖x是否已經被某執行單元保持(即被鎖),如果是,返回真,否則返回假。

spin_unlock_wait(x)

該宏用於等待自旋鎖x變得沒有被任何執行單元保持,如果沒有任何執行單元保持該自旋鎖,該宏立即返回,否則將循環在那裏,直到該自旋鎖被保持者釋放。

spin_trylock(lock)

該宏盡力獲得自旋鎖lock,如果能立即獲得鎖,它獲得鎖並返回真,否則不能立即獲得鎖,立即返回假。它不會自旋等待lock被釋放。

spin_lock(lock)

該宏用於獲得自旋鎖lock,如果能夠立即獲得鎖,它就馬上返回,否則,它將自旋在那裏,直到該自旋鎖的保持者釋放,這時,它獲得鎖並返回。總之,只有它獲得鎖才返回。

spin_lock_irqsave(lock, flags)

該宏獲得自旋鎖的同時把標誌寄存器的值保存到變量flags中並失效本地中斷。

spin_lock_irq(lock)

該宏類似於spin_lock_irqsave,只是該宏不保存標誌寄存器的值。

spin_lock_bh(lock)

該宏在得到自旋鎖的同時失效本地軟中斷。

spin_unlock(lock)

該宏釋放自旋鎖lock,它與spin_trylockspin_lock配對使用。如果spin_trylock返回假,表明沒有獲得自旋鎖,因此不必使用spin_unlock釋放。

spin_unlock_irqrestore(lock, flags)

該宏釋放自旋鎖lock的同時,也恢復標誌寄存器的值爲變量flags保存的值。它與spin_lock_irqsave配對使用。

spin_unlock_irq(lock)

該宏釋放自旋鎖lock的同時,也使能本地中斷。它與spin_lock_irq配對應用。

spin_unlock_bh(lock)

該宏釋放自旋鎖lock的同時,也使能本地的軟中斷。它與spin_lock_bh配對使用。

spin_trylock_irqsave(lock, flags)

該宏如果獲得自旋鎖lock,它也將保存標誌寄存器的值到變量flags中,並且失效本地中斷,如果沒有獲得鎖,它什麼也不做。因此如果能夠立即獲得鎖,它等同於spin_lock_irqsave,如果不能獲得鎖,它等同於spin_trylock。如果該宏獲得自旋鎖lock,那需要使用spin_unlock_irqrestore來釋放。

spin_trylock_irq(lock)

該宏類似於spin_trylock_irqsave,只是該宏不保存標誌寄存器。如果該宏獲得自旋鎖lock,需要使用spin_unlock_irq來釋放。

spin_trylock_bh(lock)

該宏如果獲得了自旋鎖,它也將失效本地軟中斷。如果得不到鎖,它什麼也不做。因此,如果得到了鎖,它等同於spin_lock_bh,如果得不到鎖,它等同於spin_trylock。如果該宏得到了自旋鎖,需要使用spin_unlock_bh來釋放。

spin_can_lock(lock)

該宏用於判斷自旋鎖lock是否能夠被鎖,它實際是spin_is_locked取反。如果lock沒有被鎖,它返回真,否則,返回假。該宏在2.6.11中第一次被定義,在先前的內核中並沒有該宏。

獲得自旋鎖和釋放自旋鎖有好幾個版本,因此讓讀者知道在什麼樣的情況下使用什麼版本的獲得和釋放鎖的宏是非常必要的。

如果被保護的共享資源只在進程上下文訪問和軟中斷上下文訪問,那麼當在進程上下文訪問共享資源時,可能被軟中斷打斷,從而可能進入軟中斷上下文來對被保護的共享資源訪問,因此對於這種情況,對共享資源的訪問必須使用spin_lock_bhspin_unlock_bh來保護。當然使用spin_lock_irqspin_unlock_irq以及spin_lock_irqsavespin_unlock_irqrestore也可以,它們失效了本地硬中斷,失效硬中斷隱式地也失效了軟中斷。但是使用spin_lock_bhspin_unlock_bh是最恰當的,它比其他兩個快。

如果被保護的共享資源只在進程上下文和tasklettimer上下文訪問,那麼應該使用與上面情況相同的獲得和釋放鎖的宏,因爲tasklettimer是用軟中斷實現的。

如果被保護的共享資源只在一個tasklettimer上下文訪問,那麼不需要任何自旋鎖保護,因爲同一個tasklettimer只能在一個CPU上運行,即使是在SMP環境下也是如此。實際上tasklet在調用tasklet_schedule標記其需要被調度時已經把該tasklet綁定到當前CPU,因此同一個tasklet決不可能同時在其他CPU上運行。timer也是在其被使用add_timer添加到timer隊列中時已經被幫定到當前CPU,所以同一個timer絕不可能運行在其他CPU上。當然同一個tasklet有兩個實例同時運行在同一個CPU就更不可能了。

如果被保護的共享資源只在兩個或多個tasklettimer上下文訪問,那麼對共享資源的訪問僅需要用spin_lockspin_unlock來保護,不必使用_bh版本,因爲當tasklettimer運行時,不可能有其他tasklettimer在當前CPU上運行。如果被保護的共享資源只在一個軟中斷(tasklettimer除外)上下文訪問,那麼這個共享資源需要用spin_lockspin_unlock來保護,因爲同樣的軟中斷可以同時在不同的CPU上運行。

如果被保護的共享資源在兩個或多個軟中斷上下文訪問,那麼這個共享資源當然更需要用spin_lockspin_unlock來保護,不同的軟中斷能夠同時在不同的CPU上運行。

如果被保護的共享資源在軟中斷(包括tasklettimer)或進程上下文和硬中斷上下文訪問,那麼在軟中斷或進程上下文訪問期間,可能被硬中斷打斷,從而進入硬中斷上下文對共享資源進行訪問,因此,在進程或軟中斷上下文需要使用spin_lock_irqspin_unlock_irq來保護對共享資源的訪問。而在中斷處理句柄中使用什麼版本,需依情況而定,如果只有一箇中斷處理句柄訪問該共享資源,那麼在中斷處理句柄中僅需要spin_lockspin_unlock來保護對共享資源的訪問就可以了。因爲在執行中斷處理句柄期間,不可能被同一CPU上的軟中斷或進程打斷。但是如果有不同的中斷處理句柄訪問該共享資源,那麼需要在中斷處理句柄中使用spin_lock_irqspin_unlock_irq來保護對共享資源的訪問。

在使用spin_lock_irqspin_unlock_irq的情況下,完全可以用spin_lock_irqsavespin_unlock_irqrestore取代,那具體應該使用哪一個也需要依情況而定,如果可以確信在對共享資源訪問前中斷是使能的,那麼使用spin_lock_irq更好一些,因爲它比spin_lock_irqsave要快一些,但是如果你不能確定是否中斷使能,那麼使用spin_lock_irqsavespin_unlock_irqrestore更好,因爲它將恢復訪問共享資源前的中斷標誌而不是直接使能中斷。當然,有些情況下需要在訪問共享資源時必須中斷失效,而訪問完後必須中斷使能,這樣的情形使用spin_lock_irqspin_unlock_irq最好。

需要特別提醒讀者,spin_lock用於阻止在不同CPU上的執行單元對共享資源的同時訪



mutex(互斥鎖)

/linux/include/linux/mutex.h

47struct mutex {

48

49 atomic_t count;

50 spinlock_t wait_lock;

51 struct list_head wait_list;

52#ifdef CONFIG_DEBUG_MUTEXES

53 struct thread_info *owner;

54 const char *name;

55 void *magic;

56#endif

57#ifdef CONFIG_DEBUG_LOCK_ALLOC

58 struct lockdep_map dep_map;

59#endif

60};



一、作用及訪問規則:

互斥鎖主要用於實現內核中的互斥訪問功能。內核互斥鎖是在原子API之上實現的,但這對於內核用戶是不可見的。對它的訪問必須遵循一些規則:同一時間只能有一個任務持有互斥鎖,而且只有這個任務可以對互斥鎖進行解鎖。互斥鎖不能進行遞歸鎖定或解鎖。一個互斥鎖對象必須通過其API初始化,而不能使用memset或複製初始化。一個任務在持有互斥鎖的時候是不能結束的。互斥鎖所使用的內存區域是不能被釋放的。使用中的互斥鎖是不能被重新初始化的。並且互斥鎖不能用於中斷上下文。但是互斥鎖比當前的內核信號量選項更快,並且更加緊湊,因此如果它們滿足您的需求,那麼它們將是您明智的選擇。

二、各字段詳解:

1atomic_t count; --指示互斥鎖的狀態:1沒有上鎖,可以獲得;0被鎖定,不能獲得;負數被鎖定,且可能在該鎖上有等待進程初始化爲沒有上鎖。

2spinlock_t wait_lock;--等待獲取互斥鎖中使用的自旋鎖。在獲取互斥鎖的過程中,操作會在自旋鎖的保護中進行。初始化爲爲鎖定。

3struct list_head wait_list;--等待互斥鎖的進程隊列。

四、操作:

1、定義並初始化:

struct mutex mutex;

mutex_init(&mutex);

79# define mutex_init(mutex) \

80do { \

81 static struct lock_class_key __key; \

82 \

83 __mutex_init((mutex), #mutex, &__key); \

84} while (0)

42void

43__mutex_init(struct mutex *lock, const char *name, structlock_class_key *key)

44{

45 atomic_set(&lock->count, 1);

46 spin_lock_init(&lock->wait_lock);

47 INIT_LIST_HEAD(&lock->wait_list);

48

49 debug_mutex_init(lock, name, key);

50}

直接定於互斥鎖mutex並初始化爲未鎖定,己count1wait_lock爲未上鎖,等待隊列wait_list爲空。

2、獲取互斥鎖:

(1)具體參見linux/kernel/mutex.c

void inline __sched mutex_lock(struct mutex *lock)

{

might_sleep();

__mutex_fastpath_lock(&lock->count,__mutex_lock_slowpath);

}

獲取互斥鎖。實際上是先給count做自減操作,然後使用本身的自旋鎖進入臨界區操作。首先取得count的值,再將count置爲-1,判斷如果原來count的值爲1,也即互斥鎖可以獲得,則直接獲取,跳出。否則進入循環反覆測試互斥鎖的狀態。在循環中,也是先取得互斥鎖原來的狀態,再將其置爲-1,判斷如果可以獲取(等於1),則退出循環,否則設置當前進程的狀態爲不可中斷狀態,解鎖自身的自旋鎖,進入睡眠狀態,待被在調度喚醒時,再獲得自身的自旋鎖,進入新一次的查詢其自身狀態(該互斥鎖的狀態)的循環。

(2)具體參見linux/kernel/mutex.c

int __sched mutex_lock_interruptible(struct mutex *lock)

{

might_sleep();

return __mutex_fastpath_lock_retval(&lock->count,__mutex_lock_interruptible_slowpath);

}

mutex_lock()一樣,也是獲取互斥鎖。在獲得了互斥鎖或進入睡眠直到獲得互斥鎖之後會返回0。如果在等待獲取鎖的時候進入睡眠狀態收到一個信號(被信號打斷睡眠),則返回_EINIR

(3)具體參見linux/kernel/mutex.c

int __sched mutex_trylock(struct mutex *lock)

{

return __mutex_fastpath_trylock(&lock->count,

__mutex_trylock_slowpath);

}

試圖獲取互斥鎖,如果成功獲取則返回1,否則返回0,不等待。

3、釋放互斥鎖:

具體參見linux/kernel/mutex.c

void __sched mutex_unlock(struct mutex *lock)

{

__mutex_fastpath_unlock(&lock->count,__mutex_unlock_slowpath);

}

釋放被當前進程獲取的互斥鎖。該函數不能用在中斷上下文中,而且不允許去釋放一個沒有上鎖的互斥鎖。

4.void mutex_destroy(struct mutex *lock) --清除互斥鎖,使互斥鎖不可用

mutex_destroy()函數解除由lock指向的互斥鎖的任何狀態。在調用執行這個函數的時候,lock指向的互斥鎖不能在被鎖狀態。儲存互斥鎖的內存不被釋放。

返回值--mutex_destroy()在成功執行後返回零。其他值意味着錯誤。在以下情況發生時,函數失敗並返回相關值。

EINVAL 非法參數

EFAULT mp指向一個非法地址。

5.static inline int mutex_is_locked(struct mutex *lock)--測試互斥鎖的狀態

這個調用實際上編譯成一個內聯函數。如果互斥鎖被持有(鎖定),那麼就會返回1;否則,返回0

五、使用形式:

struct mutex mutex;

mutex_init(&mutex);

...

mutex_lock(&mutex);

...

mutex_unlock(&mutex);

semaphore (信號量)

Linux內核的信號量在概念和原理上與用戶態的SystemVIPC機制信號量是一樣的,但是它絕不可能在內核之外使用,因此它與SystemVIPC機制信號量毫不相干。

信號量在創建時需要設置一個初始值,表示同時可以有幾個任務可以訪問該信號量保護的共享資源,初始值爲1就變成互斥鎖(Mutex),即同時只能有一個任務可以訪問信號量保護的共享資源。一個任務要想訪問共享資源,首先必須得到信號量,獲取信號量的操作將把信號量的值減1,若當前信號量的值爲負數,表明無法獲得信號量,該任務必須掛起在該信號量的等待隊列等待該信號量可用;若當前信號量的值爲非負數,表示可以獲得信號量,因而可以立刻訪問被該信號量保護的共享資源。當任務訪問完被信號量保護的共享資源後,必須釋放信號量,釋放信號量通過把信號量的值加1實現,如果信號量的值爲非正數,表明有任務等待當前信號量,因此它也喚醒所有等待該信號量的任務。

信號量的API有:

DECLARE_MUTEX(name)

該宏聲明一個信號量name並初始化它的值爲0,即聲明一個互斥鎖。

DECLARE_MUTEX_LOCKED(name)

該宏聲明一個互斥鎖name,但把它的初始值設置爲0,即鎖在創建時就處在已鎖狀態。因此對於這種鎖,一般是先釋放後獲得。

void sema_init (struct semaphore *sem, int val);

該函用於數初始化設置信號量的初值,它設置信號量sem的值爲val

void init_MUTEX (struct semaphore *sem);

該函數用於初始化一個互斥鎖,即它把信號量sem的值設置爲1

void init_MUTEX_LOCKED (struct semaphore *sem);

該函數也用於初始化一個互斥鎖,但它把信號量sem的值設置爲0,即一開始就處在已鎖狀態。

void down(struct semaphore * sem);

該函數用於獲得信號量sem,它會導致睡眠,因此不能在中斷上下文(包括IRQ上下文和softirq上下文)使用該函數。該函數將把sem的值減1,如果信號量sem的值非負,就直接返回,否則調用者將被掛起,直到別的任務釋放該信號量才能繼續運行。

int down_interruptible(struct semaphore * sem);

該函數功能與down類似,不同之處爲,down不會被信號(signal)打斷,但down_interruptible能被信號打斷,因此該函數有返回值來區分是正常返回還是被信號中斷,如果返回0,表示獲得信號量正常返回,如果被信號打斷,返回-EINTR

int down_trylock(struct semaphore * sem);

該函數試着獲得信號量sem,如果能夠立刻獲得,它就獲得該信號量並返回0,否則,表示不能獲得信號量sem,返回值爲非0值。因此,它不會導致調用者睡眠,可以在中斷上下文使用。

void up(struct semaphore * sem);

該函數釋放信號量sem,即把sem的值加1,如果sem的值爲非正數,表明有任務等待該信號量,因此喚醒這些等待者。

信號量在絕大部分情況下作爲互斥鎖使用,下面以console驅動系統爲例說明信號量的使用。

在內核源碼樹的kernel/printk.c中,使用宏DECLARE_MUTEX聲明瞭一個互斥鎖console_sem,它用於保護console驅動列表console_drivers以及同步對整個console驅動系統的訪問,其中定義了函數acquire_console_sem來獲得互斥鎖console_sem,定義了release_console_sem來釋放互斥鎖console_sem,定義了函數try_acquire_console_sem來盡力得到互斥鎖console_sem。這三個函數實際上是分別對函數downupdown_trylock的簡單包裝。需要訪問console_drivers驅動列表時就需要使用acquire_console_sem來保護console_drivers列表,當訪問完該列表後,就調用release_console_sem釋放信號量console_sem。函數console_unblankconsole_deviceconsole_stopconsole_startregister_consoleunregister_console都需要訪問console_drivers,因此它們都使用函數對acquire_console_semrelease_console_sem來對console_drivers進行保護。



rw_semaphore (讀寫信號量)

讀寫信號量對訪問者進行了細分,或者爲讀者,或者爲寫者,讀者在保持讀寫信號量期間只能對該讀寫信號量保護的共享資源進行讀訪問,如果一個任務除了需要讀,可能還需要寫,那麼它必須被歸類爲寫者,它在對共享資源訪問之前必須先獲得寫者身份,寫者在發現自己不需要寫訪問的情況下可以降級爲讀者。讀寫信號量同時擁有的讀者數不受限制,也就說可以有任意多個讀者同時擁有一個讀寫信號量。如果一個讀寫信號量當前沒有被寫者擁有並且也沒有寫者等待讀者釋放信號量,那麼任何讀者都可以成功獲得該讀寫信號量;否則,讀者必須被掛起直到寫者釋放該信號量。如果一個讀寫信號量當前沒有被讀者或寫者擁有並且也沒有寫者等待該信號量,那麼一個寫者可以成功獲得該讀寫信號量,否則寫者將被掛起,直到沒有任何訪問者。因此,寫者是排他性的,獨佔性的。

讀寫信號量有兩種實現,一種是通用的,不依賴於硬件架構,因此,增加新的架構不需要重新實現它,但缺點是性能低,獲得和釋放讀寫信號量的開銷大;另一種是架構相關的,因此性能高,獲取和釋放讀寫信號量的開銷小,但增加新的架構需要重新實現。在內核配置時,可以通過選項去控制使用哪一種實現。

讀寫信號量的相關API有:

DECLARE_RWSEM(name)

該宏聲明一個讀寫信號量name並對其進行初始化。

void init_rwsem(struct rw_semaphore *sem);

該函數對讀寫信號量sem進行初始化。

void down_read(struct rw_semaphore *sem);

讀者調用該函數來得到讀寫信號量sem。該函數會導致調用者睡眠,因此只能在進程上下文使用。

int down_read_trylock(struct rw_semaphore *sem);

該函數類似於down_read,只是它不會導致調用者睡眠。它盡力得到讀寫信號量sem,如果能夠立即得到,它就得到該讀寫信號量,並且返回1,否則表示不能立刻得到該信號量,返回0。因此,它也可以在中斷上下文使用。

void down_write(struct rw_semaphore *sem);

寫者使用該函數來得到讀寫信號量sem,它也會導致調用者睡眠,因此只能在進程上下文使用。

int down_write_trylock(struct rw_semaphore *sem);

該函數類似於down_write,只是它不會導致調用者睡眠。該函數盡力得到讀寫信號量,如果能夠立刻獲得,就獲得該讀寫信號量並且返回1,否則表示無法立刻獲得,返回0。它可以在中斷上下文使用。

void up_read(struct rw_semaphore *sem);

讀者使用該函數釋放讀寫信號量sem。它與down_readdown_read_trylock配對使用。如果down_read_trylock返回0,不需要調用up_read來釋放讀寫信號量,因爲根本就沒有獲得信號量。

void up_write(struct rw_semaphore *sem);

寫者調用該函數釋放信號量sem。它與down_writedown_write_trylock配對使用。如果down_write_trylock返回0,不需要調用up_write,因爲返回0表示沒有獲得該讀寫信號量。

void downgrade_write(struct rw_semaphore *sem);

該函數用於把寫者降級爲讀者,這有時是必要的。因爲寫者是排他性的,因此在寫者保持讀寫信號量期間,任何讀者或寫者都將無法訪問該讀寫信號量保護的共享資源,對於那些當前條件下不需要寫訪問的寫者,降級爲讀者將,使得等待訪問的讀者能夠立刻訪問,從而增加了併發性,提高了效率。

讀寫信號量適於在讀多寫少的情況下使用,在linux內核中對進程的內存映像描述結構的訪問就使用了讀寫信號量進行保護。在Linux中,每一個進程都用一個類型爲task_tstructtask_struct的結構來描述,該結構的類型爲structmm_struct的字段mm描述了進程的內存映像,特別是mm_struct結構的mmap字段維護了整個進程的內存塊列表,該列表將在進程生存期間被大量地遍利或修改,因此mm_struct結構就有一個字段mmap_sem來對mmap的訪問進行保護,mmap_sem就是一個讀寫信號量,在proc文件系統裏有很多進程內存使用情況的接口,通過它們能夠查看某一進程的內存使用情況,命令freepstop都是通過proc來得到內存使用信息的,proc接口就使用down_readup_read來讀取進程的mmap信息。當進程動態地分配或釋放內存時,需要修改mmap來反映分配或釋放後的內存映像,因此動態內存分配或釋放操作需要以寫者身份獲得讀寫信號量mmap_sem來對mmap進行更新。系統調用brkmunmap就使用了down_writeup_write來保護對mmap的訪問。



seqlock(順序鎖)

用於能夠區分讀與寫的場合,並且是讀操作很多、寫操作很少,寫操作的優先權大於讀操作。
seqlock的實現思路是,用一個遞增的整型數表示sequence。寫操作進入臨界區時,sequence++;退出臨界區時,sequence++。寫操作還需要獲得一個鎖(比如mutex),這個鎖僅用於寫寫互斥,以保證同一時間最多隻有一個正在進行的寫操作。
sequence爲奇數時,表示有寫操作正在進行,這時讀操作要進入臨界區需要等待,直到sequence變爲偶數。讀操作進入臨界區時,需要記錄下當前sequence的值,等它退出臨界區的時候用記錄的sequence與當前sequence做比較,不相等則表示在讀操作進入臨界區期間發生了寫操作,這時候讀操作讀到的東西是無效的,需要返回重試。

seqlock寫寫是必須要互斥的。但是seqlock的應用場景本身就是讀多寫少的情況,寫衝突的概率是很低的。所以這裏的寫寫互斥基本上不會有什麼性能損失。
而讀寫操作是不需要互斥的。seqlock的應用場景是寫操作優先於讀操作,對於寫操作來說,幾乎是沒有阻塞的(除非發生寫寫衝突這一小概率事件),只需要做sequence++這一附加動作。而讀操作也不需要阻塞,只是當發現讀寫衝突時需要retry

seqlock的一個典型應用是時鐘的更新,系統中每1毫秒會有一個時鐘中斷,相應的中斷處理程序會更新時鐘(見《linux時鐘淺析》)(寫操作)。而用戶程序可以調用gettimeofday之類的系統調用來獲取當前時間(讀操作)。在這種情況下,使用seqlock可以避免過多的gettimeofday系統調用把中斷處理程序給阻塞了(如果使用讀寫鎖,而不用seqlock的話就會這樣)。中斷處理程序總是優先的,而如果gettimeofday系統調用與之衝突了,那用戶程序多等等也無妨。

seqlock的實現非常簡單:
寫操作進入臨界區時:
voidwrite_seqlock(seqlock_t *sl)
{
spin_lock(&sl->lock); //
上寫寫互斥鎖
++sl->sequence;      // sequence++
}

寫操作退出臨界區時:
voidwrite_sequnlock(seqlock_t *sl)
{
sl->sequence++;        // sequence
++
spin_unlock(&sl->lock);//
釋放寫寫互斥鎖
}


讀操作進入臨界區時:
unsignedread_seqbegin(const seqlock_t *sl)
{
unsigned ret;
repeat:
ret= sl->sequence;      //
sequence
if(unlikely(ret & 1)) { //
如果sequence爲奇數自旋等待
gotorepeat;
}
return ret;
}

讀操作嘗試退出臨界區時:
intread_seqretry(const seqlock_t *sl, unsigned start)
{
return(sl->sequence != start); //
看看sequence與進入臨界區時是否發生過改變
}

而讀操作一般會這樣進行:
do{
seq = read_seqbegin(&seq_lock);     //
進入臨界區
do_something();
}while (read_seqretry(&seq_lock, seq)); //
嘗試退出臨界區,存在衝突則重試

RCUread-copy-update

RCU也是用於能夠區分讀與寫的場合,並且也是讀多寫少,但是讀操作的優先權大於寫操作(與seqlock相反)。
RCU的實現思路是,讀操作不需要互斥、不需要阻塞、也不需要原子指令,直接讀就行了。而寫操作在進行之前需要把被寫的對象copy一份,寫完之後再更新回去。其實RCU所能保護的並不是任意的臨界區,它只能保護由指針指向的對象(而不保護指針本身)。讀操作通過這個指針來訪問對象(這個對象就是臨界區);寫操作把對象複製一份,然後更新,最後修改指針使其指向新的對象。由於指針總是一個字長的,對它的讀寫對於CPU來說總是原子的,所以不用擔心更新指針只更新到一半就被讀取的情況(指針的值爲0x11111111,要更新爲0x22222222,不會出現類似0x11112222這樣的中間狀態)。所以,當讀寫操作同時發生時,讀操作要麼讀到指針的舊值,引用了更新前的對象、要麼讀到了指針的新值,引用了更新後的對象。即使同時有多個寫操作發生也沒關係(是否需要寫寫互斥跟寫操作本身的場景相關)。

RCU封裝了rcu_dereferencercu_assign_pointer兩個函數,分別用於對指針進行讀和寫。
rcu_assign_pointer(p,v) => (p) = (v)
rcu_dereference(p) => (p)
裏面其實就是簡單的指針讀和寫,然後可能設置內存屏障(以避免編譯器或CPU指令亂序對程序造成影響)。當然,如果出現了一種奇怪的不能直接保證原子性讀寫指針的體系結構,還需要這兩個函數來保證原子性。

可以看到,使用了RCU之後,讀寫操作竟然神奇地都不需要阻塞了,臨界區已經不是臨界區了。只不過寫操作稍微麻煩些,需要readcopyupdate。不過RCU的核心問題並不是如何同步,而是如何釋放舊的對象。指向對象的指針被更新了,但是之前發生的讀操作可能還在引用舊的對象呢,舊的對象什麼時候釋放掉呢?讓讀操作來釋放舊的對象似乎並不是很和理,它不知道對象是否已經被更新了,也不知道有多少讀操作都引用了這個舊對象。給對象加一個引用計數呢?這或許可以奏效,但是這也太不通用了,RCU是一種機制,如果要求每個使用RCU的對象都在對象的某某位置維護一個引用計數,相當於RCU機制要跟具體的對象耦合上了。並且對引用計數的修改還需要另一套同步機制來提供保障。
爲解決舊對象釋放的問題,RCU提供了四個函數(另外還有一些它們的變形):
rcu_read_lock(void)rcu_read_unlock(void)
synchronize_rcu(void)call_rcu(struct rcu_head *head, void (*func)(struct rcu_head*head))
當讀操作要調用rcu_dereference訪問對象之前,需要先調用rcu_read_lock;當不再需要訪問對象時,調用rcu_read_unlock
當寫操作調用rcu_assign_pointer完成對對象的更新之後,需要調用synchronize_rcucall_rcu。其中synchronize_rcu會阻塞等待在此之前所有調用了rcu_read_lock的讀操作都已經調用rcu_read_unlocksynchronize_rcu返回後寫操作一方就可以將被它替換掉的舊對象釋放了;而call_rcu則是通過註冊回調函數的方式,由回調函數來釋放舊對象,寫操作一方將不需要阻塞等待。同樣,等到在此之前所有調用了rcu_read_lock的讀操作都調用rcu_read_unlock之後,回調函數將被調用。

如果你足夠細心,可能已經注意到了這樣一個問題。synchronize_rcucall_rcu會等待的是“在此之前所有調用了rcu_read_lock的讀操作都已經調用了rcu_read_unlock”,然而在rcu_assign_pointersynchronize_rcucall_rcu之間,可能也有讀操作發生(調用了rcu_read_lock),它們引用到的是寫操作rcu_assign_pointer後的新對象。按理說寫操作一方想要釋放舊對象時,是不需要等待這樣的讀操作的。但是由於這些讀操作發生在synchronize_rcucall_rcu之前,按照RCU的機制,還真得等它們都rcu_read_unlock。這豈不是多等了一些時日?
實際情況的確是這樣,甚至可能更糟。因爲目前linux內核裏面的RCU是一個全局的實現,注意,rcu_read_locksynchronize_rcu、等等操作都是不帶參數的。它不像seqlock或其他同步機制那樣,一把鎖保護一個臨界區。這個全局的RCU將保護使用RCU機制的所有臨界區。所以,對於寫操作一方來說,在它調用synchronize_rcucall_rcu之前發生的所有讀操作它都得等待(不管讀的對象與該寫操作有無關係),直到這些讀操作都rcu_read_unlock之後,舊的對象才能被釋放。所以,寫操作更新對象之後,舊對象並不是精確地在它能夠被釋放之時立刻被釋放的,可能會存在一定的延遲。
不過話說回來,這樣的實現減少了很多不必要的麻煩,因爲舊的對象晚一些釋放是不會有太大關係的。想一想,精確舊對象的釋放時機有多大意義呢?無非是儘可能早的回收一些內存(一般來說,內核裏面使用的這些對象並不會太大吧,晚一點回收也不會晚得太過分吧)。但是爲此你得花費很大的代價去跟蹤每一個對象的引用情況,這是不是有些得不償失呢?

最後,RCU要求,讀操作在rcu_read_lockrcu_read_unlock之間是不能睡眠的(WHY?),call_rcu提供的回調函數也不能睡眠(因爲回調函數一般會在軟中斷裏面去調用,中斷上下文是不能睡眠的,見《linux中斷處理淺析》)。

那麼,RCU具體是怎麼實現的呢?儘管沒有要求在精確的時間回收舊對象,RCU的實現還是很複雜的。以下簡單討論一下rcu_read_lockrcu_read_unlockcall_rcu三個函數的實現。而synchronize_rcu實際上是利用call_rcu來實現的(調用call_rcu提交一個回調函數,然後自己進入睡眠,而回調函數要做的事情就是把自己喚醒)。
linux2.6.30版本中,RCU有三種實現,分別命名爲rcuclassicrcupreemptrcutree。這三種實現也是逐步發展出來的,最開始是rcuclassic,然後rcupreempt,最後rcutree。在編譯內核時可以通過編譯選項選擇需要使用的RCU實現。

rcuclassic
rcuclassic的實現思路是,讀操作在rcu_read_lock時禁止內核搶佔、在rcu_read_unlock時重新啓用內核搶佔。由於RCU只會在內核態裏面使用,而且RCU也要求rcu_read_lockrcu_read_unlock之間不能睡眠。所以在rcu_read_lock之後,這個讀操作的相關代碼肯定會在當前CPU上持續被執行,直到rcu_read_unlock之後纔可能被調度。而同一時間,在一個CPU上,也最多只能有一個正在進行的讀操作。可以說,rcuclassic是基於CPU來跟蹤讀操作的。
於是,如果發現一個CPU已經發生了調度,就說明這個CPU上的讀操作肯定已經rcu_read_unlock了(注意這裏又是一次延遲,rcu_read_unlock之後可能還要過一段時間纔會發生調度。RCU的實現中,這樣的延遲隨處可見,因爲它根本就不要求在精確的時間點回收舊對象)。於是,從一次call_rcu被調用之後開始,如果等到所有CPU都已經發生了調度,這次call_rcu需要等待的讀操作就必定都已經rcu_read_unlock了,這時候就可以處理這個call_rcu提交的回調函數了。
但是實現上,rcuclassic並不是爲每一次call_rcu都提供一個這樣的等待週期(等待所有CPU都已發生調度),那樣的話粒度太細,實現起來會比較複雜。rcuclassic將現有的全部call_rcu提交的回調函數分爲兩個批次(batch),以批次爲單位來進行等待。如果所有CPU都已發生調度,則第一批次的所有回調函數將被調用,然後將第一批次清空、第二批變爲第一批,並繼續下一次的等待。而所有新來的call_rcu總是將回調函數提交到第二批。
rcuclassic邏輯上通過三個鏈表來管理call_rcu提交的回調函數,分別是第二批次鏈表、第一批次鏈表、待處理鏈表(2.6.30版本的實現實際用了四個鏈表,把待處理鏈表分解成兩個鏈表)。call_rcu總是將回調函數提交到第二批次鏈表中,如果發現第一批次鏈表爲空(之前的call_rcu都已經處理完了),就將第二批次鏈表中的回調函數都移入第一批次鏈表(第二批次鏈表清空);從回調函數被移入第一批次鏈表開始,如果所有CPU都發生了調度,則將第一批次鏈表中的回調函數都移入待處理鏈表(第一批次鏈表清空,同時第二批次鏈表中新的回調函數又被移過來);待處理鏈表裏面的回調函數都是等待被調用的,下一次進入軟中斷的時候就要調用它們。
什麼時候檢查“所有CPU都已發生調度”呢?並不是在CPU發生調度的時候。調度的時候只是做一個標記,標記這個CPU已經調度過了。而檢查是放在每毫秒一次的時鐘中斷處理函數裏面來進行的。
另外,這裏提到的第二批次鏈表、第一批次鏈表、待處理鏈表其實是每個CPU維護一份的,這樣可以避免操作鏈表時CPU之間的競爭。
rcuclassic的實現利用了禁止內核搶佔,這對於一些實時性要求高的環境是不適用的(實時性要求不高則無妨),所以後來又有了rcupreempt的實現。

rcupreempt
rcupreempt是相對於rcuclassic禁止內核搶佔而言的,rcupreempt允許內核搶佔,以滿足更高的實時性要求。
rcupreempt的實現思路是,通過計數器來記錄rcu_read_lockrcu_read_unlock發生的次數。讀操作在rcu_read_lock時給計數器加1rcu_read_unlock時則減1。只要計數器的值爲0,說明所有的讀操作都rcu_read_unlock了,則在此之前所有call_rcu提交的回調函數都可以被執行。不過,這樣的話,新來的rcu_read_lock會使得之前的call_rcu不斷延遲(如果rcu_read_unlock總是跟不上rcu_read_lock的速度,那麼計數器可能永遠都無法減爲0。但是對於之前的某個call_rcu來說,它所關心的讀操作卻可能都已經rcu_read_unlock了)。所以,rcupreempt還是像rcuclassic那樣,將call_rcu提交的回調函數分爲兩個批次,然後由兩個計數器分別計數。
rcuclassic一樣,call_rcu提交的回調函數總是加入到第二批次,所以rcu_read_lock總是增加第二批次的計數。而當第一批次爲空時,第二批次將移動到第一批次,計數值也應該一起移過來。所以,rcu_read_unlock必須知道它應該減少哪個批次的計數(rcu_read_lock增加第二批次的計數,之後第一批次可能被處理,然後第二批次被移動到第一批次。這種情況下對應的rcu_read_unlock應該減少的是第一批次的計數了)。
實現上,rcupreempt提供了兩個[等待隊列+計數器],並且交替的選擇其中的一個作爲“第一批次”。之前說的將第二批次移動到第一批次的過程實際上就是批次交替一次的過程,批次並沒移動,只是兩個[等待隊列+計數器]的含義發生了交換。於是,rcu_read_lock的時候需要記錄下現在增加的是第幾個計數器的計數,rcu_read_unlock就相應減少那個計數就行了。
那麼rcu_read_lockrcu_read_unlock怎麼對應上呢?rcupreempt已經不禁止內核搶佔了,同一個讀操作裏面的rcu_read_lockrcu_read_unlock可能發生在不同CPU上,不能通過CPU來聯繫rcu_read_lockrcu_read_unlock,只能通過上下文,也就是執行rcu_read_lockrcu_read_unlock的進程。所以,在進程控制塊(task_struct)中新增了一個index字段,用來記錄這個進程上執行的rcu_read_lock增加了哪個計數器的計數,於是這個進程上執行的rcu_read_unlock也應該減少相應的計數。
rcupreempt也維護了一個待處理鏈表。於是,當第一批次的計數爲0時,第一批次裏面的回調函數將被移動到待處理鏈表中,等到下一次進入軟中斷的時候就調用它們。然後第一批次被清空,兩個批次做交換(相當於第二批次移動到第一批次)。
rcuclassic類似,對於計數值的檢查並不是在rcu_read_unlock的時候進行的,rcu_read_unlock只管修改計數值。而檢查也是放在每毫秒一次的時鐘中斷處理函數裏面來進行的。
同樣,這裏提到的等待隊列和計數器也是每個CPU維護一份的,以避免操作鏈表和計數器時CPU之間的競爭。那麼當然,要檢查第一批次計數爲0,是需要把所有CPU的第一批次計數值進行相加的。

rcutree
最後說說rcutree。它跟rcuclassic的實現思路幾乎是一模一樣的,通過禁止搶佔、檢查每一個CPU是否已經發生過調度,來判斷髮生在某一批次rcu_call之前的所有讀操作是否都已經rcu_read_unlock。並且實現上,批次的管理、各種隊列、等等都幾乎一樣,CPU發生調度時也是通過設置一個標記來表示自己已經調度過了,然後又在時鐘中斷的處理程序中判斷是否所有CPU都已經發生過調度……那麼,不同之處在哪裏呢?在於“判斷是否每一個CPU都調度過”這一細節上。
rcuclassic對於多個CPU的管理是對稱的,在時鐘中斷處理函數中,要判斷是否每一個CPU都調度過就得去看每一個CPU所設置的標記,而這個“看”的過程勢必是需要互斥的(因爲這些標記也會被其他CPU讀或寫)。這樣就造成了CPU之間的競爭。如果CPU個數不多,就這麼競爭一下倒也無妨。要是CPU很多的話(比如64個?或更多?),那當然越少競爭越好。rcutree就是爲了這種擁有很多CPU的環境而設計的,以期減少競爭。
rcutree的思路是提供一個樹型結構,其中的每一個非葉子節點提供一個鎖(代表了一次競爭),而每個CPU就對應到樹的葉子節點上。然後呢?當需要判斷“是否每一個CPU都調度過”的時候,CPU嘗試在自己的父節點上鎖(這個鎖只會由它的子節點來競爭,而不會被所有CPU競爭),然後判斷這個“父節點”的子節點(CPU)是否都已經調度過。如果不是,則顯然“每一個CPU都調度過”不成立。而如果是,則再向上遍歷,直到走到樹根,那麼就可以知道所有CPU都已經調度過了。使用這樣的樹型結構就縮小了每一次加鎖的粒度,減少了CPU間的競爭。


BKL(大內核鎖)



大內核鎖這個簡單且不常用的內核加鎖機制一直是內核開發者之間頗具爭議的話題。它在早期linux版本里的廣泛使用,從2.4內核開始逐漸被各種各樣的自旋鎖替代,可是直到現在還不能完全將它拋棄;它曾經使用自旋鎖實現,到了2.6.11版修改爲信號量,可是在2.6.26-rc2又退回到使用自旋鎖的老路上;它甚至引發了linux的創始人LinusTorvalds和著名的完全公平調度(CFS)算法的貢獻者IngoMolnar之間的一場爭議。這究竟是怎麼回事呢?

1.1 應運而生,特立獨行

使用過自旋鎖或信號量這些內核互斥機制的人幾乎不會想到還有大內核鎖這個東西。和自旋鎖或信號量一樣,大內核鎖也是用來保護臨界區資源,避免出現多個處理器上的進程同時訪問同一區域的。但這把鎖獨特的地方是,它不象自旋鎖或信號量一樣可以創建許多實例或者叫對象,每個對象保護特定的臨界區。事實上整個內核只有一把這樣的鎖,一旦一個進程獲得大內核鎖,進入了被它保護的臨界區,不但該臨界區被鎖住,所有被它保護的其它臨界區都將無法訪問,直到該進程釋放大內核鎖。這看似不可思議:一個進程在一個處理器上操作一個全局的鏈表,怎麼可能導致其它進程無法訪問另一個全局數組呢?使用兩個自旋鎖,一個保護鏈表,另一個保護數組不就解決了嗎?可是如果你使用大內核鎖,效果就是這樣的。

大內核鎖的產生是有其歷史原因的。早期linux版本對對稱多處理(SMP)器的支持非常有限,爲了保證可靠性,對處理器之間的互斥採取了‘寧可錯殺三千,不可放過一個’的方式:在內核入口處安裝一把‘巨大’的鎖,一旦一個處理器進入內核態就立刻上鎖,其它將要進入內核態的進程只能在門口等待,以此保證每次只有一個進程處於內核態運行。這把鎖就是大內核鎖。有了大內核鎖保護的系統當然可以安全地運行在多處理器上:由於同時只有一個處理器在運行內核代碼,內核的執行本質上和單處理器沒有什麼區別;而多個處理器同時運行於進程的用戶態也是安全的,因爲每個進程有自己獨立的地址空間。但是這樣粗魯地加鎖其缺點也是顯而易見的:多處理器對性能的提示只能體現在用戶態的並行處理上,而在內核態下還是單線執行,完全無法發揮多處理器的威力。於是內核開發者就開始想辦法逐步縮小這把鎖保護的範圍。實際上內核大部分代碼是多處理器安全的,只有少數全局資源需要需要在做互斥加以保護,所以沒必要限制同時運行於內核態處理器的個數。所有處理器都可隨時進入內核態運行,只要把這些需要保護的資源一一挑出來,限制同時訪問這些資源的處理器個數就可以了。這樣一來,大內核鎖從保護整個內核態縮小爲零散地保護內核態某些關鍵片段。這是一個進步,可步伐還不夠大,仍有上面提到的,‘鎖了臥室廚房也沒法進’的毛病。隨着自旋鎖的廣泛應用,新的內核代碼裏已經不再有人使用大內核鎖了。

1.2 食之無味,揮之不去

既然已經有了替代物,大內核鎖應該可以‘光榮下崗’了。可事實上沒這麼簡單。如果大內核鎖僅僅是‘只有一個實例’的自旋鎖,睿智的內核開發者早就把它替換掉了:爲每一種處於自旋鎖保護下的資源創建一把自旋鎖,把大內核鎖加鎖/解鎖替換成相應的自旋鎖的加鎖/解鎖就可以了。但如今的大內核鎖就象一個被寵壞的孩子,內核在一些關鍵點給予了它許多額外關照,使得大內核鎖的替換變得有點煩。下面是IngoMolnar在一封名爲 ’kill the BigKernel Lock (BKL)’的郵件裏的抱怨:

The biggest technical complication is that the BKL is unlike anyother lock: it "self-releases" when schedule() is called.This makes the BKL spinlock very "sticky", "invisible"and viral: it's very easy to add it to a piece of code (evenunknowingly) and you never really know whether it's held or not.PREEMPT_BKL made it even more invisible, because it made its effectseven less visible to ordinary users.

這段話的大意是:最大的技術難點是大內核鎖的與衆不同:它在調用schedule()時能夠‘自動釋放’。這一點使得大內核鎖非常麻煩和隱蔽:它使你能夠非常容易地添加一段代碼而幾乎從不知道它鎖上與否。PREEMPT_BKL選項使得它更加隱蔽,因爲這導致它的效果在普通用戶面前更加‘遁形’。

翻譯linux開發者的話比看懂他們寫的代碼更難,但有一點很明白:是schedule()函數裏對於大內核鎖的自動釋放導致了問題的複雜化。那就看看schedule()裏到底對大內核鎖執行了什麼操作:

1 /*

2 * schedule() is the main scheduler function.

3 */

4 asmlinkage void __sched schedule(void)

5 {

19 release_kernel_lock(prev);

55 context_switch(rq, prev, next); /* unlocks the rq */

67 if (unlikely(reacquire_kernel_lock(current) < 0)) {

68 prev = rq->curr;

69 switch_count = &prev->nivcsw;

70 goto need_resched_nonpreemptible;

71 }

code 1.2 1 linux_2.6.34/kernel/sched.c

在第19release_kernel_lock(prev)函數釋放當前進程(prev)所佔據的大內核鎖,接着在第55行執行進程的切換,從當前進程prev切換到了下一個進程nextcontext_switch()可以看做一個超級函數,調用它不是去執行一段代碼,而是去執行另一個進程。系統的多任務切換就是依靠這個超級函數從一個進程切換到另一個進程,從另一個進程再切換下一個進程,如此連續不斷地輪轉。只要被切走的進程還處於就緒狀態,總有一天還會有機會調度回來繼續運行,效果看起來就象函數context_switch()運行完畢返回到了schedule()。繼續運行到第67行,調用函數reacquire_kernel_lock()。這是和release_kernel_lock()配對的函數,將前面釋放的大內核鎖又重新鎖起來。If語句測試爲真表示對大內核鎖嘗試加鎖失敗,這時可以做一些優化。正常的加鎖應該是‘原地踏步’,在同一個地方反覆查詢大內核鎖的狀態,直到其它進程釋放爲止。但這樣做會浪費寶貴的處理器時間,尤其是當運行隊列裏有進程在等待運行時。所以release_lernel_lock()只是做了’try_lock’的工作,即假如沒人把持大內核鎖就把它鎖住,返回0表示成功;假如已經被鎖住就立即返回-1表示失敗。一旦失敗就重新執行一遍schedule()的主體部分,檢查運行隊列,挑選一個合適的進程運行,等到下一次被調度運行時可能鎖就解開了。這樣做利用另一個進程(假如有進程在排隊等候)的運行代替了原地死等,提高了處理器利用率。

除了在schedule()中的‘照顧’,大內核鎖還有另外的優待:在同一進程中你可以對它反覆嵌套加鎖解鎖,只要加鎖個數和解鎖個數能配上對就不會有任何問題,這是自旋鎖望塵莫及的,同一進程裏自旋鎖如果發生嵌套加鎖就會死鎖。爲此在進程控制塊(PCB)中專門爲大內核鎖開闢了加鎖計數器,即task_struct中的lock_depth域。該域的初始值爲-1,表示進程沒有獲得大內核鎖。每次加鎖時lock_depth都會加1,再檢查如果lock_depth0就執行真正的加鎖操作,這樣保證在加了一次鎖以後所有嵌套的加鎖操作都會被忽略,從而避免了死鎖。解鎖過程正好相反,每次都將lock_depth1,直到發現其值變爲-1時就執行真正的解鎖操作。

內核對大內核鎖的偏袒導致開發者在鎖住了它,進入被它保護的臨界區後,執行了不該執行的代碼卻還無法察覺。其一:程序在鎖住臨界區後必須儘快退出,否則會阻塞其它將要進入臨界區的進程。所以在臨界區裏絕對不可以調用schedule()函數,否則一旦發生進程切換何時能解鎖就變得遙遙無期。另外在使用自旋鎖保護的臨界區中做進程切換很容易造成死鎖。比如一個進程鎖住了一把自旋鎖,期間調用schedule()切換到另一個進程,而這個進程又要獲得這把鎖,這是系統就會掛死在這個進程等待解鎖的自旋處。這個問題在大內核鎖保護的臨界區是不存在的,因爲schedule()函數在調度到新進程之前會自動解鎖已經獲得的大內核鎖;在切回該進程時又會自動將大內核鎖鎖住。用戶在鎖住了大內核鎖後,幾乎無法察覺期間是否用過schedule()函數。這一點就是上面IngoMolnar提到的’technicalcomplication’:將大內核鎖替換成自旋鎖後,萬一在加鎖過程中調用了schedule(),會造成不可預估的,災難性的後果。當然作爲一個訓練有素的程序員,即使大內核鎖放寬了約束條件,也不會在臨界區中有意識地調用schedule()函數的。可是如果是調用陌生模塊的代碼,再高超的程序員也無法保證其中不會調用到該函數。其二就是上面提到的,在臨界區中不能再次獲得保護該臨界區的鎖,否則會死鎖。可是由於大內核鎖有加鎖計數器的保護,怎樣嵌套也不會有事。這也是一個’technicalcomplication’:將大內核鎖替換成自旋鎖後,萬一發生了同一把自旋鎖的嵌套加鎖後果也是災難性的。同schedule()函數一樣,訓練有素的程序員是不會有意識地多次鎖住大內核鎖,但在獲得自旋鎖後調用了陌生模塊的代碼就無法保證這些模塊中不會再次使用大內核鎖。這種情況在開發大型系統時非常常見:每個人都很小心地避免自己模塊的死鎖,可誰也無法避免當調用其它模塊時可能引入的死鎖問題。

IngoMolnar還提到了大內核鎖的另一弊端:大內核鎖沒有被lockdep所覆蓋。lockdeplinux內核的一個調試模塊,用來檢查內核互斥機制尤其是自旋鎖潛在的死鎖問題。自旋鎖由於是查詢方式等待,不釋放處理器,比一般的互斥機制更容易死鎖,故引入lockdep檢查以下幾種情況可能的死鎖(lockdep將有專門的文章詳細介紹,在此只是簡單列舉):

· 同一個進程遞歸地加鎖同一把鎖;

· 一把鎖既在中斷(或中斷下半部)使能的情況下執行過加鎖操作,又在中斷(或中斷下半部)裏執行過加鎖操作。這樣該鎖有可能在鎖定時由於中斷髮生又試圖在同一處理器上加鎖;

· 加鎖後導致依賴圖產生成閉環,這是典型的死鎖現象。

由於大內核鎖遊離於lockdep之外,它自身以及和其它互斥機制之間的依賴關係沒有受到監控,可能會導致死鎖的場景也無法被記錄下來,使得它的使用越來越混亂,處於失控狀態。

如此看來,大內核鎖已經成了內核的雞肋,而且不能與時俱進,到了非整改不可的地步。可是將大內核鎖完全從內核中移除將要面臨重重挑戰,對於那些散落在‘年久失修’,多年無人問津的代碼裏的大內核鎖,更是沒人敢去動它們。既然完全移除希望不大,那就想辦法優化它也不失爲一種權宜之計。

1.3 一改再改:無奈的選擇

早些時候大內核鎖是在自旋鎖的基礎上實現的。自旋鎖是處理器之間臨界區互斥常用的機制。當臨界區非常短暫,比如只改變幾個變量的值時,自旋鎖是一種簡單高效的互斥手段。但自旋鎖的缺點是會增大系統負荷,因爲在自旋等待過程中進程依舊佔據處理器,這部分等待時間是在做無用功。尤其是使用大內核鎖時,一把鎖管所有臨界區,發生‘碰撞’的機會就更大了。另外爲了使進程能夠儘快全速‘衝’出臨界區,自旋鎖在加鎖的同時關閉了內核搶佔式調度。因此鎖住自旋鎖就意味着在一個處理器上製造了一個調度‘禁區’:期間既不被其它進程搶佔,又不允許調用schedule()進行自主進程切換。也就是說,一旦處理器上某個進程獲得了自旋鎖,該處理器就只能一直運行該進程,即便有高優先級的實時進程就緒也只能排隊等候。調度禁區的出現增加了調度延時,降低了系統實時反應的速度,這與大家一直努力從事的內核實時化改造是背道而馳的。於是在2.6.7版本的linux中對自旋鎖做了徹底改造,放棄了自旋鎖改用信號量。信號量沒有上面提到的兩個問題:在等待信號量空閒時進程不佔用處理器,處於阻塞狀態;在獲得信號量後內核搶佔依舊是使能的,不會出現調度盲區。這樣的解決方案應該毫無爭議了。可任何事情都是有利有弊的。信號量最大的缺陷是太複雜了,每次阻塞一個進程時都要產生費時的進程上下文切換,信號量就緒喚醒等待的進程時又有一次上下文切換。除了上下文切換耗時,進程切換造成的TLB刷新,cache冷卻等都有較大開銷。如果阻塞時間比較長,達到毫秒級,這樣的切換是值得的。但是大部分情況下只需在臨界區入口等候幾十上百個指令循環另一個進程就可以交出臨界區,這時候這種切換就有點牛刀殺雞了。這就好象去醫院看普通門診,當醫生正在爲病人看病時,別的病人在門口等待一會就會輪到了,不必留下電話號碼回家睡覺,直到醫生空閒了打電話通知再匆匆趕往醫院。

由於使用信號量引起的進程頻繁切換導致大內核鎖在某些情況下出現嚴重性能問題,LinusTorvalds不得不考慮將大內核鎖的實現改回自旋鎖,自然調度延時問題也會跟着回來。這使得以‘延時迷(latencyjunkie)’自居的IngoMolnar不太高興。但linux還是LinusTorvalds說了算,於是在2.6.26-rc2版大內核鎖又變成了自旋鎖,直到現在。總的來說LinusTorvalds的改動是有道理的。使用繁瑣,重量級的信號量保護短暫的臨界區確實不值得;而且Linux也不是以實時性見長的操作系統,不應該片面追求實時信而犧牲了整體性能。

1.4 日薄西山:謝幕在即

改回自旋鎖並不意味着LinusTorvalds不關心調度延時,相反他真正的觀點是有朝一日徹底剷除大內核鎖,這一點他和IngoMolnar是英雄所見略同。可是由於剷除大內核鎖的難度和風險巨大,IngoMolnar覺得‘在當前的遊戲規則下解決大內核鎖是不現實的’必須使用新的遊戲規則。他專門建立一個版本分支叫做kill-the-BLK,在這個分支上將大內核鎖替換爲新的互斥機制,一步一步解決這個問題:

· 解決所有已知的,利用到了大內核鎖自動解鎖機制的臨界區;也就是說,消除使用大內核鎖的代碼對自動解鎖機制的依賴,使其更加接近普通的互斥機制;

· 添加許多調試設施用來警告那些在新互斥機制下不再有效的假設;

· 將大內核鎖轉換爲普通的互斥體,並刪除遺留在調度器裏的自動解鎖代碼;

· 添加lockdep對它的監控;

· 極大簡化大內核鎖代碼,最終將它從內核裏刪除。

這已經是兩年前的事情了。現在這項工作還沒結束,還在‘義無反顧’地向前推進。期待着在不遠的將來大內核鎖這一不和諧的音符徹底淡出linux的內核。



rwlock (讀寫鎖)

讀寫鎖實際是一種特殊的自旋鎖,它把對共享資源的訪問者劃分成讀者和寫者,讀者只對共享資源進行讀訪問,寫者則需要對共享資源進行寫操作。這種鎖相對於自旋鎖而言,能提高併發性,因爲在多處理器系統中,它允許同時有多個讀者來訪問共享資源,最大可能的讀者數爲實際的邏輯CPU數。寫者是排他性的,一個讀寫鎖同時只能有一個寫者或多個讀者(與CPU數相關),但不能同時既有讀者又有寫者。

在讀寫鎖保持期間也是搶佔失效的。

如果讀寫鎖當前沒有讀者,也沒有寫者,那麼寫者可以立刻獲得讀寫鎖,否則它必須自旋在那裏,直到沒有任何寫者或讀者。如果讀寫鎖沒有寫者,那麼讀者可以立即獲得該讀寫鎖,否則讀者必須自旋在那裏,直到寫者釋放該讀寫鎖。

讀寫鎖的API看上去與自旋鎖很象,只是讀者和寫者需要不同的獲得和釋放鎖的API。下面是讀寫鎖API清單:

rwlock_init(x)

該宏用於動態初始化讀寫鎖x

DEFINE_RWLOCK(x)

該宏聲明一個讀寫鎖並對其進行初始化。它用於靜態初始化。

RW_LOCK_UNLOCKED

它用於靜態初始化一個讀寫鎖。

DEFINE_RWLOCK(x)等同於rwlock_tx = RW_LOCK_UNLOCKED

read_trylock(lock)

讀者用它來盡力獲得讀寫鎖lock,如果能夠立即獲得讀寫鎖,它就獲得鎖並返回真,否則不能獲得鎖,返回假。無論是否能夠獲得鎖,它都將立即返回,絕不自旋在那裏。

write_trylock(lock)

寫者用它來盡力獲得讀寫鎖lock,如果能夠立即獲得讀寫鎖,它就獲得鎖並返回真,否則不能獲得鎖,返回假。無論是否能夠獲得鎖,它都將立即返回,絕不自旋在那裏。

read_lock(lock)

讀者要訪問被讀寫鎖lock保護的共享資源,需要使用該宏來得到讀寫鎖lock。如果能夠立即獲得,它將立即獲得讀寫鎖並返回,否則,將自旋在那裏,直到獲得該讀寫鎖。

write_lock(lock)

寫者要想訪問被讀寫鎖lock保護的共享資源,需要使用該宏來得到讀寫鎖lock。如果能夠立即獲得,它將立即獲得讀寫鎖並返回,否則,將自旋在那裏,直到獲得該讀寫鎖。

read_lock_irqsave(lock, flags)

讀者也可以使用該宏來獲得讀寫鎖,與read_lock不同的是,該宏還同時把標誌寄存器的值保存到了變量flags中,並失效了本地中斷。

write_lock_irqsave(lock, flags)

寫者可以用它來獲得讀寫鎖,與write_lock不同的是,該宏還同時把標誌寄存器的值保存到了變量flags中,並失效了本地中斷。

read_lock_irq(lock)

讀者也可以用它來獲得讀寫鎖,與read_lock不同的是,該宏還同時失效了本地中斷。該宏與read_lock_irqsave的不同之處是,它沒有保存標誌寄存器。

write_lock_irq(lock)

寫者也可以用它來獲得鎖,與write_lock不同的是,該宏還同時失效了本地中斷。該宏與write_lock_irqsave的不同之處是,它沒有保存標誌寄存器。

read_lock_bh(lock)

讀者也可以用它來獲得讀寫鎖,與與read_lock不同的是,該宏還同時失效了本地的軟中斷。

write_lock_bh(lock)

寫者也可以用它來獲得讀寫鎖,與write_lock不同的是,該宏還同時失效了本地的軟中斷。

read_unlock(lock)

讀者使用該宏來釋放讀寫鎖lock。它必須與read_lock配對使用。

write_unlock(lock)

寫者使用該宏來釋放讀寫鎖lock。它必須與write_lock配對使用。

read_unlock_irqrestore(lock, flags)

讀者也可以使用該宏來釋放讀寫鎖,與read_unlock不同的是,該宏還同時把標誌寄存器的值恢復爲變量flags的值。它必須與read_lock_irqsave配對使用。

write_unlock_irqrestore(lock, flags)

寫者也可以使用該宏來釋放讀寫鎖,與write_unlock不同的是,該宏還同時把標誌寄存器的值恢復爲變量flags的值,並使能本地中斷。它必須與write_lock_irqsave配對使用。

read_unlock_irq(lock)

讀者也可以使用該宏來釋放讀寫鎖,與read_unlock不同的是,該宏還同時使能本地中斷。它必須與read_lock_irq配對使用。

write_unlock_irq(lock)

寫者也可以使用該宏來釋放讀寫鎖,與write_unlock不同的是,該宏還同時使能本地中斷。它必須與write_lock_irq配對使用。

read_unlock_bh(lock)

讀者也可以使用該宏來釋放讀寫鎖,與read_unlock不同的是,該宏還同時使能本地軟中斷。它必須與read_lock_bh配對使用。

write_unlock_bh(lock)

寫者也可以使用該宏來釋放讀寫鎖,與write_unlock不同的是,該宏還同時使能本地軟中斷。它必須與write_lock_bh配對使用。

讀寫鎖的獲得和釋放鎖的方法也有許多版本,具體用哪個與自旋鎖一樣,因此參考自旋鎖部分就可以了。只是需要區分讀者與寫者,讀者要用讀者版本,而寫者必須用寫者版本。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章