午夜视频在线网站,日韩视频精品在线,中文字幕精品一区二区三区在线,在线播放精品,1024你懂我懂的旧版人,欧美日韩一级黄色片,一区二区三区在线观看视频

分享

Linux鎖機(jī)制

 linux_android 2014-09-29

在linux內(nèi)核中,有很多同步機(jī)制。比較經(jīng)典的有原子操作、spin_lock(忙等待的鎖)、mutex(互斥鎖)、semaphore(信號(hào)量)等。并且它們幾乎都有對(duì)應(yīng)的rw_XXX(讀寫(xiě)鎖),以便在能夠區(qū)分讀與寫(xiě)的情況下,讓讀操作相互不互斥(讀寫(xiě)、寫(xiě)寫(xiě)依然互斥)。而seqlock和rcu應(yīng)該可以不算在經(jīng)典之列,它們是兩種比較有意思的同步機(jī)制。

atomic(原子操作):

所謂原子操作,就是該操作絕不會(huì)在執(zhí)行完畢前被任何其他任務(wù)或事件打斷,也就說(shuō),它的最小的執(zhí)行單位,不可能有比它更小的執(zhí)行單位,因此這里的原子實(shí)際是使用了物理學(xué)里的物質(zhì)微粒的概念。
原子操作需要硬件的支持,因此是架構(gòu)相關(guān)的,其API和原子類型的定義都定義在內(nèi)核源碼樹(shù)的include/asm/atomic.h文件中,它們都使用匯編語(yǔ)言實(shí)現(xiàn),因?yàn)镃語(yǔ)言并不能實(shí)現(xiàn)這樣的操作。

原子操作主要用于實(shí)現(xiàn)資源計(jì)數(shù),很多引用計(jì)數(shù)(refcnt)就是通過(guò)原子操作實(shí)現(xiàn)的。

原子類型定義如下:
typedefstruct { volatile int counter; }atomic_t;
volatile
修飾字段告訴gcc不要對(duì)該類型的數(shù)據(jù)做優(yōu)化處理,對(duì)它的訪問(wèn)都是對(duì)內(nèi)存的訪問(wèn),而不是對(duì)寄存器的訪問(wèn)。

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

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

該函數(shù)設(shè)置原子類型的變量v的值為i。
voidatomic_add(int i, atomic_t *v);

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

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

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

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

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

該函數(shù)對(duì)原子類型的變量v原子地減1,并判斷結(jié)果是否為0,如果為0,返回真,否則返回假。
intatomic_inc_and_test(atomic_t *v);

該函數(shù)對(duì)原子類型的變量v原子地增加1,并判斷結(jié)果是否為0,如果為0,返回真,否則返回假。
intatomic_add_negative(int i, atomic_t*v);

該函數(shù)對(duì)原子類型的變量v原子地增加I,并判斷結(jié)果是否為負(fù)數(shù),如果是,返回真,否則返回假。
intatomic_add_return(int i, atomic_t *v);

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

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

intatomic_inc_return(atomic_t * v);

該函數(shù)對(duì)原子類型的變量v原子地增加1并且返回指向v的指針。

intatomic_dec_return(atomic_t * v);

該函數(shù)對(duì)原子類型的變量v原子地減1并且返回指向v的指針。

原子操作通常用于實(shí)現(xiàn)資源的引用計(jì)數(shù),在TCP/IP協(xié)議棧的IP碎片處理中,就使用了引用計(jì)數(shù),碎片隊(duì)列結(jié)構(gòu)structipq描述了一個(gè)IP碎片,字段refcnt就是引用計(jì)數(shù)器,它的類型為atomic_t,當(dāng)創(chuàng)建IP碎片時(shí)(在函數(shù)ip_frag_create中),使用atomic_set函數(shù)把它設(shè)置為1,當(dāng)引用該IP碎片時(shí),就使用函數(shù)atomic_inc把引用計(jì)數(shù)加1,當(dāng)不需要引用該IP碎片時(shí),就使用函數(shù)ipq_put來(lái)釋放該IP碎片,ipq_put使用函數(shù)atomic_dec_and_test把引用計(jì)數(shù)減1并判斷引用計(jì)數(shù)是否為0,如果是就釋放Ip碎片。函數(shù)ipq_kill把IP碎片從ipq隊(duì)列中刪除,并把該刪除的IP碎片的引用計(jì)數(shù)減1(通過(guò)使用函數(shù)atomic_dec實(shí)現(xiàn))。



Spanlock(自旋鎖)

自旋鎖與互斥鎖有點(diǎn)類似,只是自旋鎖不會(huì)引起調(diào)用者睡眠,如果自旋鎖已經(jīng)被別的執(zhí)行單元保持,調(diào)用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖,"自旋"一詞就是因此而得名。由于自旋鎖使用者一般保持鎖時(shí)間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠(yuǎn)高于互斥鎖。

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

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

跟互斥鎖一樣,一個(gè)執(zhí)行單元要想訪問(wèn)被自旋鎖保護(hù)的共享資源,必須先得到鎖,在訪問(wèn)完共享資源后,必須釋放鎖。如果在獲取自旋鎖時(shí),沒(méi)有任何執(zhí)行單元保持該鎖,那么將立即得到鎖;如果在獲取自旋鎖時(shí)鎖已經(jīng)有保持者,那么獲取鎖操作將自旋在那里,直到該自旋鎖的保持者釋放了鎖。

無(wú)論是互斥鎖,還是自旋鎖,在任何時(shí)刻,最多只能有一個(gè)保持者,也就說(shuō),在任何時(shí)刻最多只能有一個(gè)執(zhí)行單元獲得鎖。

自旋鎖的API有:

spin_lock_init(x)

該宏用于初始化自旋鎖x。自旋鎖在真正使用前必須先初始化。該宏用于動(dòng)態(tài)初始化。

DEFINE_SPINLOCK(x)

該宏聲明一個(gè)自旋鎖x并初始化它。該宏在2.6.11中第一次被定義,在先前的內(nèi)核中并沒(méi)有該宏。

SPIN_LOCK_UNLOCKED

該宏用于靜態(tài)初始化一個(gè)自旋鎖。

DEFINE_SPINLOCK(x)等同于spinlock_tx = SPIN_LOCK_UNLOCKED

spin_is_locked(x)

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

spin_unlock_wait(x)

該宏用于等待自旋鎖x變得沒(méi)有被任何執(zhí)行單元保持,如果沒(méi)有任何執(zhí)行單元保持該自旋鎖,該宏立即返回,否則將循環(huán)在那里,直到該自旋鎖被保持者釋放。

spin_trylock(lock)

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

spin_lock(lock)

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

spin_lock_irqsave(lock, flags)

該宏獲得自旋鎖的同時(shí)把標(biāo)志寄存器的值保存到變量flags中并失效本地中斷。

spin_lock_irq(lock)

該宏類似于spin_lock_irqsave,只是該宏不保存標(biāo)志寄存器的值。

spin_lock_bh(lock)

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

spin_unlock(lock)

該宏釋放自旋鎖lock,它與spin_trylock或spin_lock配對(duì)使用。如果spin_trylock返回假,表明沒(méi)有獲得自旋鎖,因此不必使用spin_unlock釋放。

spin_unlock_irqrestore(lock, flags)

該宏釋放自旋鎖lock的同時(shí),也恢復(fù)標(biāo)志寄存器的值為變量flags保存的值。它與spin_lock_irqsave配對(duì)使用。

spin_unlock_irq(lock)

該宏釋放自旋鎖lock的同時(shí),也使能本地中斷。它與spin_lock_irq配對(duì)應(yīng)用。

spin_unlock_bh(lock)

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

spin_trylock_irqsave(lock, flags)

該宏如果獲得自旋鎖lock,它也將保存標(biāo)志寄存器的值到變量flags中,并且失效本地中斷,如果沒(méi)有獲得鎖,它什么也不做。因此如果能夠立即獲得鎖,它等同于spin_lock_irqsave,如果不能獲得鎖,它等同于spin_trylock。如果該宏獲得自旋鎖lock,那需要使用spin_unlock_irqrestore來(lái)釋放。

spin_trylock_irq(lock)

該宏類似于spin_trylock_irqsave,只是該宏不保存標(biāo)志寄存器。如果該宏獲得自旋鎖lock,需要使用spin_unlock_irq來(lái)釋放。

spin_trylock_bh(lock)

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

spin_can_lock(lock)

該宏用于判斷自旋鎖lock是否能夠被鎖,它實(shí)際是spin_is_locked取反。如果lock沒(méi)有被鎖,它返回真,否則,返回假。該宏在2.6.11中第一次被定義,在先前的內(nèi)核中并沒(méi)有該宏。

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

如果被保護(hù)的共享資源只在進(jìn)程上下文訪問(wèn)和軟中斷上下文訪問(wèn),那么當(dāng)在進(jìn)程上下文訪問(wèn)共享資源時(shí),可能被軟中斷打斷,從而可能進(jìn)入軟中斷上下文來(lái)對(duì)被保護(hù)的共享資源訪問(wèn),因此對(duì)于這種情況,對(duì)共享資源的訪問(wèn)必須使用spin_lock_bh和spin_unlock_bh來(lái)保護(hù)。當(dāng)然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它們失效了本地硬中斷,失效硬中斷隱式地也失效了軟中斷。但是使用spin_lock_bh和spin_unlock_bh是最恰當(dāng)?shù)?,它比其他兩個(gè)快。

如果被保護(hù)的共享資源只在進(jìn)程上下文和tasklet或timer上下文訪問(wèn),那么應(yīng)該使用與上面情況相同的獲得和釋放鎖的宏,因?yàn)閠asklet和timer是用軟中斷實(shí)現(xiàn)的。

如果被保護(hù)的共享資源只在一個(gè)tasklet或timer上下文訪問(wèn),那么不需要任何自旋鎖保護(hù),因?yàn)橥粋€(gè)tasklet或timer只能在一個(gè)CPU上運(yùn)行,即使是在SMP環(huán)境下也是如此。實(shí)際上tasklet在調(diào)用tasklet_schedule標(biāo)記其需要被調(diào)度時(shí)已經(jīng)把該tasklet綁定到當(dāng)前CPU,因此同一個(gè)tasklet決不可能同時(shí)在其他CPU上運(yùn)行。timer也是在其被使用add_timer添加到timer隊(duì)列中時(shí)已經(jīng)被幫定到當(dāng)前CPU,所以同一個(gè)timer絕不可能運(yùn)行在其他CPU上。當(dāng)然同一個(gè)tasklet有兩個(gè)實(shí)例同時(shí)運(yùn)行在同一個(gè)CPU就更不可能了。

