在現(xiàn)代操作系統(tǒng)里,同一時間可能有多個內(nèi)核執(zhí)行流在執(zhí)行,因此內(nèi)核其實(shí)象多進(jìn)程多線程編程一樣也需要一些同步機(jī)制來同步各執(zhí)行單元對共享數(shù)據(jù)的訪問。尤其是在多處理器系統(tǒng)上,更需要一些同步機(jī)制來同步不同處理器上的執(zhí)行單元對共享的數(shù)據(jù)的訪問。
在主流的Linux內(nèi)核中包含了幾乎所有現(xiàn)代的操作系統(tǒng)具有的同步機(jī)制,這些同步機(jī)制包括:原子操作、信號量(semaphore)、讀寫信號量(rw_semaphore)、spinlock、BKL(Big Kernel Lock)、rwlock、brlock(只包含在2.4內(nèi)核中)、RCU(只包含在2.6內(nèi)核中)和seqlock(只包含在2.6內(nèi)核中)。
比較經(jīng)典的有原子操作、spin_lock(忙等待的鎖)、mutex(互斥鎖)、semaphore(信號量)等。并且它們幾乎都有對應(yīng)的rw_XXX(讀寫鎖),以便在能夠區(qū)分讀與寫的情況下,讓讀操作相互不互斥(讀寫、寫寫依然互斥)。而seqlock和rcu應(yīng)該可以不算在經(jīng)典之列,它們是兩種比較有意思的同步機(jī)制。
原子操作
原子操作就是指某一個操作在執(zhí)行過程中不可以被打斷,要么全部執(zhí)行,要不就一點(diǎn)也不執(zhí)行。原子操作需要硬件的支持,與體系結(jié)構(gòu)相關(guān),使用匯編語言實(shí)現(xiàn)。原子操作主要用于實(shí)現(xiàn)資源計數(shù),很多引用計數(shù)就是通過原子操作實(shí)現(xiàn)。Linux中提供了兩種原子操作接口,分別是原子整數(shù)操作和原子位操作。
原子整數(shù)操作只對atomic_t類型的數(shù)據(jù)進(jìn)行操作,不能對C語言的int進(jìn)行操作,使用atomic_t只能將其作為24位數(shù)據(jù)處理,主要是在SPARC體系結(jié)構(gòu)中int的低8為中設(shè)置了一個鎖,避免對原子類型數(shù)據(jù)的并發(fā)訪問。
原子位操作是針對由指針變量指定的任意一塊內(nèi)存區(qū)域的位序列的某一位進(jìn)行操作。它只是針對普通指針的操作,不需要定義一個與該操作相對應(yīng)的數(shù)據(jù)類型。
原子類型定義如下:
typedefstruct { volatile int counter; }atomic_t;
volatile修飾字段告訴gcc不要對該類型的數(shù)據(jù)做優(yōu)化處理,對它的訪問都是對內(nèi)存的訪問,而不是對寄存器的訪問。
原子操作API包括:
atomic_read(atomic_t* v);
該函數(shù)對原子類型的變量進(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ù)對原子類型變量v原子地增加1。
voidatomic_dec(atomic_t *v);
該函數(shù)對原子類型的變量v原子地減1。
intatomic_dec_and_test(atomic_t *v);
該函數(shù)對原子類型的變量v原子地減1,并判斷結(jié)果是否為0,如果為0,返回真,否則返回假。
intatomic_inc_and_test(atomic_t *v);
該函數(shù)對原子類型的變量v原子地增加1,并判斷結(jié)果是否為0,如果為0,返回真,否則返回假。
intatomic_add_negative(int i, atomic_t*v);
該函數(shù)對原子類型的變量v原子地增加I,并判斷結(jié)果是否為負(fù)數(shù),如果是,返回真,否則返回假。
intatomic_add_return(int i, atomic_t *v);
該函數(shù)對原子類型的變量v原子地增加i,并且返回指向v的指針。
intatomic_sub_return(int i, atomic_t *v);
該函數(shù)從原子類型的變量v中減去i,并且返回指向v的指針。
intatomic_inc_return(atomic_t * v);
該函數(shù)對原子類型的變量v原子地增加1并且返回指向v的指針。
intatomic_dec_return(atomic_t * v);
該函數(shù)對原子類型的變量v原子地減1并且返回指向v的指針。
原子操作通常用于實(shí)現(xiàn)資源的引用計數(shù),在TCP/IP協(xié)議棧的IP碎片處理中,就使用了引用計數(shù),碎片隊(duì)列結(jié)構(gòu)structipq描述了一個IP碎片,字段refcnt就是引用計數(shù)器,它的類型為atomic_t,當(dāng)創(chuàng)建IP碎片時(在函數(shù)ip_frag_create中),使用atomic_set函數(shù)把它設(shè)置為1,當(dāng)引用該IP碎片時,就使用函數(shù)atomic_inc把引用計數(shù)加1,當(dāng)不需要引用該IP碎片時,就使用函數(shù)ipq_put來釋放該IP碎片,ipq_put使用函數(shù)atomic_dec_and_test把引用計數(shù)減1并判斷引用計數(shù)是否為0,如果是就釋放Ip碎片。函數(shù)ipq_kill把IP碎片從ipq隊(duì)列中刪除,并把該刪除的IP碎片的引用計數(shù)減1(通過使用函數(shù)atomic_dec實(shí)現(xiàn))。
自旋鎖
Linux自旋鎖保證了任意時刻只能有一個執(zhí)行線程進(jìn)入臨界區(qū),其他試圖進(jìn)入臨界區(qū)的線程將一直進(jìn)行嘗試(即自旋),直到獲得該鎖。自旋鎖主要應(yīng)用在加鎖時間不長并且不會睡眠的情況。
自旋鎖的本質(zhì)是對內(nèi)存區(qū)域的一個整數(shù)的操作,任何線程進(jìn)入臨界區(qū)之前都必須檢查該整數(shù),可用則進(jìn)入,都則一直忙循環(huán)等待。
自旋鎖機(jī)制讓試圖獲得該鎖的線程一直進(jìn)行忙循環(huán)(占用CPU),因此自旋鎖適合于斷時間內(nèi)進(jìn)行輕量級加鎖。而且自旋鎖絕對不可以遞歸使用,否則會被自己鎖死。
Linux自旋鎖主要應(yīng)用與多核處理器中,單CPU中不會進(jìn)行自旋鎖操作。
linux上的自旋鎖有三種實(shí)現(xiàn):
a. 在單cpu,不可搶占內(nèi)核中,自旋鎖為空操作。
b. 在單cpu,可搶占內(nèi)核中,自旋鎖實(shí)現(xiàn)為“禁止內(nèi)核搶占”,并不實(shí)現(xiàn)“自旋”。
c. 在多cpu,可搶占內(nèi)核中,自旋鎖實(shí)現(xiàn)為“禁止內(nèi)核搶占” + “自旋”。其中,禁止內(nèi)核搶占只是關(guān)閉“可搶占標(biāo)志”,而不是禁止進(jìn)程切換。顯式使用schedule或進(jìn)程阻塞(此也會導(dǎo)致調(diào)用schedule)時,還是會發(fā)生進(jìn)程調(diào)度的。
使用自旋鎖需要注意有可能造成的死鎖情況:
static DEFINE_SPINLOCK(xxx_lock);
unsigned long flags;
spin_lock_irqsave(&xxx_lock, flags);
。。。 critical section here 。。
spin_unlock_irqrestore(&xxx_lock, flags);
代碼中spin_lock_irqsave會禁止本地cpu中斷的搶占。以上代碼在任何情況下都是安全的。但問題是關(guān)中斷的代價太大。
如果把spin_lock_irqsave/spin_unlock_irqrestore換成spin_lock/spin_unlock會有什么問題嗎?
答案是,如果中斷中調(diào)用了spin_lock,可能會引起死鎖!
例如:
spin_lock(&lock);
。。。
《- interrupt comes in:
spin_lock(&lock);
值得注意的是,如果產(chǎn)生中斷的cpu和進(jìn)程中調(diào)用spin_lock的cpu不是同一個,則不會有問題。這也是irq版本的spin_lock函數(shù)實(shí)現(xiàn)時只需要禁止本地cpu中斷的原因。
結(jié)論:要想在進(jìn)程中用spin_lock代替spin_lock_irqsave,條件是中斷中不會使用相應(yīng)的spin_lock
何時使用自旋鎖?
不允許睡眠的上下文且臨界區(qū)操作較短時使用自旋鎖。
?
? ? 讀/寫自旋鎖
Linux中規(guī)定,讀/寫自旋鎖允許多個線程同時以只讀的方式訪問臨界資源,只有當(dāng)一個線程想更新數(shù)據(jù)時,才會互斥訪問資源。
讀寫自旋鎖包括一個24位讀者計數(shù)和一個解鎖標(biāo)記來實(shí)現(xiàn)的。
注意:讀寫鎖需要比spinlocks更多的訪問原子內(nèi)存操作,如果讀臨界區(qū)不是很大,最好別使用讀寫鎖。
讀寫鎖代碼:
點(diǎn)擊(此處)折疊或打開rwlock_t xxx_lock = __RW_LOCK_UNLOCKED(xxx_lock);
unsigned long flags;
read_lock_irqsave(&xxx_lock, flags);
。。 critical section that only reads the info 。。。
read_unlock_irqrestore(&xxx_lock, flags);
write_lock_irqsave(&xxx_lock, flags);
。。 read and write exclusive access to the info 。。。
write_unlock_irqrestore(&xxx_lock, flags);
讀寫鎖比較適合鏈表等數(shù)據(jù)結(jié)構(gòu),特別是查找遠(yuǎn)多于修改的情況。
另外,可以靈活的使用read-write和irq版本的自旋鎖。例如,如果中斷中只是用了讀鎖,進(jìn)程中就可以使用non-irq版本的讀鎖和irq版本的寫鎖。
注意:RCU比讀寫鎖更適合遍歷list,但需要更關(guān)注細(xì)節(jié)。目前kernel社區(qū)正在努力用RCU代替讀寫鎖。
BKL(Big Kernel Lock)
BKL即全局內(nèi)核鎖,也稱大內(nèi)核鎖,它是一個全局自旋鎖。大內(nèi)核鎖也是用來保護(hù)臨界區(qū)資源的,避免出現(xiàn)多個處理器上的進(jìn)程同時訪問同一區(qū)域,整個內(nèi)核中只有一個大內(nèi)核鎖。
BKL是一個名為kernel_flag的自旋鎖,持有該鎖的進(jìn)程仍可以睡眠,當(dāng)睡眠時持有的鎖將被自動釋放,該進(jìn)程被喚醒時重新持有該鎖。Linux允許一個進(jìn)程可以遞歸的持有BKL,BKL是一個遞歸鎖。
它的設(shè)計思想是,一旦某個內(nèi)核路徑獲取了這把鎖,那么其他所有的內(nèi)核路徑都不能再獲取到這把鎖。自旋鎖加鎖的對象一般是一個全局變量,大內(nèi)核鎖加鎖的對象是一段代碼,里面可能包含多個全局變量。那么他帶來的問題是,雖然A只需要互斥訪問全局變量a,但附帶鎖了全局變量b,從而導(dǎo)致B不能訪問b了
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};
一、作用及訪問規(guī)則:
互斥鎖主要用于實(shí)現(xiàn)內(nèi)核中的互斥訪問功能。內(nèi)核互斥鎖是在原子API之上實(shí)現(xiàn)的,但這對于內(nèi)核用戶是不可見的。對它的訪問必須遵循一些規(guī)則:同一時間只能有一個任務(wù)持有互斥鎖,而且只有這個任務(wù)可以對互斥鎖進(jìn)行解鎖。互斥鎖不能進(jìn)行遞歸鎖定或解鎖。一個互斥鎖對象必須通過其API初始化,而不能使用memset或復(fù)制初始化。一個任務(wù)在持有互斥鎖的時候是不能結(jié)束的。互斥鎖所使用的內(nèi)存區(qū)域是不能被釋放的。使用中的互斥鎖是不能被重新初始化的。并且互斥鎖不能用于中斷上下文。但是互斥鎖比當(dāng)前的內(nèi)核信號量選項(xiàng)更快,并且更加緊湊,因此如果它們滿足您的需求,那么它們將是您明智的選擇。
二、各字段詳解:
1、atomic_t count; --指示互斥鎖的狀態(tài):1沒有上鎖,可以獲得;0被鎖定,不能獲得;負(fù)數(shù)被鎖定,且可能在該鎖上有等待進(jìn)程初始化為沒有上鎖。
2、spinlock_t wait_lock;--等待獲取互斥鎖中使用的自旋鎖。在獲取互斥鎖的過程中,操作會在自旋鎖的保護(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)具體參見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,判斷如果原來count的值為1,也即互斥鎖可以獲得,則直接獲取,跳出。否則進(jìn)入循環(huán)反復(fù)測試互斥鎖的狀態(tài)。在循環(huán)中,也是先取得互斥鎖原來的狀態(tài),再將其置為-1,判斷如果可以獲取(等于1),則退出循環(huán),否則設(shè)置當(dāng)前進(jìn)程的狀態(tài)為不可中斷狀態(tài),解鎖自身的自旋鎖,進(jìn)入睡眠狀態(tài),待被在調(diào)度喚醒時,再獲得自身的自旋鎖,進(jìn)入新一次的查詢其自身狀態(tài)(該互斥鎖的狀態(tài))的循環(huán)。
(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()一樣,也是獲取互斥鎖。在獲得了互斥鎖或進(jìn)入睡眠直到獲得互斥鎖之后會返回0。如果在等待獲取鎖的時候進(jìn)入睡眠狀態(tài)收到一個信號(被信號打斷睡眠),則返回_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);
}
釋放被當(dāng)前進(jìn)程獲取的互斥鎖。該函數(shù)不能用在中斷上下文中,而且不允許去釋放一個沒有上鎖的互斥鎖。
4.void mutex_destroy(struct mutex *lock) --清除互斥鎖,使互斥鎖不可用
用mutex_destroy()函數(shù)解除由lock指向的互斥鎖的任何狀態(tài)。在調(diào)用執(zhí)行這個函數(shù)的時候,lock指向的互斥鎖不能在被鎖狀態(tài)。儲存互斥鎖的內(nèi)存不被釋放。
返回值--mutex_destroy()在成功執(zhí)行后返回零。其他值意味著錯誤。在以下情況發(fā)生時,函數(shù)失敗并返回相關(guān)值。
EINVAL 非法參數(shù)
EFAULT mp指向一個非法地址。
5.static inline int mutex_is_locked(struct mutex *lock)--測試互斥鎖的狀態(tài)
這個調(diào)用實(shí)際上編譯成一個內(nèi)聯(lián)函數(shù)。如果互斥鎖被持有(鎖定),那么就會返回1;否則,返回0。
五、使用形式:
struct mutex mutex;
mutex_init(&mutex);
。。。
mutex_lock(&mutex);
。。。
mutex_unlock(&mutex);
順序鎖
順序鎖為寫者賦予更高的優(yōu)先級,寫者永遠(yuǎn)不會等待讀者。缺點(diǎn)是讀者有時不得不讀多次數(shù)據(jù)以獲取正確的結(jié)果。
順序鎖的數(shù)據(jù)結(jié)構(gòu)中除了有spinlock外,還有一個順序號。如果成功獲得鎖,順序鎖的順序號會加1,以便讀者能夠檢查出是否在讀期間有寫者訪問過。讀者在讀取數(shù)據(jù)前后兩次讀順序值,如果兩次值不相同,則說明讀取期間有新的寫者操作過數(shù)據(jù)了,那么本次讀取就是無效的。
典型使用:
讀端:
do {
seqnum = read_seqbegin(&seqlock_a);
//讀操作代碼塊
。。.
} while (read_seqretry(&seqlock_a, seqnum));
寫端:
spin_lock(&lock);
write_seqlock(&seqlock_a)
。。.
write_sequnlock(&seqlock_a)
spin_unlock(&lock);
寫者通過調(diào)用write_seqlock()和write_sequnlock()獲取和釋放順序鎖。write_seqlock()函數(shù)獲取seqlock_t數(shù)據(jù)結(jié)構(gòu)中的自旋鎖,然后使順序計數(shù)器sequence加1;write_sequnlock()函數(shù)再次增加順序計數(shù)器sequence,然后釋放自旋鎖。這樣可以保證寫者在整個寫的過程中,計數(shù)器sequence的值是奇數(shù),并且當(dāng)沒有寫者在改變數(shù)據(jù)的時候,計數(shù)器的值是偶數(shù)。
read_seqbegin()返回順序鎖的當(dāng)前順序號;如果局部變量seq的值是奇數(shù)(寫者在read_seqbegin()函數(shù)被調(diào)用后,正更新數(shù)據(jù)結(jié)構(gòu)),或seq的值與順序鎖的順序計數(shù)器的當(dāng)前值不匹配(當(dāng)讀者正執(zhí)行臨界區(qū)代碼時,寫者開始工作),read_seqretry()就返回1,說明本次讀取失敗,需要重新讀取 。
并不是每一種資源都可以使用順序鎖來保護(hù)。一般來說,必須在滿足下述條件時才能使用順序鎖:
1. 讀者的臨界區(qū)資源不包括被寫者修改和被讀者取值的指針,否則,寫者有可能使指針失效,讀者讀取時會產(chǎn)生OPPs。
2. 讀者的臨界區(qū)代碼沒有副作用。
何時使用順序鎖?
讀操作遠(yuǎn)多于寫操作、且寫操作很緊急時使用順序鎖。
評論