如果被保護(hù)的共享資源只在兩個(gè)或多個(gè)tasklet或timer上下文訪問(wèn),那么對(duì)共享資源的訪問(wèn)僅需要用spin_lock和spin_unlock來(lái)保護(hù),不必使用_bh版本,因?yàn)楫?dāng)tasklet或timer運(yùn)行時(shí),不可能有其他tasklet或timer在當(dāng)前CPU上運(yùn)行。如果被保護(hù)的共享資源只在一個(gè)軟中斷(tasklet和timer除外)上下文訪問(wèn),那么這個(gè)共享資源需要用spin_lock和spin_unlock來(lái)保護(hù),因?yàn)橥瑯拥能浿袛嗫梢酝瑫r(shí)在不同的CPU上運(yùn)行。

如果被保護(hù)的共享資源在兩個(gè)或多個(gè)軟中斷上下文訪問(wèn),那么這個(gè)共享資源當(dāng)然更需要用spin_lock和spin_unlock來(lái)保護(hù),不同的軟中斷能夠同時(shí)在不同的CPU上運(yùn)行。

如果被保護(hù)的共享資源在軟中斷(包括tasklet和timer)或進(jìn)程上下文和硬中斷上下文訪問(wèn),那么在軟中斷或進(jìn)程上下文訪問(wèn)期間,可能被硬中斷打斷,從而進(jìn)入硬中斷上下文對(duì)共享資源進(jìn)行訪問(wèn),因此,在進(jìn)程或軟中斷上下文需要使用spin_lock_irq和spin_unlock_irq來(lái)保護(hù)對(duì)共享資源的訪問(wèn)。而在中斷處理句柄中使用什么版本,需依情況而定,如果只有一個(gè)中斷處理句柄訪問(wèn)該共享資源,那么在中斷處理句柄中僅需要spin_lock和spin_unlock來(lái)保護(hù)對(duì)共享資源的訪問(wèn)就可以了。因?yàn)樵趫?zhí)行中斷處理句柄期間,不可能被同一CPU上的軟中斷或進(jìn)程打斷。但是如果有不同的中斷處理句柄訪問(wèn)該共享資源,那么需要在中斷處理句柄中使用spin_lock_irq和spin_unlock_irq來(lái)保護(hù)對(duì)共享資源的訪問(wèn)。

在使用spin_lock_irq和spin_unlock_irq的情況下,完全可以用spin_lock_irqsave和spin_unlock_irqrestore取代,那具體應(yīng)該使用哪一個(gè)也需要依情況而定,如果可以確信在對(duì)共享資源訪問(wèn)前中斷是使能的,那么使用spin_lock_irq更好一些,因?yàn)樗萻pin_lock_irqsave要快一些,但是如果你不能確定是否中斷使能,那么使用spin_lock_irqsave和spin_unlock_irqrestore更好,因?yàn)樗鼘⒒謴?fù)訪問(wèn)共享資源前的中斷標(biāo)志而不是直接使能中斷。當(dāng)然,有些情況下需要在訪問(wèn)共享資源時(shí)必須中斷失效,而訪問(wèn)完后必須中斷使能,這樣的情形使用spin_lock_irq和spin_unlock_irq最好。

需要特別提醒讀者,spin_lock用于阻止在不同CPU上的執(zhí)行單元對(duì)共享資源的同時(shí)訪



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};



一、作用及訪問(wèn)規(guī)則:

互斥鎖主要用于實(shí)現(xiàn)內(nèi)核中的互斥訪問(wèn)功能。內(nèi)核互斥鎖是在原子API之上實(shí)現(xiàn)的,但這對(duì)于內(nèi)核用戶是不可見(jiàn)的。對(duì)它的訪問(wèn)必須遵循一些規(guī)則:同一時(shí)間只能有一個(gè)任務(wù)持有互斥鎖,而且只有這個(gè)任務(wù)可以對(duì)互斥鎖進(jìn)行解鎖?;コ怄i不能進(jìn)行遞歸鎖定或解鎖。一個(gè)互斥鎖對(duì)象必須通過(guò)其API初始化,而不能使用memset或復(fù)制初始化。一個(gè)任務(wù)在持有互斥鎖的時(shí)候是不能結(jié)束的?;コ怄i所使用的內(nèi)存區(qū)域是不能被釋放的。使用中的互斥鎖是不能被重新初始化的。并且互斥鎖不能用于中斷上下文。但是互斥鎖比當(dāng)前的內(nèi)核信號(hào)量選項(xiàng)更快,并且更加緊湊,因此如果它們滿足您的需求,那么它們將是您明智的選擇。

二、各字段詳解:

1、atomic_t count; --指示互斥鎖的狀態(tài):1沒(méi)有上鎖,可以獲得;0被鎖定,不能獲得;負(fù)數(shù)被鎖定,且可能在該鎖上有等待進(jìn)程初始化為沒(méi)有上鎖。

2、spinlock_t wait_lock;--等待獲取互斥鎖中使用的自旋鎖。在獲取互斥鎖的過(guò)程中,操作會(huì)在自旋鎖的保護(hù)中進(jìn)行。初始化為為鎖定。

3、struct list_head wait_list;--等待互斥鎖的進(jìn)程隊(duì)列。

四、操作:

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并初始化為未鎖定,己count為1,wait_lock為未上鎖,等待隊(duì)列wait_list為空。

2、獲取互斥鎖:

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

void inline __sched mutex_lock(struct mutex *lock)

{

might_sleep();

__mutex_fastpath_lock(&lock->count,__mutex_lock_slowpath);

}

獲取互斥鎖。實(shí)際上是先給count做自減操作,然后使用本身的自旋鎖進(jìn)入臨界區(qū)操作。首先取得count的值,再將count置為-1,判斷如果原來(lái)count的值為1,也即互斥鎖可以獲得,則直接獲取,跳出。否則進(jìn)入循環(huán)反復(fù)測(cè)試互斥鎖的狀態(tài)。在循環(huán)中,也是先取得互斥鎖原來(lái)的狀態(tài),再將其置為-1,判斷如果可以獲取(等于1),則退出循環(huán),否則設(shè)置當(dāng)前進(jìn)程的狀態(tài)為不可中斷狀態(tài),解鎖自身的自旋鎖,進(jìn)入睡眠狀態(tài),待被在調(diào)度喚醒時(shí),再獲得自身的自旋鎖,進(jìn)入新一次的查詢其自身狀態(tài)(該互斥鎖的狀態(tài))的循環(huán)。

(2)具體參見(jiàn)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()一樣,也是獲取互斥鎖。在獲得了互斥鎖或進(jìn)入睡眠直到獲得互斥鎖之后會(huì)返回0。如果在等待獲取鎖的時(shí)候進(jìn)入睡眠狀態(tài)收到一個(gè)信號(hào)(被信號(hào)打斷睡眠),則返回_EINIR。

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

int __sched mutex_trylock(struct mutex *lock)

{

return __mutex_fastpath_trylock(&lock->count,

__mutex_trylock_slowpath);

}

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

3、釋放互斥鎖:

具體參見(jiàn)linux/kernel/mutex.c

void __sched mutex_unlock(struct mutex *lock)

{

__mutex_fastpath_unlock(&lock->count,__mutex_unlock_slowpath);

}

釋放被當(dāng)前進(jìn)程獲取的互斥鎖。該函數(shù)不能用在中斷上下文中,而且不允許去釋放一個(gè)沒(méi)有上鎖的互斥鎖。

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

用mutex_destroy()函數(shù)解除由lock指向的互斥鎖的任何狀態(tài)。在調(diào)用執(zhí)行這個(gè)函數(shù)的時(shí)候,lock指向的互斥鎖不能在被鎖狀態(tài)。儲(chǔ)存互斥鎖的內(nèi)存不被釋放。

返回值--mutex_destroy()在成功執(zhí)行后返回零。其他值意味著錯(cuò)誤。在以下情況發(fā)生時(shí),函數(shù)失敗并返回相關(guān)值。

EINVAL 非法參數(shù)

EFAULT mp指向一個(gè)非法地址。

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

這個(gè)調(diào)用實(shí)際上編譯成一個(gè)內(nèi)聯(lián)函數(shù)。如果互斥鎖被持有(鎖定),那么就會(huì)返回1;否則,返回0

五、使用形式:

struct mutex mutex;

mutex_init(&mutex);

...

mutex_lock(&mutex);

...

mutex_unlock(&mutex);

semaphore (信號(hào)量)

Linux內(nèi)核的信號(hào)量在概念和原理上與用戶態(tài)的SystemV的IPC機(jī)制信號(hào)量是一樣的,但是它絕不可能在內(nèi)核之外使用,因此它與SystemV的IPC機(jī)制信號(hào)量毫不相干。

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

信號(hào)量的API有:

DECLARE_MUTEX(name)

該宏聲明一個(gè)信號(hào)量name并初始化它的值為0,即聲明一個(gè)互斥鎖。

DECLARE_MUTEX_LOCKED(name)

該宏聲明一個(gè)互斥鎖name,但把它的初始值設(shè)置為0,即鎖在創(chuàng)建時(shí)就處在已鎖狀態(tài)。因此對(duì)于這種鎖,一般是先釋放后獲得。

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

該函用于數(shù)初始化設(shè)置信號(hào)量的初值,它設(shè)置信號(hào)量sem的值為val。

void init_MUTEX (struct semaphore *sem);

該函數(shù)用于初始化一個(gè)互斥鎖,即它把信號(hào)量sem的值設(shè)置為1。

void init_MUTEX_LOCKED (struct semaphore *sem);

該函數(shù)也用于初始化一個(gè)互斥鎖,但它把信號(hào)量sem的值設(shè)置為0,即一開(kāi)始就處在已鎖狀態(tài)。

void down(struct semaphore * sem);

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

int down_interruptible(struct semaphore * sem);

該函數(shù)功能與down類似,不同之處為,down不會(huì)被信號(hào)(signal)打斷,但down_interruptible能被信號(hào)打斷,因此該函數(shù)有返回值來(lái)區(qū)分是正常返回還是被信號(hào)中斷,如果返回0,表示獲得信號(hào)量正常返回,如果被信號(hào)打斷,返回-EINTR。

int down_trylock(struct semaphore * sem);

該函數(shù)試著獲得信號(hào)量sem,如果能夠立刻獲得,它就獲得該信號(hào)量并返回0,否則,表示不能獲得信號(hào)量sem,返回值為非0值。因此,它不會(huì)導(dǎo)致調(diào)用者睡眠,可以在中斷上下文使用。

void up(struct semaphore * sem);

該函數(shù)釋放信號(hào)量sem,即把sem的值加1,如果sem的值為非正數(shù),表明有任務(wù)等待該信號(hào)量,因此喚醒這些等待者。

信號(hào)量在絕大部分情況下作為互斥鎖使用,下面以console驅(qū)動(dòng)系統(tǒng)為例說(shuō)明信號(hào)量的使用。

在內(nèi)核源碼樹(shù)的kernel/printk.c中,使用宏DECLARE_MUTEX聲明了一個(gè)互斥鎖console_sem,它用于保護(hù)console驅(qū)動(dòng)列表console_drivers以及同步對(duì)整個(gè)console驅(qū)動(dòng)系統(tǒng)的訪問(wèn),其中定義了函數(shù)acquire_console_sem來(lái)獲得互斥鎖console_sem,定義了release_console_sem來(lái)釋放互斥鎖console_sem,定義了函數(shù)try_acquire_console_sem來(lái)盡力得到互斥鎖console_sem。這三個(gè)函數(shù)實(shí)際上是分別對(duì)函數(shù)down,up和down_trylock的簡(jiǎn)單包裝。需要訪問(wèn)console_drivers驅(qū)動(dòng)列表時(shí)就需要使用acquire_console_sem來(lái)保護(hù)console_drivers列表,當(dāng)訪問(wèn)完該列表后,就調(diào)用release_console_sem釋放信號(hào)量console_sem。函數(shù)console_unblank,console_device,console_stop,console_start,register_console和unregister_console都需要訪問(wèn)console_drivers,因此它們都使用函數(shù)對(duì)acquire_console_sem和release_console_sem來(lái)對(duì)console_drivers進(jìn)行保護(hù)。



rw_semaphore (讀寫(xiě)信號(hào)量)

讀寫(xiě)信號(hào)量對(duì)訪問(wèn)者進(jìn)行了細(xì)分,或者為讀者,或者為寫(xiě)者,讀者在保持讀寫(xiě)信號(hào)量期間只能對(duì)該讀寫(xiě)信號(hào)量保護(hù)的共享資源進(jìn)行讀訪問(wèn),如果一個(gè)任務(wù)除了需要讀,可能還需要寫(xiě),那么它必須被歸類為寫(xiě)者,它在對(duì)共享資源訪問(wèn)之前必須先獲得寫(xiě)者身份,寫(xiě)者在發(fā)現(xiàn)自己不需要寫(xiě)訪問(wèn)的情況下可以降級(jí)為讀者。讀寫(xiě)信號(hào)量同時(shí)擁有的讀者數(shù)不受限制,也就說(shuō)可以有任意多個(gè)讀者同時(shí)擁有一個(gè)讀寫(xiě)信號(hào)量。如果一個(gè)讀寫(xiě)信號(hào)量當(dāng)前沒(méi)有被寫(xiě)者擁有并且也沒(méi)有寫(xiě)者等待讀者釋放信號(hào)量,那么任何讀者都可以成功獲得該讀寫(xiě)信號(hào)量;否則,讀者必須被掛起直到寫(xiě)者釋放該信號(hào)量。如果一個(gè)讀寫(xiě)信號(hào)量當(dāng)前沒(méi)有被讀者或?qū)懻邠碛胁⑶乙矝](méi)有寫(xiě)者等待該信號(hào)量,那么一個(gè)寫(xiě)者可以成功獲得該讀寫(xiě)信號(hào)量,否則寫(xiě)者將被掛起,直到?jīng)]有任何訪問(wèn)者。因此,寫(xiě)者是排他性的,獨(dú)占性的。

讀寫(xiě)信號(hào)量有兩種實(shí)現(xiàn),一種是通用的,不依賴于硬件架構(gòu),因此,增加新的架構(gòu)不需要重新實(shí)現(xiàn)它,但缺點(diǎn)是性能低,獲得和釋放讀寫(xiě)信號(hào)量的開(kāi)銷大;另一種是架構(gòu)相關(guān)的,因此性能高,獲取和釋放讀寫(xiě)信號(hào)量的開(kāi)銷小,但增加新的架構(gòu)需要重新實(shí)現(xiàn)。在內(nèi)核配置時(shí),可以通過(guò)選項(xiàng)去控制使用哪一種實(shí)現(xiàn)。

讀寫(xiě)信號(hào)量的相關(guān)API有:

DECLARE_RWSEM(name)

該宏聲明一個(gè)讀寫(xiě)信號(hào)量name并對(duì)其進(jìn)行初始化。

void init_rwsem(struct rw_semaphore *sem);

該函數(shù)對(duì)讀寫(xiě)信號(hào)量sem進(jìn)行初始化。

void down_read(struct rw_semaphore *sem);

讀者調(diào)用該函數(shù)來(lái)得到讀寫(xiě)信號(hào)量sem。該函數(shù)會(huì)導(dǎo)致調(diào)用者睡眠,因此只能在進(jìn)程上下文使用。

int down_read_trylock(struct rw_semaphore *sem);

該函數(shù)類似于down_read,只是它不會(huì)導(dǎo)致調(diào)用者睡眠。它盡力得到讀寫(xiě)信號(hào)量sem,如果能夠立即得到,它就得到該讀寫(xiě)信號(hào)量,并且返回1,否則表示不能立刻得到該信號(hào)量,返回0。因此,它也可以在中斷上下文使用。

void down_write(struct rw_semaphore *sem);

寫(xiě)者使用該函數(shù)來(lái)得到讀寫(xiě)信號(hào)量sem,它也會(huì)導(dǎo)致調(diào)用者睡眠,因此只能在進(jìn)程上下文使用。

int down_write_trylock(struct rw_semaphore *sem);

該函數(shù)類似于down_write,只是它不會(huì)導(dǎo)致調(diào)用者睡眠。該函數(shù)盡力得到讀寫(xiě)信號(hào)量,如果能夠立刻獲得,就獲得該讀寫(xiě)信號(hào)量并且返回1,否則表示無(wú)法立刻獲得,返回0。它可以在中斷上下文使用。

void up_read(struct rw_semaphore *sem);

讀者使用該函數(shù)釋放讀寫(xiě)信號(hào)量sem。它與down_read或down_read_trylock配對(duì)使用。如果down_read_trylock返回0,不需要調(diào)用up_read來(lái)釋放讀寫(xiě)信號(hào)量,因?yàn)楦揪蜎](méi)有獲得信號(hào)量。

void up_write(struct rw_semaphore *sem);

寫(xiě)者調(diào)用該函數(shù)釋放信號(hào)量sem。它與down_write或down_write_trylock配對(duì)使用。如果down_write_trylock返回0,不需要調(diào)用up_write,因?yàn)榉祷?表示沒(méi)有獲得該讀寫(xiě)信號(hào)量。

void downgrade_write(struct rw_semaphore *sem);

該函數(shù)用于把寫(xiě)者降級(jí)為讀者,這有時(shí)是必要的。因?yàn)閷?xiě)者是排他性的,因此在寫(xiě)者保持讀寫(xiě)信號(hào)量期間,任何讀者或?qū)懻叨紝o(wú)法訪問(wèn)該讀寫(xiě)信號(hào)量保護(hù)的共享資源,對(duì)于那些當(dāng)前條件下不需要寫(xiě)訪問(wèn)的寫(xiě)者,降級(jí)為讀者將,使得等待訪問(wèn)的讀者能夠立刻訪問(wèn),從而增加了并發(fā)性,提高了效率。

讀寫(xiě)信號(hào)量適于在讀多寫(xiě)少的情況下使用,在linux內(nèi)核中對(duì)進(jìn)程的內(nèi)存映像描述結(jié)構(gòu)的訪問(wèn)就使用了讀寫(xiě)信號(hào)量進(jìn)行保護(hù)。在Linux中,每一個(gè)進(jìn)程都用一個(gè)類型為task_t或structtask_struct的結(jié)構(gòu)來(lái)描述,該結(jié)構(gòu)的類型為structmm_struct的字段mm描述了進(jìn)程的內(nèi)存映像,特別是mm_struct結(jié)構(gòu)的mmap字段維護(hù)了整個(gè)進(jìn)程的內(nèi)存塊列表,該列表將在進(jìn)程生存期間被大量地遍利或修改,因此mm_struct結(jié)構(gòu)就有一個(gè)字段mmap_sem來(lái)對(duì)mmap的訪問(wèn)進(jìn)行保護(hù),mmap_sem就是一個(gè)讀寫(xiě)信號(hào)量,在proc文件系統(tǒng)里有很多進(jìn)程內(nèi)存使用情況的接口,通過(guò)它們能夠查看某一進(jìn)程的內(nèi)存使用情況,命令free、ps和top都是通過(guò)proc來(lái)得到內(nèi)存使用信息的,proc接口就使用down_read和up_read來(lái)讀取進(jìn)程的mmap信息。當(dāng)進(jìn)程動(dòng)態(tài)地分配或釋放內(nèi)存時(shí),需要修改mmap來(lái)反映分配或釋放后的內(nèi)存映像,因此動(dòng)態(tài)內(nèi)存分配或釋放操作需要以寫(xiě)者身份獲得讀寫(xiě)信號(hào)量mmap_sem來(lái)對(duì)mmap進(jìn)行更新。系統(tǒng)調(diào)用brk和munmap就使用了down_write和up_write來(lái)保護(hù)對(duì)mmap的訪問(wèn)。



seqlock(順序鎖)

用于能夠區(qū)分讀與寫(xiě)的場(chǎng)合,并且是讀操作很多、寫(xiě)操作很少,寫(xiě)操作的優(yōu)先權(quán)大于讀操作。
seqlock
的實(shí)現(xiàn)思路是,用一個(gè)遞增的整型數(shù)表示sequence。寫(xiě)操作進(jìn)入臨界區(qū)時(shí),sequence++;退出臨界區(qū)時(shí),sequence再++。寫(xiě)操作還需要獲得一個(gè)鎖(比如mutex),這個(gè)鎖僅用于寫(xiě)寫(xiě)互斥,以保證同一時(shí)間最多只有一個(gè)正在進(jìn)行的寫(xiě)操作。
當(dāng)sequence為奇數(shù)時(shí),表示有寫(xiě)操作正在進(jìn)行,這時(shí)讀操作要進(jìn)入臨界區(qū)需要等待,直到sequence變?yōu)榕紨?shù)。讀操作進(jìn)入臨界區(qū)時(shí),需要記錄下當(dāng)前sequence的值,等它退出臨界區(qū)的時(shí)候用記錄的sequence與當(dāng)前sequence做比較,不相等則表示在讀操作進(jìn)入臨界區(qū)期間發(fā)生了寫(xiě)操作,這時(shí)候讀操作讀到的東西是無(wú)效的,需要返回重試。

seqlock
寫(xiě)寫(xiě)是必須要互斥的。但是seqlock的應(yīng)用場(chǎng)景本身就是讀多寫(xiě)少的情況,寫(xiě)沖突的概率是很低的。所以這里的寫(xiě)寫(xiě)互斥基本上不會(huì)有什么性能損失。
而讀寫(xiě)操作是不需要互斥的。seqlock的應(yīng)用場(chǎng)景是寫(xiě)操作優(yōu)先于讀操作,對(duì)于寫(xiě)操作來(lái)說(shuō),幾乎是沒(méi)有阻塞的(除非發(fā)生寫(xiě)寫(xiě)沖突這一小概率事件),只需要做sequence++這一附加動(dòng)作。而讀操作也不需要阻塞,只是當(dāng)發(fā)現(xiàn)讀寫(xiě)沖突時(shí)需要retry

seqlock
的一個(gè)典型應(yīng)用是時(shí)鐘的更新,系統(tǒng)中每1毫秒會(huì)有一個(gè)時(shí)鐘中斷,相應(yīng)的中斷處理程序會(huì)更新時(shí)鐘(見(jiàn)《linux時(shí)鐘淺析》)(寫(xiě)操作)。而用戶程序可以調(diào)用gettimeofday之類的系統(tǒng)調(diào)用來(lái)獲取當(dāng)前時(shí)間(讀操作)。在這種情況下,使用seqlock可以避免過(guò)多的gettimeofday系統(tǒng)調(diào)用把中斷處理程序給阻塞了(如果使用讀寫(xiě)鎖,而不用seqlock的話就會(huì)這樣)。中斷處理程序總是優(yōu)先的,而如果gettimeofday系統(tǒng)調(diào)用與之沖突了,那用戶程序多等等也無(wú)妨。

seqlock
的實(shí)現(xiàn)非常簡(jiǎn)單:
寫(xiě)操作進(jìn)入臨界區(qū)時(shí):
voidwrite_seqlock(seqlock_t *sl)
{
spin_lock(&sl->lock); //
上寫(xiě)寫(xiě)互斥鎖
++sl->sequence;      // sequence++
}

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


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

讀操作嘗試退出臨界區(qū)時(shí):
intread_seqretry(const seqlock_t *sl, unsigned start)
{
return(sl->sequence != start); //
看看sequence與進(jìn)入臨界區(qū)時(shí)是否發(fā)生過(guò)改變
}

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

RCUread-copy-update

RCU
也是用于能夠區(qū)分讀與寫(xiě)的場(chǎng)合,并且也是讀多寫(xiě)少,但是讀操作的優(yōu)先權(quán)大于寫(xiě)操作(與seqlock相反)。
RCU
的實(shí)現(xiàn)思路是,讀操作不需要互斥、不需要阻塞、也不需要原子指令,直接讀就行了。而寫(xiě)操作在進(jìn)行之前需要把被寫(xiě)的對(duì)象copy一份,寫(xiě)完之后再更新回去。其實(shí)RCU所能保護(hù)的并不是任意的臨界區(qū),它只能保護(hù)由指針指向的對(duì)象(而不保護(hù)指針本身)。讀操作通過(guò)這個(gè)指針來(lái)訪問(wèn)對(duì)象(這個(gè)對(duì)象就是臨界區(qū));寫(xiě)操作把對(duì)象復(fù)制一份,然后更新,最后修改指針使其指向新的對(duì)象。由于指針總是一個(gè)字長(zhǎng)的,對(duì)它的讀寫(xiě)對(duì)于CPU來(lái)說(shuō)總是原子的,所以不用擔(dān)心更新指針只更新到一半就被讀取的情況(指針的值為0x11111111,要更新為0x22222222,不會(huì)出現(xiàn)類似0x11112222這樣的中間狀態(tài))。所以,當(dāng)讀寫(xiě)操作同時(shí)發(fā)生時(shí),讀操作要么讀到指針的舊值,引用了更新前的對(duì)象、要么讀到了指針的新值,引用了更新后的對(duì)象。即使同時(shí)有多個(gè)寫(xiě)操作發(fā)生也沒(méi)關(guān)系(是否需要寫(xiě)寫(xiě)互斥跟寫(xiě)操作本身的場(chǎng)景相關(guān))。

RCU
封裝了rcu_dereference和rcu_assign_pointer兩個(gè)函數(shù),分別用于對(duì)指針進(jìn)行讀和寫(xiě)。
rcu_assign_pointer(p,v) => (p) = (v)
rcu_dereference(p) => (p)
里面其實(shí)就是簡(jiǎn)單的指針讀和寫(xiě),然后可能設(shè)置內(nèi)存屏障(以避免編譯器或CPU指令亂序?qū)Τ绦蛟斐捎绊懀?。?dāng)然,如果出現(xiàn)了一種奇怪的不能直接保證原子性讀寫(xiě)指針的體系結(jié)構(gòu),還需要這兩個(gè)函數(shù)來(lái)保證原子性。

可以看到,使用了RCU之后,讀寫(xiě)操作竟然神奇地都不需要阻塞了,臨界區(qū)已經(jīng)不是臨界區(qū)了。只不過(guò)寫(xiě)操作稍微麻煩些,需要read、copy再update。不過(guò)RCU的核心問(wèn)題并不是如何同步,而是如何釋放舊的對(duì)象。指向?qū)ο蟮闹羔槺桓铝?,但是之前發(fā)生的讀操作可能還在引用舊的對(duì)象呢,舊的對(duì)象什么時(shí)候釋放掉呢?讓讀操作來(lái)釋放舊的對(duì)象似乎并不是很和理,它不知道對(duì)象是否已經(jīng)被更新了,也不知道有多少讀操作都引用了這個(gè)舊對(duì)象。給對(duì)象加一個(gè)引用計(jì)數(shù)呢?這或許可以奏效,但是這也太不通用了,RCU是一種機(jī)制,如果要求每個(gè)使用RCU的對(duì)象都在對(duì)象的某某位置維護(hù)一個(gè)引用計(jì)數(shù),相當(dāng)于RCU機(jī)制要跟具體的對(duì)象耦合上了。并且對(duì)引用計(jì)數(shù)的修改還需要另一套同步機(jī)制來(lái)提供保障。
為解決舊對(duì)象釋放的問(wèn)題,RCU提供了四個(gè)函數(shù)(另外還有一些它們的變形):
rcu_read_lock(void)
、rcu_read_unlock(void)
synchronize_rcu(void)
、call_rcu(struct rcu_head *head, void (*func)(struct rcu_head*head))。
當(dāng)讀操作要調(diào)用rcu_dereference訪問(wèn)對(duì)象之前,需要先調(diào)用rcu_read_lock;當(dāng)不再需要訪問(wèn)對(duì)象時(shí),調(diào)用rcu_read_unlock。
當(dāng)寫(xiě)操作調(diào)用rcu_assign_pointer完成對(duì)對(duì)象的更新之后,需要調(diào)用synchronize_rcu或call_rcu。其中synchronize_rcu會(huì)阻塞等待在此之前所有調(diào)用了rcu_read_lock的讀操作都已經(jīng)調(diào)用rcu_read_unlock,synchronize_rcu返回后寫(xiě)操作一方就可以將被它替換掉的舊對(duì)象釋放了;而call_rcu則是通過(guò)注冊(cè)回調(diào)函數(shù)的方式,由回調(diào)函數(shù)來(lái)釋放舊對(duì)象,寫(xiě)操作一方將不需要阻塞等待。同樣,等到在此之前所有調(diào)用了rcu_read_lock的讀操作都調(diào)用rcu_read_unlock之后,回調(diào)函數(shù)將被調(diào)用。

如果你足夠細(xì)心,可能已經(jīng)注意到了這樣一個(gè)問(wèn)題。synchronize_rcu和call_rcu會(huì)等待的是“在此之前所有調(diào)用了rcu_read_lock的讀操作都已經(jīng)調(diào)用了rcu_read_unlock”,然而在rcu_assign_pointer與synchronize_rcu或call_rcu之間,可能也有讀操作發(fā)生(調(diào)用了rcu_read_lock),它們引用到的是寫(xiě)操作rcu_assign_pointer后的新對(duì)象。按理說(shuō)寫(xiě)操作一方想要釋放舊對(duì)象時(shí),是不需要等待這樣的讀操作的。但是由于這些讀操作發(fā)生在synchronize_rcu或call_rcu之前,按照RCU的機(jī)制,還真得等它們都rcu_read_unlock。這豈不是多等了一些時(shí)日?
實(shí)際情況的確是這樣,甚至可能更糟。因?yàn)槟壳發(fā)inux內(nèi)核里面的RCU是一個(gè)全局的實(shí)現(xiàn),注意,rcu_read_lock、synchronize_rcu、等等操作都是不帶參數(shù)的。它不像seqlock或其他同步機(jī)制那樣,一把鎖保護(hù)一個(gè)臨界區(qū)。這個(gè)全局的RCU將保護(hù)使用RCU機(jī)制的所有臨界區(qū)。所以,對(duì)于寫(xiě)操作一方來(lái)說(shuō),在它調(diào)用synchronize_rcu或call_rcu之前發(fā)生的所有讀操作它都得等待(不管讀的對(duì)象與該寫(xiě)操作有無(wú)關(guān)系),直到這些讀操作都rcu_read_unlock之后,舊的對(duì)象才能被釋放。所以,寫(xiě)操作更新對(duì)象之后,舊對(duì)象并不是精確地在它能夠被釋放之時(shí)立刻被釋放的,可能會(huì)存在一定的延遲。
不過(guò)話說(shuō)回來(lái),這樣的實(shí)現(xiàn)減少了很多不必要的麻煩,因?yàn)榕f的對(duì)象晚一些釋放是不會(huì)有太大關(guān)系的。想一想,精確舊對(duì)象的釋放時(shí)機(jī)有多大意義呢?無(wú)非是盡可能早的回收一些內(nèi)存(一般來(lái)說(shuō),內(nèi)核里面使用的這些對(duì)象并不會(huì)太大吧,晚一點(diǎn)回收也不會(huì)晚得太過(guò)分吧)。但是為此你得花費(fèi)很大的代價(jià)去跟蹤每一個(gè)對(duì)象的引用情況,這是不是有些得不償失呢?

最后,RCU要求,讀操作在rcu_read_lock與rcu_read_unlock之間是不能睡眠的(WHY?),call_rcu提供的回調(diào)函數(shù)也不能睡眠(因?yàn)榛卣{(diào)函數(shù)一般會(huì)在軟中斷里面去調(diào)用,中斷上下文是不能睡眠的,見(jiàn)《linux中斷處理淺析》)。

那么,RCU具體是怎么實(shí)現(xiàn)的呢?盡管沒(méi)有要求在精確的時(shí)間回收舊對(duì)象,RCU的實(shí)現(xiàn)還是很復(fù)雜的。以下簡(jiǎn)單討論一下rcu_read_lock、rcu_read_unlock、call_rcu三個(gè)函數(shù)的實(shí)現(xiàn)。而synchronize_rcu實(shí)際上是利用call_rcu來(lái)實(shí)現(xiàn)的(調(diào)用call_rcu提交一個(gè)回調(diào)函數(shù),然后自己進(jìn)入睡眠,而回調(diào)函數(shù)要做的事情就是把自己?jiǎn)拘眩?br> 在linux2.6.30版本中,RCU有三種實(shí)現(xiàn),分別命名為rcuclassic、rcupreempt、rcutree。這三種實(shí)現(xiàn)也是逐步發(fā)展出來(lái)的,最開(kāi)始是rcuclassic,然后rcupreempt,最后rcutree。在編譯內(nèi)核時(shí)可以通過(guò)編譯選項(xiàng)選擇需要使用的RCU實(shí)現(xiàn)。

rcuclassic
rcuclassic
的實(shí)現(xiàn)思路是,讀操作在rcu_read_lock時(shí)禁止內(nèi)核搶占、在rcu_read_unlock時(shí)重新啟用內(nèi)核搶占。由于RCU只會(huì)在內(nèi)核態(tài)里面使用,而且RCU也要求rcu_read_lock與rcu_read_unlock之間不能睡眠。所以在rcu_read_lock之后,這個(gè)讀操作的相關(guān)代碼肯定會(huì)在當(dāng)前CPU上持續(xù)被執(zhí)行,直到rcu_read_unlock之后才可能被調(diào)度。而同一時(shí)間,在一個(gè)CPU上,也最多只能有一個(gè)正在進(jìn)行的讀操作。可以說(shuō),rcuclassic是基于CPU來(lái)跟蹤讀操作的。
于是,如果發(fā)現(xiàn)一個(gè)CPU已經(jīng)發(fā)生了調(diào)度,就說(shuō)明這個(gè)CPU上的讀操作肯定已經(jīng)rcu_read_unlock了(注意這里又是一次延遲,rcu_read_unlock之后可能還要過(guò)一段時(shí)間才會(huì)發(fā)生調(diào)度。RCU的實(shí)現(xiàn)中,這樣的延遲隨處可見(jiàn),因?yàn)樗揪筒灰笤诰_的時(shí)間點(diǎn)回收舊對(duì)象)。于是,從一次call_rcu被調(diào)用之后開(kāi)始,如果等到所有CPU都已經(jīng)發(fā)生了調(diào)度,這次call_rcu需要等待的讀操作就必定都已經(jīng)rcu_read_unlock了,這時(shí)候就可以處理這個(gè)call_rcu提交的回調(diào)函數(shù)了。
但是實(shí)現(xiàn)上,rcuclassic并不是為每一次call_rcu都提供一個(gè)這樣的等待周期(等待所有CPU都已發(fā)生調(diào)度),那樣的話粒度太細(xì),實(shí)現(xiàn)起來(lái)會(huì)比較復(fù)雜。rcuclassic將現(xiàn)有的全部call_rcu提交的回調(diào)函數(shù)分為兩個(gè)批次(batch),以批次為單位來(lái)進(jìn)行等待。如果所有CPU都已發(fā)生調(diào)度,則第一批次的所有回調(diào)函數(shù)將被調(diào)用,然后將第一批次清空、第二批變?yōu)榈谝慌?,并繼續(xù)下一次的等待。而所有新來(lái)的call_rcu總是將回調(diào)函數(shù)提交到第二批。
rcuclassic
邏輯上通過(guò)三個(gè)鏈表來(lái)管理call_rcu提交的回調(diào)函數(shù),分別是第二批次鏈表、第一批次鏈表、待處理鏈表(2.6.30版本的實(shí)現(xiàn)實(shí)際用了四個(gè)鏈表,把待處理鏈表分解成兩個(gè)鏈表)。call_rcu總是將回調(diào)函數(shù)提交到第二批次鏈表中,如果發(fā)現(xiàn)第一批次鏈表為空(之前的call_rcu都已經(jīng)處理完了),就將第二批次鏈表中的回調(diào)函數(shù)都移入第一批次鏈表(第二批次鏈表清空);從回調(diào)函數(shù)被移入第一批次鏈表開(kāi)始,如果所有CPU都發(fā)生了調(diào)度,則將第一批次鏈表中的回調(diào)函數(shù)都移入待處理鏈表(第一批次鏈表清空,同時(shí)第二批次鏈表中新的回調(diào)函數(shù)又被移過(guò)來(lái));待處理鏈表里面的回調(diào)函數(shù)都是等待被調(diào)用的,下一次進(jìn)入軟中斷的時(shí)候就要調(diào)用它們。
什么時(shí)候檢查“所有CPU都已發(fā)生調(diào)度”呢?并不是在CPU發(fā)生調(diào)度的時(shí)候。調(diào)度的時(shí)候只是做一個(gè)標(biāo)記,標(biāo)記這個(gè)CPU已經(jīng)調(diào)度過(guò)了。而檢查是放在每毫秒一次的時(shí)鐘中斷處理函數(shù)里面來(lái)進(jìn)行的。
另外,這里提到的第二批次鏈表、第一批次鏈表、待處理鏈表其實(shí)是每個(gè)CPU維護(hù)一份的,這樣可以避免操作鏈表時(shí)CPU之間的競(jìng)爭(zhēng)。
rcuclassic
的實(shí)現(xiàn)利用了禁止內(nèi)核搶占,這對(duì)于一些實(shí)時(shí)性要求高的環(huán)境是不適用的(實(shí)時(shí)性要求不高則無(wú)妨),所以后來(lái)又有了rcupreempt的實(shí)現(xiàn)。

rcupreempt
rcupreempt
是相對(duì)于rcuclassic禁止內(nèi)核搶占而言的,rcupreempt允許內(nèi)核搶占,以滿足更高的實(shí)時(shí)性要求。
rcupreempt
的實(shí)現(xiàn)思路是,通過(guò)計(jì)數(shù)器來(lái)記錄rcu_read_lock與rcu_read_unlock發(fā)生的次數(shù)。讀操作在rcu_read_lock時(shí)給計(jì)數(shù)器加1,rcu_read_unlock時(shí)則減1。只要計(jì)數(shù)器的值為0,說(shuō)明所有的讀操作都rcu_read_unlock了,則在此之前所有call_rcu提交的回調(diào)函數(shù)都可以被執(zhí)行。不過(guò),這樣的話,新來(lái)的rcu_read_lock會(huì)使得之前的call_rcu不斷延遲(如果rcu_read_unlock總是跟不上rcu_read_lock的速度,那么計(jì)數(shù)器可能永遠(yuǎn)都無(wú)法減為0。但是對(duì)于之前的某個(gè)call_rcu來(lái)說(shuō),它所關(guān)心的讀操作卻可能都已經(jīng)rcu_read_unlock了)。所以,rcupreempt還是像rcuclassic那樣,將call_rcu提交的回調(diào)函數(shù)分為兩個(gè)批次,然后由兩個(gè)計(jì)數(shù)器分別計(jì)數(shù)。
跟rcuclassic一樣,call_rcu提交的回調(diào)函數(shù)總是加入到第二批次,所以rcu_read_lock總是增加第二批次的計(jì)數(shù)。而當(dāng)?shù)谝慌螢榭諘r(shí),第二批次將移動(dòng)到第一批次,計(jì)數(shù)值也應(yīng)該一起移過(guò)來(lái)。所以,rcu_read_unlock必須知道它應(yīng)該減少哪個(gè)批次的計(jì)數(shù)(rcu_read_lock增加第二批次的計(jì)數(shù),之后第一批次可能被處理,然后第二批次被移動(dòng)到第一批次。這種情況下對(duì)應(yīng)的rcu_read_unlock應(yīng)該減少的是第一批次的計(jì)數(shù)了)。
實(shí)現(xiàn)上,rcupreempt提供了兩個(gè)[等待隊(duì)列+計(jì)數(shù)器],并且交替的選擇其中的一個(gè)作為“第一批次”。之前說(shuō)的將第二批次移動(dòng)到第一批次的過(guò)程實(shí)際上就是批次交替一次的過(guò)程,批次并沒(méi)移動(dòng),只是兩個(gè)[等待隊(duì)列+計(jì)數(shù)器]的含義發(fā)生了交換。于是,rcu_read_lock的時(shí)候需要記錄下現(xiàn)在增加的是第幾個(gè)計(jì)數(shù)器的計(jì)數(shù),rcu_read_unlock就相應(yīng)減少那個(gè)計(jì)數(shù)就行了。
那么rcu_read_lock與rcu_read_unlock怎么對(duì)應(yīng)上呢?rcupreempt已經(jīng)不禁止內(nèi)核搶占了,同一個(gè)讀操作里面的rcu_read_lock和rcu_read_unlock可能發(fā)生在不同CPU上,不能通過(guò)CPU來(lái)聯(lián)系rcu_read_lock與rcu_read_unlock,只能通過(guò)上下文,也就是執(zhí)行rcu_read_lock與rcu_read_unlock的進(jìn)程。所以,在進(jìn)程控制塊(task_struct)中新增了一個(gè)index字段,用來(lái)記錄這個(gè)進(jìn)程上執(zhí)行的rcu_read_lock增加了哪個(gè)計(jì)數(shù)器的計(jì)數(shù),于是這個(gè)進(jìn)程上執(zhí)行的rcu_read_unlock也應(yīng)該減少相應(yīng)的計(jì)數(shù)。
rcupreempt
也維護(hù)了一個(gè)待處理鏈表。于是,當(dāng)?shù)谝慌蔚挠?jì)數(shù)為0時(shí),第一批次里面的回調(diào)函數(shù)將被移動(dòng)到待處理鏈表中,等到下一次進(jìn)入軟中斷的時(shí)候就調(diào)用它們。然后第一批次被清空,兩個(gè)批次做交換(相當(dāng)于第二批次移動(dòng)到第一批次)。
跟rcuclassic類似,對(duì)于計(jì)數(shù)值的檢查并不是在rcu_read_unlock的時(shí)候進(jìn)行的,rcu_read_unlock只管修改計(jì)數(shù)值。而檢查也是放在每毫秒一次的時(shí)鐘中斷處理函數(shù)里面來(lái)進(jìn)行的。
同樣,這里提到的等待隊(duì)列和計(jì)數(shù)器也是每個(gè)CPU維護(hù)一份的,以避免操作鏈表和計(jì)數(shù)器時(shí)CPU之間的競(jìng)爭(zhēng)。那么當(dāng)然,要檢查第一批次計(jì)數(shù)為0,是需要把所有CPU的第一批次計(jì)數(shù)值進(jìn)行相加的。

rcutree
最后說(shuō)說(shuō)rcutree。它跟rcuclassic的實(shí)現(xiàn)思路幾乎是一模一樣的,通過(guò)禁止搶占、檢查每一個(gè)CPU是否已經(jīng)發(fā)生過(guò)調(diào)度,來(lái)判斷發(fā)生在某一批次rcu_call之前的所有讀操作是否都已經(jīng)rcu_read_unlock。并且實(shí)現(xiàn)上,批次的管理、各種隊(duì)列、等等都幾乎一樣,CPU發(fā)生調(diào)度時(shí)也是通過(guò)設(shè)置一個(gè)標(biāo)記來(lái)表示自己已經(jīng)調(diào)度過(guò)了,然后又在時(shí)鐘中斷的處理程序中判斷是否所有CPU都已經(jīng)發(fā)生過(guò)調(diào)度……那么,不同之處在哪里呢?在于“判斷是否每一個(gè)CPU都調(diào)度過(guò)”這一細(xì)節(jié)上。
rcuclassic
對(duì)于多個(gè)CPU的管理是對(duì)稱的,在時(shí)鐘中斷處理函數(shù)中,要判斷是否每一個(gè)CPU都調(diào)度過(guò)就得去看每一個(gè)CPU所設(shè)置的標(biāo)記,而這個(gè)“看”的過(guò)程勢(shì)必是需要互斥的(因?yàn)檫@些標(biāo)記也會(huì)被其他CPU讀或?qū)懀?。這樣就造成了CPU之間的競(jìng)爭(zhēng)。如果CPU個(gè)數(shù)不多,就這么競(jìng)爭(zhēng)一下倒也無(wú)妨。要是CPU很多的話(比如64個(gè)?或更多?),那當(dāng)然越少競(jìng)爭(zhēng)越好。rcutree就是為了這種擁有很多CPU的環(huán)境而設(shè)計(jì)的,以期減少競(jìng)爭(zhēng)。
rcutree
的思路是提供一個(gè)樹(shù)型結(jié)構(gòu),其中的每一個(gè)非葉子節(jié)點(diǎn)提供一個(gè)鎖(代表了一次競(jìng)爭(zhēng)),而每個(gè)CPU就對(duì)應(yīng)到樹(shù)的葉子節(jié)點(diǎn)上。然后呢?當(dāng)需要判斷“是否每一個(gè)CPU都調(diào)度過(guò)”的時(shí)候,CPU嘗試在自己的父節(jié)點(diǎn)上鎖(這個(gè)鎖只會(huì)由它的子節(jié)點(diǎn)來(lái)競(jìng)爭(zhēng),而不會(huì)被所有CPU競(jìng)爭(zhēng)),然后判斷這個(gè)“父節(jié)點(diǎn)”的子節(jié)點(diǎn)(CPU)是否都已經(jīng)調(diào)度過(guò)。如果不是,則顯然“每一個(gè)CPU都調(diào)度過(guò)”不成立。而如果是,則再向上遍歷,直到走到樹(shù)根,那么就可以知道所有CPU都已經(jīng)調(diào)度過(guò)了。使用這樣的樹(shù)型結(jié)構(gòu)就縮小了每一次加鎖的粒度,減少了CPU間的競(jìng)爭(zhēng)。


BKL(大內(nèi)核鎖)



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

1.1 應(yīng)運(yùn)而生,特立獨(dú)行

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

大內(nèi)核鎖的產(chǎn)生是有其歷史原因的。早期linux版本對(duì)對(duì)稱多處理(SMP)器的支持非常有限,為了保證可靠性,對(duì)處理器之間的互斥采取了‘寧可錯(cuò)殺三千,不可放過(guò)一個(gè)’的方式:在內(nèi)核入口處安裝一把‘巨大’的鎖,一旦一個(gè)處理器進(jìn)入內(nèi)核態(tài)就立刻上鎖,其它將要進(jìn)入內(nèi)核態(tài)的進(jìn)程只能在門口等待,以此保證每次只有一個(gè)進(jìn)程處于內(nèi)核態(tài)運(yùn)行。這把鎖就是大內(nèi)核鎖。有了大內(nèi)核鎖保護(hù)的系統(tǒng)當(dāng)然可以安全地運(yùn)行在多處理器上:由于同時(shí)只有一個(gè)處理器在運(yùn)行內(nèi)核代碼,內(nèi)核的執(zhí)行本質(zhì)上和單處理器沒(méi)有什么區(qū)別;而多個(gè)處理器同時(shí)運(yùn)行于進(jìn)程的用戶態(tài)也是安全的,因?yàn)槊總€(gè)進(jìn)程有自己獨(dú)立的地址空間。但是這樣粗魯?shù)丶渔i其缺點(diǎn)也是顯而易見(jiàn)的:多處理器對(duì)性能的提示只能體現(xiàn)在用戶態(tài)的并行處理上,而在內(nèi)核態(tài)下還是單線執(zhí)行,完全無(wú)法發(fā)揮多處理器的威力。于是內(nèi)核開(kāi)發(fā)者就開(kāi)始想辦法逐步縮小這把鎖保護(hù)的范圍。實(shí)際上內(nèi)核大部分代碼是多處理器安全的,只有少數(shù)全局資源需要需要在做互斥加以保護(hù),所以沒(méi)必要限制同時(shí)運(yùn)行于內(nèi)核態(tài)處理器的個(gè)數(shù)。所有處理器都可隨時(shí)進(jìn)入內(nèi)核態(tài)運(yùn)行,只要把這些需要保護(hù)的資源一一挑出來(lái),限制同時(shí)訪問(wèn)這些資源的處理器個(gè)數(shù)就可以了。這樣一來(lái),大內(nèi)核鎖從保護(hù)整個(gè)內(nèi)核態(tài)縮小為零散地保護(hù)內(nèi)核態(tài)某些關(guān)鍵片段。這是一個(gè)進(jìn)步,可步伐還不夠大,仍有上面提到的,‘鎖了臥室廚房也沒(méi)法進(jìn)’的毛病。隨著自旋鎖的廣泛應(yīng)用,新的內(nèi)核代碼里已經(jīng)不再有人使用大內(nèi)核鎖了。

1.2 食之無(wú)味,揮之不去

既然已經(jīng)有了替代物,大內(nèi)核鎖應(yīng)該可以‘光榮下崗’了??墒聦?shí)上沒(méi)這么簡(jiǎn)單。如果大內(nèi)核鎖僅僅是‘只有一個(gè)實(shí)例’的自旋鎖,睿智的內(nèi)核開(kāi)發(fā)者早就把它替換掉了:為每一種處于自旋鎖保護(hù)下的資源創(chuàng)建一把自旋鎖,把大內(nèi)核鎖加鎖/解鎖替換成相應(yīng)的自旋鎖的加鎖/解鎖就可以了。但如今的大內(nèi)核鎖就象一個(gè)被寵壞的孩子,內(nèi)核在一些關(guān)鍵點(diǎn)給予了它許多額外關(guān)照,使得大內(nèi)核鎖的替換變得有點(diǎn)煩。下面是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.

這段話的大意是:最大的技術(shù)難點(diǎn)是大內(nèi)核鎖的與眾不同:它在調(diào)用schedule()時(shí)能夠‘自動(dòng)釋放’。這一點(diǎn)使得大內(nèi)核鎖非常麻煩和隱蔽:它使你能夠非常容易地添加一段代碼而幾乎從不知道它鎖上與否。PREEMPT_BKL選項(xiàng)使得它更加隱蔽,因?yàn)檫@導(dǎo)致它的效果在普通用戶面前更加‘遁形’。

翻譯linux開(kāi)發(fā)者的話比看懂他們寫(xiě)的代碼更難,但有一點(diǎn)很明白:是schedule()函數(shù)里對(duì)于大內(nèi)核鎖的自動(dòng)釋放導(dǎo)致了問(wèn)題的復(fù)雜化。那就看看schedule()里到底對(duì)大內(nèi)核鎖執(zhí)行了什么操作:

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

在第19行release_kernel_lock(prev)函數(shù)釋放當(dāng)前進(jìn)程(prev)所占據(jù)的大內(nèi)核鎖,接著在第55行執(zhí)行進(jìn)程的切換,從當(dāng)前進(jìn)程prev切換到了下一個(gè)進(jìn)程next。context_switch()可以看做一個(gè)超級(jí)函數(shù),調(diào)用它不是去執(zhí)行一段代碼,而是去執(zhí)行另一個(gè)進(jìn)程。系統(tǒng)的多任務(wù)切換就是依靠這個(gè)超級(jí)函數(shù)從一個(gè)進(jìn)程切換到另一個(gè)進(jìn)程,從另一個(gè)進(jìn)程再切換下一個(gè)進(jìn)程,如此連續(xù)不斷地輪轉(zhuǎn)。只要被切走的進(jìn)程還處于就緒狀態(tài),總有一天還會(huì)有機(jī)會(huì)調(diào)度回來(lái)繼續(xù)運(yùn)行,效果看起來(lái)就象函數(shù)context_switch()運(yùn)行完畢返回到了schedule()。繼續(xù)運(yùn)行到第67行,調(diào)用函數(shù)reacquire_kernel_lock()。這是和release_kernel_lock()配對(duì)的函數(shù),將前面釋放的大內(nèi)核鎖又重新鎖起來(lái)。If語(yǔ)句測(cè)試為真表示對(duì)大內(nèi)核鎖嘗試加鎖失敗,這時(shí)可以做一些優(yōu)化。正常的加鎖應(yīng)該是‘原地踏步’,在同一個(gè)地方反復(fù)查詢大內(nèi)核鎖的狀態(tài),直到其它進(jìn)程釋放為止。但這樣做會(huì)浪費(fèi)寶貴的處理器時(shí)間,尤其是當(dāng)運(yùn)行隊(duì)列里有進(jìn)程在等待運(yùn)行時(shí)。所以release_lernel_lock()只是做了’try_lock’的工作,即假如沒(méi)人把持大內(nèi)核鎖就把它鎖住,返回0表示成功;假如已經(jīng)被鎖住就立即返回-1表示失敗。一旦失敗就重新執(zhí)行一遍schedule()的主體部分,檢查運(yùn)行隊(duì)列,挑選一個(gè)合適的進(jìn)程運(yùn)行,等到下一次被調(diào)度運(yùn)行時(shí)可能鎖就解開(kāi)了。這樣做利用另一個(gè)進(jìn)程(假如有進(jìn)程在排隊(duì)等候)的運(yùn)行代替了原地死等,提高了處理器利用率。

除了在schedule()中的‘照顧’,大內(nèi)核鎖還有另外的優(yōu)待:在同一進(jìn)程中你可以對(duì)它反復(fù)嵌套加鎖解鎖,只要加鎖個(gè)數(shù)和解鎖個(gè)數(shù)能配上對(duì)就不會(huì)有任何問(wèn)題,這是自旋鎖望塵莫及的,同一進(jìn)程里自旋鎖如果發(fā)生嵌套加鎖就會(huì)死鎖。為此在進(jìn)程控制塊(PCB)中專門為大內(nèi)核鎖開(kāi)辟了加鎖計(jì)數(shù)器,即task_struct中的lock_depth域。該域的初始值為-1,表示進(jìn)程沒(méi)有獲得大內(nèi)核鎖。每次加鎖時(shí)lock_depth都會(huì)加1,再檢查如果lock_depth為0就執(zhí)行真正的加鎖操作,這樣保證在加了一次鎖以后所有嵌套的加鎖操作都會(huì)被忽略,從而避免了死鎖。解鎖過(guò)程正好相反,每次都將lock_depth減1,直到發(fā)現(xiàn)其值變?yōu)?1時(shí)就執(zhí)行真正的解鎖操作。

內(nèi)核對(duì)大內(nèi)核鎖的偏袒導(dǎo)致開(kāi)發(fā)者在鎖住了它,進(jìn)入被它保護(hù)的臨界區(qū)后,執(zhí)行了不該執(zhí)行的代碼卻還無(wú)法察覺(jué)。其一:程序在鎖住臨界區(qū)后必須盡快退出,否則會(huì)阻塞其它將要進(jìn)入臨界區(qū)的進(jìn)程。所以在臨界區(qū)里絕對(duì)不可以調(diào)用schedule()函數(shù),否則一旦發(fā)生進(jìn)程切換何時(shí)能解鎖就變得遙遙無(wú)期。另外在使用自旋鎖保護(hù)的臨界區(qū)中做進(jìn)程切換很容易造成死鎖。比如一個(gè)進(jìn)程鎖住了一把自旋鎖,期間調(diào)用schedule()切換到另一個(gè)進(jìn)程,而這個(gè)進(jìn)程又要獲得這把鎖,這是系統(tǒng)就會(huì)掛死在這個(gè)進(jìn)程等待解鎖的自旋處。這個(gè)問(wèn)題在大內(nèi)核鎖保護(hù)的臨界區(qū)是不存在的,因?yàn)閟chedule()函數(shù)在調(diào)度到新進(jìn)程之前會(huì)自動(dòng)解鎖已經(jīng)獲得的大內(nèi)核鎖;在切回該進(jìn)程時(shí)又會(huì)自動(dòng)將大內(nèi)核鎖鎖住。用戶在鎖住了大內(nèi)核鎖后,幾乎無(wú)法察覺(jué)期間是否用過(guò)schedule()函數(shù)。這一點(diǎn)就是上面IngoMolnar提到的’technicalcomplication’:將大內(nèi)核鎖替換成自旋鎖后,萬(wàn)一在加鎖過(guò)程中調(diào)用了schedule(),會(huì)造成不可預(yù)估的,災(zāi)難性的后果。當(dāng)然作為一個(gè)訓(xùn)練有素的程序員,即使大內(nèi)核鎖放寬了約束條件,也不會(huì)在臨界區(qū)中有意識(shí)地調(diào)用schedule()函數(shù)的??墒侨绻钦{(diào)用陌生模塊的代碼,再高超的程序員也無(wú)法保證其中不會(huì)調(diào)用到該函數(shù)。其二就是上面提到的,在臨界區(qū)中不能再次獲得保護(hù)該臨界區(qū)的鎖,否則會(huì)死鎖??墒怯捎诖髢?nèi)核鎖有加鎖計(jì)數(shù)器的保護(hù),怎樣嵌套也不會(huì)有事。這也是一個(gè)’technicalcomplication’:將大內(nèi)核鎖替換成自旋鎖后,萬(wàn)一發(fā)生了同一把自旋鎖的嵌套加鎖后果也是災(zāi)難性的。同schedule()函數(shù)一樣,訓(xùn)練有素的程序員是不會(huì)有意識(shí)地多次鎖住大內(nèi)核鎖,但在獲得自旋鎖后調(diào)用了陌生模塊的代碼就無(wú)法保證這些模塊中不會(huì)再次使用大內(nèi)核鎖。這種情況在開(kāi)發(fā)大型系統(tǒng)時(shí)非常常見(jiàn):每個(gè)人都很小心地避免自己模塊的死鎖,可誰(shuí)也無(wú)法避免當(dāng)調(diào)用其它模塊時(shí)可能引入的死鎖問(wèn)題。

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

· 同一個(gè)進(jìn)程遞歸地加鎖同一把鎖;

· 一把鎖既在中斷(或中斷下半部)使能的情況下執(zhí)行過(guò)加鎖操作,又在中斷(或中斷下半部)里執(zhí)行過(guò)加鎖操作。這樣該鎖有可能在鎖定時(shí)由于中斷發(fā)生又試圖在同一處理器上加鎖;

· 加鎖后導(dǎo)致依賴圖產(chǎn)生成閉環(huán),這是典型的死鎖現(xiàn)象。

由于大內(nèi)核鎖游離于lockdep之外,它自身以及和其它互斥機(jī)制之間的依賴關(guān)系沒(méi)有受到監(jiān)控,可能會(huì)導(dǎo)致死鎖的場(chǎng)景也無(wú)法被記錄下來(lái),使得它的使用越來(lái)越混亂,處于失控狀態(tài)。

如此看來(lái),大內(nèi)核鎖已經(jīng)成了內(nèi)核的雞肋,而且不能與時(shí)俱進(jìn),到了非整改不可的地步??墒菍⒋髢?nèi)核鎖完全從內(nèi)核中移除將要面臨重重挑戰(zhàn),對(duì)于那些散落在‘年久失修’,多年無(wú)人問(wèn)津的代碼里的大內(nèi)核鎖,更是沒(méi)人敢去動(dòng)它們。既然完全移除希望不大,那就想辦法優(yōu)化它也不失為一種權(quán)宜之計(jì)。

1.3 一改再改:無(wú)奈的選擇

早些時(shí)候大內(nèi)核鎖是在自旋鎖的基礎(chǔ)上實(shí)現(xiàn)的。自旋鎖是處理器之間臨界區(qū)互斥常用的機(jī)制。當(dāng)臨界區(qū)非常短暫,比如只改變幾個(gè)變量的值時(shí),自旋鎖是一種簡(jiǎn)單高效的互斥手段。但自旋鎖的缺點(diǎn)是會(huì)增大系統(tǒng)負(fù)荷,因?yàn)樵谧孕却^(guò)程中進(jìn)程依舊占據(jù)處理器,這部分等待時(shí)間是在做無(wú)用功。尤其是使用大內(nèi)核鎖時(shí),一把鎖管所有臨界區(qū),發(fā)生‘碰撞’的機(jī)會(huì)就更大了。另外為了使進(jìn)程能夠盡快全速‘沖’出臨界區(qū),自旋鎖在加鎖的同時(shí)關(guān)閉了內(nèi)核搶占式調(diào)度。因此鎖住自旋鎖就意味著在一個(gè)處理器上制造了一個(gè)調(diào)度‘禁區(qū)’:期間既不被其它進(jìn)程搶占,又不允許調(diào)用schedule()進(jìn)行自主進(jìn)程切換。也就是說(shuō),一旦處理器上某個(gè)進(jìn)程獲得了自旋鎖,該處理器就只能一直運(yùn)行該進(jìn)程,即便有高優(yōu)先級(jí)的實(shí)時(shí)進(jìn)程就緒也只能排隊(duì)等候。調(diào)度禁區(qū)的出現(xiàn)增加了調(diào)度延時(shí),降低了系統(tǒng)實(shí)時(shí)反應(yīng)的速度,這與大家一直努力從事的內(nèi)核實(shí)時(shí)化改造是背道而馳的。于是在2.6.7版本的linux中對(duì)自旋鎖做了徹底改造,放棄了自旋鎖改用信號(hào)量。信號(hào)量沒(méi)有上面提到的兩個(gè)問(wèn)題:在等待信號(hào)量空閑時(shí)進(jìn)程不占用處理器,處于阻塞狀態(tài);在獲得信號(hào)量后內(nèi)核搶占依舊是使能的,不會(huì)出現(xiàn)調(diào)度盲區(qū)。這樣的解決方案應(yīng)該毫無(wú)爭(zhēng)議了。可任何事情都是有利有弊的。信號(hào)量最大的缺陷是太復(fù)雜了,每次阻塞一個(gè)進(jìn)程時(shí)都要產(chǎn)生費(fèi)時(shí)的進(jìn)程上下文切換,信號(hào)量就緒喚醒等待的進(jìn)程時(shí)又有一次上下文切換。除了上下文切換耗時(shí),進(jìn)程切換造成的TLB刷新,cache冷卻等都有較大開(kāi)銷。如果阻塞時(shí)間比較長(zhǎng),達(dá)到毫秒級(jí),這樣的切換是值得的。但是大部分情況下只需在臨界區(qū)入口等候幾十上百個(gè)指令循環(huán)另一個(gè)進(jìn)程就可以交出臨界區(qū),這時(shí)候這種切換就有點(diǎn)牛刀殺雞了。這就好象去醫(yī)院看普通門診,當(dāng)醫(yī)生正在為病人看病時(shí),別的病人在門口等待一會(huì)就會(huì)輪到了,不必留下電話號(hào)碼回家睡覺(jué),直到醫(yī)生空閑了打電話通知再匆匆趕往醫(yī)院。

由于使用信號(hào)量引起的進(jìn)程頻繁切換導(dǎo)致大內(nèi)核鎖在某些情況下出現(xiàn)嚴(yán)重性能問(wèn)題,LinusTorvalds不得不考慮將大內(nèi)核鎖的實(shí)現(xiàn)改回自旋鎖,自然調(diào)度延時(shí)問(wèn)題也會(huì)跟著回來(lái)。這使得以‘延時(shí)迷(latencyjunkie)’自居的IngoMolnar不太高興。但linux還是LinusTorvalds說(shuō)了算,于是在2.6.26-rc2版大內(nèi)核鎖又變成了自旋鎖,直到現(xiàn)在??偟膩?lái)說(shuō)LinusTorvalds的改動(dòng)是有道理的。使用繁瑣,重量級(jí)的信號(hào)量保護(hù)短暫的臨界區(qū)確實(shí)不值得;而且Linux也不是以實(shí)時(shí)性見(jiàn)長(zhǎng)的操作系統(tǒng),不應(yīng)該片面追求實(shí)時(shí)信而犧牲了整體性能。

1.4 日薄西山:謝幕在即

改回自旋鎖并不意味著LinusTorvalds不關(guān)心調(diào)度延時(shí),相反他真正的觀點(diǎn)是有朝一日徹底鏟除大內(nèi)核鎖,這一點(diǎn)他和IngoMolnar是英雄所見(jiàn)略同??墒怯捎阽P除大內(nèi)核鎖的難度和風(fēng)險(xiǎn)巨大,IngoMolnar覺(jué)得‘在當(dāng)前的游戲規(guī)則下解決大內(nèi)核鎖是不現(xiàn)實(shí)的’必須使用新的游戲規(guī)則。他專門建立一個(gè)版本分支叫做kill-the-BLK,在這個(gè)分支上將大內(nèi)核鎖替換為新的互斥機(jī)制,一步一步解決這個(gè)問(wèn)題:

· 解決所有已知的,利用到了大內(nèi)核鎖自動(dòng)解鎖機(jī)制的臨界區(qū);也就是說(shuō),消除使用大內(nèi)核鎖的代碼對(duì)自動(dòng)解鎖機(jī)制的依賴,使其更加接近普通的互斥機(jī)制;

· 添加許多調(diào)試設(shè)施用來(lái)警告那些在新互斥機(jī)制下不再有效的假設(shè);

· 將大內(nèi)核鎖轉(zhuǎn)換為普通的互斥體,并刪除遺留在調(diào)度器里的自動(dòng)解鎖代碼;

· 添加lockdep對(duì)它的監(jiān)控;

· 極大簡(jiǎn)化大內(nèi)核鎖代碼,最終將它從內(nèi)核里刪除。

這已經(jīng)是兩年前的事情了?,F(xiàn)在這項(xiàng)工作還沒(méi)結(jié)束,還在‘義無(wú)反顧’地向前推進(jìn)。期待著在不遠(yuǎn)的將來(lái)大內(nèi)核鎖這一不和諧的音符徹底淡出linux的內(nèi)核。



rwlock (讀寫(xiě)鎖)

讀寫(xiě)鎖實(shí)際是一種特殊的自旋鎖,它把對(duì)共享資源的訪問(wèn)者劃分成讀者和寫(xiě)者,讀者只對(duì)共享資源進(jìn)行讀訪問(wèn),寫(xiě)者則需要對(duì)共享資源進(jìn)行寫(xiě)操作。這種鎖相對(duì)于自旋鎖而言,能提高并發(fā)性,因?yàn)樵诙嗵幚砥飨到y(tǒng)中,它允許同時(shí)有多個(gè)讀者來(lái)訪問(wèn)共享資源,最大可能的讀者數(shù)為實(shí)際的邏輯CPU數(shù)。寫(xiě)者是排他性的,一個(gè)讀寫(xiě)鎖同時(shí)只能有一個(gè)寫(xiě)者或多個(gè)讀者(與CPU數(shù)相關(guān)),但不能同時(shí)既有讀者又有寫(xiě)者。

在讀寫(xiě)鎖保持期間也是搶占失效的。

如果讀寫(xiě)鎖當(dāng)前沒(méi)有讀者,也沒(méi)有寫(xiě)者,那么寫(xiě)者可以立刻獲得讀寫(xiě)鎖,否則它必須自旋在那里,直到?jīng)]有任何寫(xiě)者或讀者。如果讀寫(xiě)鎖沒(méi)有寫(xiě)者,那么讀者可以立即獲得該讀寫(xiě)鎖,否則讀者必須自旋在那里,直到寫(xiě)者釋放該讀寫(xiě)鎖。

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

rwlock_init(x)

該宏用于動(dòng)態(tài)初始化讀寫(xiě)鎖x

DEFINE_RWLOCK(x)

該宏聲明一個(gè)讀寫(xiě)鎖并對(duì)其進(jìn)行初始化。它用于靜態(tài)初始化。

RW_LOCK_UNLOCKED

它用于靜態(tài)初始化一個(gè)讀寫(xiě)鎖。

DEFINE_RWLOCK(x)等同于rwlock_tx = RW_LOCK_UNLOCKED

read_trylock(lock)

讀者用它來(lái)盡力獲得讀寫(xiě)鎖lock,如果能夠立即獲得讀寫(xiě)鎖,它就獲得鎖并返回真,否則不能獲得鎖,返回假。無(wú)論是否能夠獲得鎖,它都將立即返回,絕不自旋在那里。

write_trylock(lock)

寫(xiě)者用它來(lái)盡力獲得讀寫(xiě)鎖lock,如果能夠立即獲得讀寫(xiě)鎖,它就獲得鎖并返回真,否則不能獲得鎖,返回假。無(wú)論是否能夠獲得鎖,它都將立即返回,絕不自旋在那里。

read_lock(lock)

讀者要訪問(wèn)被讀寫(xiě)鎖lock保護(hù)的共享資源,需要使用該宏來(lái)得到讀寫(xiě)鎖lock。如果能夠立即獲得,它將立即獲得讀寫(xiě)鎖并返回,否則,將自旋在那里,直到獲得該讀寫(xiě)鎖。

write_lock(lock)

寫(xiě)者要想訪問(wèn)被讀寫(xiě)鎖lock保護(hù)的共享資源,需要使用該宏來(lái)得到讀寫(xiě)鎖lock。如果能夠立即獲得,它將立即獲得讀寫(xiě)鎖并返回,否則,將自旋在那里,直到獲得該讀寫(xiě)鎖。

read_lock_irqsave(lock, flags)

讀者也可以使用該宏來(lái)獲得讀寫(xiě)鎖,與read_lock不同的是,該宏還同時(shí)把標(biāo)志寄存器的值保存到了變量flags中,并失效了本地中斷。

write_lock_irqsave(lock, flags)

寫(xiě)者可以用它來(lái)獲得讀寫(xiě)鎖,與write_lock不同的是,該宏還同時(shí)把標(biāo)志寄存器的值保存到了變量flags中,并失效了本地中斷。

read_lock_irq(lock)

讀者也可以用它來(lái)獲得讀寫(xiě)鎖,與read_lock不同的是,該宏還同時(shí)失效了本地中斷。該宏與read_lock_irqsave的不同之處是,它沒(méi)有保存標(biāo)志寄存器。

write_lock_irq(lock)

寫(xiě)者也可以用它來(lái)獲得鎖,與write_lock不同的是,該宏還同時(shí)失效了本地中斷。該宏與write_lock_irqsave的不同之處是,它沒(méi)有保存標(biāo)志寄存器。

read_lock_bh(lock)

讀者也可以用它來(lái)獲得讀寫(xiě)鎖,與與read_lock不同的是,該宏還同時(shí)失效了本地的軟中斷。

write_lock_bh(lock)

寫(xiě)者也可以用它來(lái)獲得讀寫(xiě)鎖,與write_lock不同的是,該宏還同時(shí)失效了本地的軟中斷。

read_unlock(lock)

讀者使用該宏來(lái)釋放讀寫(xiě)鎖lock。它必須與read_lock配對(duì)使用。

write_unlock(lock)

寫(xiě)者使用該宏來(lái)釋放讀寫(xiě)鎖lock。它必須與write_lock配對(duì)使用。

read_unlock_irqrestore(lock, flags)

讀者也可以使用該宏來(lái)釋放讀寫(xiě)鎖,與read_unlock不同的是,該宏還同時(shí)把標(biāo)志寄存器的值恢復(fù)為變量flags的值。它必須與read_lock_irqsave配對(duì)使用。

write_unlock_irqrestore(lock, flags)

寫(xiě)者也可以使用該宏來(lái)釋放讀寫(xiě)鎖,與write_unlock不同的是,該宏還同時(shí)把標(biāo)志寄存器的值恢復(fù)為變量flags的值,并使能本地中斷。它必須與write_lock_irqsave配對(duì)使用。

read_unlock_irq(lock)

讀者也可以使用該宏來(lái)釋放讀寫(xiě)鎖,與read_unlock不同的是,該宏還同時(shí)使能本地中斷。它必須與read_lock_irq配對(duì)使用。

write_unlock_irq(lock)

寫(xiě)者也可以使用該宏來(lái)釋放讀寫(xiě)鎖,與write_unlock不同的是,該宏還同時(shí)使能本地中斷。它必須與write_lock_irq配對(duì)使用。

read_unlock_bh(lock)

讀者也可以使用該宏來(lái)釋放讀寫(xiě)鎖,與read_unlock不同的是,該宏還同時(shí)使能本地軟中斷。它必須與read_lock_bh配對(duì)使用。

write_unlock_bh(lock)

寫(xiě)者也可以使用該宏來(lái)釋放讀寫(xiě)鎖,與write_unlock不同的是,該宏還同時(shí)使能本地軟中斷。它必須與write_lock_bh配對(duì)使用。

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



From:http://blog.csdn.net/lucien_cc/article/details/7440225





    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多