本系列文章分兩部分,第一部分詳細地介紹了 Linux 內核中的同步機制:原子操作、信號量、讀寫信號量和自旋鎖的API,使用要求以及一些典型示例。第二部分將詳細介紹在Linux內核中的另外一些同步機制,包括大內核鎖、讀寫鎖、大讀者鎖、RCU和順序鎖。
楊 燚 (
[email protected]),
計算機科學碩士
一、 引言
在現代操作系統裡,同一時間可能有多個內核執行流在執行,因此內核其實象多進程多線程編程一樣也需要一些同步機制來同步各執行單元對共享數據的訪問。尤其是在多處理器系統上,更需要一些同步機制來同步不同處理器上的執行單元對共享的數據的訪問。在主流的Linux內核中包含了幾乎所有現代的操作系統具有的同步機制,這些同步機制包括:原子操作、信號量(semaphore)、讀寫信號量(rw_semaphore)、spinlock、BKL(Big Kernel Lock)、rwlock、brlock(只包含在2.4內核中)、RCU(只包含在2.6內核中)和seqlock(只包含在2.6內核中)。
本文的下面各章節將詳細講述每一種同步機制的原理、用途、API以及典型應用示例。
二、原子操作
所謂原子操作,就是該操作絕不會在執行完畢前被任何其他任務或事件打斷,也就說,它的最小的執行單位,不可能有比它更小的執行單位,因此這裡的原子實際是使用了物理學裡的物質微粒的概念。
原子操作需要硬件的支持,因此是架構相關的,其API和原子類型的定義都定義在內核源碼樹的include/asm/atomic.h文件中,它們都使用匯編語言實現,因為C語言並不能實現這樣的操作。
原子操作主要用於實現資源計數,很多引用計數(refcnt)就是通過原子操作實現的。
原子類型定義如下:
typedef struct { volatile int counter; } atomic_t;
volatile修飾字段告訴gcc不要對該類型的數據做優化處理,對它的訪問都是對內存的訪問,而不是對寄存器的訪問。
原子操作API包括:
atomic_read(atomic_t * v);
該函數對原子類型的變量進行原子讀操作,它返回原子類型的變量v的值。
atomic_set(atomic_t * v, int i);
該函數設置原子類型的變量v的值為i。
void atomic_add(int i, atomic_t *v);
該函數給原子類型的變量v增加值i。
atomic_sub(int i, atomic_t *v);
該函數從原子類型的變量v中減去i。
int atomic_sub_and_test(int i, atomic_t *v);
該函數從原子類型的變量v中減去i,並判斷結果是否為0,如果為0,返回真,否則返回假。
void atomic_inc(atomic_t *v);
該函數對原子類型變量v原子地增加1。
void atomic_dec(atomic_t *v);
該函數對原子類型的變量v原子地減1。
int atomic_dec_and_test(atomic_t *v);
該函數對原子類型的變量v原子地減1,並判斷結果是否為0,如果為0,返回真,否則返回假。
int atomic_inc_and_test(atomic_t *v);
該函數對原子類型的變量v原子地增加1,並判斷結果是否為0,如果為0,返回真,否則返回假。
int atomic_add_negative(int i, atomic_t *v);
該函數對原子類型的變量v原子地增加I,並判斷結果是否為負數,如果是,返回真,否則返回假。
int atomic_add_return(int i, atomic_t *v);
該函數對原子類型的變量v原子地增加i,並且返回指向v的指針。
int atomic_sub_return(int i, atomic_t *v);
該函數從原子類型的變量v中減去i,並且返回指向v的指針。
int atomic_inc_return(atomic_t * v);
該函數對原子類型的變量v原子地增加1並且返回指向v的指針。
int atomic_dec_return(atomic_t * v);
該函數對原子類型的變量v原子地減1並且返回指向v的指針。
原子操作通常用於實現資源的引用計數,在TCP/IP協議棧的IP碎片處理中,就使用了引用計數,碎片隊列結構struct ipq描述了一個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_kill把IP碎片從ipq隊列中刪除,並把該刪除的IP碎片的引用計數減1(通過使用函數atomic_dec實現)。
回頁首
三、信號量(semaphore)
Linux內核的信號量在概念和原理上與用戶態的System V的IPC機制信號量是一樣的,但是它絕不可能在內核之外使用,因此它與System V的IPC機制信號量毫不相干。
信號量在創建時需要設置一個初始值,表示同時可以有幾個任務可以訪問該信號量保護的共享資源,初始值為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。這三個函數實際上是分別對函數down,up和down_trylock的簡單包裝。需要訪問console_drivers驅動列表時就需要使用acquire_console_sem來保護console_drivers列表,當訪問完該列表後,就調用release_console_sem釋放信號量console_sem。函數console_unblank,console_device,console_stop,console_start,register_console和unregister_console都需要訪問console_drivers,因此它們都使用函數對acquire_console_sem和release_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_read或down_read_trylock配對使用。如果down_read_trylock返回0,不需要調用up_read來釋放讀寫信號量,因為根本就沒有獲得信號量。
void up_write(struct rw_semaphore *sem);
寫者調用該函數釋放信號量sem。它與down_write或down_write_trylock配對使用。如果down_write_trylock返回0,不需要調用up_write,因為返回0表示沒有獲得該讀寫信號量。
void downgrade_write(struct rw_semaphore *sem);
該函數用於把寫者降級為讀者,這有時是必要的。因為寫者是排他性的,因此在寫者保持讀寫信號量期間,任何讀者或寫者都將無法訪問該讀寫信號量保護的共享資源,對於那些當前條件下不需要寫訪問的寫者,降級為讀者將,使得等待訪問的讀者能夠立刻訪問,從而增加了並發性,提高了效率。
讀寫信號量適於在讀多寫少的情況下使用,在linux內核中對進程的內存映像描述結構的訪問就使用了讀寫信號量進行保護。在Linux中,每一個進程都用一個類型為task_t或struct task_struct的結構來描述,該結構的類型為struct mm_struct的字段mm描述了進程的內存映像,特別是mm_struct結構的mmap字段維護了整個進程的內存塊列表,該列表將在進程生存期間被大量地遍利或修改,因此mm_struct結構就有一個字段mmap_sem來對mmap的訪問進行保護,mmap_sem就是一個讀寫信號量,在proc文件系統裡有很多進程內存使用情況的接口,通過它們能夠查看某一進程的內存使用情況,命令free、ps和top都是通過proc來得到內存使用信息的,proc接口就使用down_read和up_read來讀取進程的mmap信息。當進程動態地分配或釋放內存時,需要修改mmap來反映分配或釋放後的內存映像,因此動態內存分配或釋放操作需要以寫者身份獲得讀寫信號量mmap_sem來對mmap進行更新。系統調用brk和munmap就使用了down_write和up_write來保護對mmap的訪問。
五、自旋鎖(spinlock)
自旋鎖與互斥鎖有點類似,只是自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環在那裡看是否該自旋鎖的保持者已經釋放了鎖,"自旋"一詞就是因此而得名。由於自旋鎖使用者一般保持鎖時間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠高於互斥鎖。
信號量和讀寫信號量適合於保持時間較長的情況,它們會導致調用者睡眠,因此只能在進程上下文使用(_trylock的變種能夠在中斷上下文使用),而自旋鎖適合於保持時間非常短的情況,它可以在任何上下文使用。如果被保護的共享資源只在進程上下文訪問,使用信號量保護該共享資源非常合適,如果對共巷資源的訪問時間非常短,自旋鎖也可以。但是如果被保護的共享資源需要在中斷上下文訪問(包括底半部即中斷處理句柄和頂半部即軟中斷),就必須使用自旋鎖。
自旋鎖保持期間是搶占失效的,而信號量和讀寫信號量保持期間是可以被搶占的。自旋鎖只有在內核可搶占或SMP的情況下才真正需要,在單CPU且不可搶占的內核下,自旋鎖的所有操作都是空操作。
跟互斥鎖一樣,一個執行單元要想訪問被自旋鎖保護的共享資源,必須先得到鎖,在訪問完共享資源後,必須釋放鎖。如果在獲取自旋鎖時,沒有任何執行單元保持該鎖,那麼將立即得到鎖;如果在獲取自旋鎖時鎖已經有保持者,那麼獲取鎖操作將自旋在那裡,直到該自旋鎖的保持者釋放了鎖。
無論是互斥鎖,還是自旋鎖,在任何時刻,最多只能有一個保持者,也就說,在任何時刻最多只能有一個執行單元獲得鎖。
自旋鎖的API有:
spin_lock_init(x)
該宏用於初始化自旋鎖x。自旋鎖在真正使用前必須先初始化。該宏用於動態初始化。
DEFINE_SPINLOCK(x)
該宏聲明一個自旋鎖x並初始化它。該宏在2.6.11中第一次被定義,在先前的內核中並沒有該宏。
SPIN_LOCK_UNLOCKED
該宏用於靜態初始化一個自旋鎖。
DEFINE_SPINLOCK(x)等同於spinlock_t x = 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_trylock或spin_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_bh和spin_unlock_bh來保護。當然使用spin_lock_irq和spin_unlock_irq以及spin_lock_irqsave和spin_unlock_irqrestore也可以,它們失效了本地硬中斷,失效硬中斷隱式地也失效了軟中斷。但是使用spin_lock_bh和spin_unlock_bh是最恰當的,它比其他兩個快。
如果被保護的共享資源只在進程上下文和tasklet或timer上下文訪問,那麼應該使用與上面情況相同的獲得和釋放鎖的宏,因為tasklet和timer是用軟中斷實現的。
如果被保護的共享資源只在一個tasklet或timer上下文訪問,那麼不需要任何自旋鎖保護,因為同一個tasklet或timer只能在一個CPU上運行,即使是在SMP環境下也是如此。實際上tasklet在調用tasklet_schedule標記其需要被調度時已經把該tasklet綁定到當前CPU,因此同一個tasklet決不可能同時在其他CPU上運行。timer也是在其被使用add_timer添加到timer隊列中時已經被幫定到當前CPU,所以同一個timer絕不可能運行在其他CPU上。當然同一個tasklet有兩個實例同時運行在同一個CPU就更不可能了。
如果被保護的共享資源只在兩個或多個tasklet或timer上下文訪問,那麼對共享資源的訪問僅需要用spin_lock和spin_unlock來保護,不必使用_bh版本,因為當tasklet或timer運行時,不可能有其他tasklet或timer在當前CPU上運行。如果被保護的共享資源只在一個軟中斷(tasklet和timer除外)上下文訪問,那麼這個共享資源需要用spin_lock和spin_unlock來保護,因為同樣的軟中斷可以同時在不同的CPU上運行。
如果被保護的共享資源在兩個或多個軟中斷上下文訪問,那麼這個共享資源當然更需要用spin_lock和spin_unlock來保護,不同的軟中斷能夠同時在不同的CPU上運行。
如果被保護的共享資源在軟中斷(包括tasklet和timer)或進程上下文和硬中斷上下文訪問,那麼在軟中斷或進程上下文訪問期間,可能被硬中斷打斷,從而進入硬中斷上下文對共享資源進行訪問,因此,在進程或軟中斷上下文需要使用spin_lock_irq和spin_unlock_irq來保護對共享資源的訪問。而在中斷處理句柄中使用什麼版本,需依情況而定,如果只有一個中斷處理句柄訪問該共享資源,那麼在中斷處理句柄中僅需要spin_lock和spin_unlock來保護對共享資源的訪問就可以了。因為在執行中斷處理句柄期間,不可能被同一CPU上的軟中斷或進程打斷。但是如果有不同的中斷處理句柄訪問該共享資源,那麼需要在中斷處理句柄中使用spin_lock_irq和spin_unlock_irq來保護對共享資源的訪問。
在使用spin_lock_irq和spin_unlock_irq的情況下,完全可以用spin_lock_irqsave和spin_unlock_irqrestore取代,那具體應該使用哪一個也需要依情況而定,如果可以確信在對共享資源訪問前中斷是使能的,那麼使用spin_lock_irq更好一些,因為它比spin_lock_irqsave要快一些,但是如果你不能確定是否中斷使能,那麼使用spin_lock_irqsave和spin_unlock_irqrestore更好,因為它將恢復訪問共享資源前的中斷標志而不是直接使能中斷。當然,有些情況下需要在訪問共享資源時必須中斷失效,而訪問完後必須中斷使能,這樣的情形使用spin_lock_irq和spin_unlock_irq最好。
需要特別提醒讀者,spin_lock用於阻止在不同CPU上的執行單元對共享資源的同時訪問以及不同進程上下文互相搶占導致的對共享資源的非同步訪問,而中斷失效和軟中斷失效卻是為了阻止在同一CPU上軟中斷或中斷對共享資源的非同步訪問。
六、大內核鎖(BKL--Big Kernel Lock)
大內核鎖本質上也是自旋鎖,但是它又不同於自旋鎖,自旋鎖是不可以遞歸獲得鎖的,因為那樣會導致死鎖。但大內核鎖可以遞歸獲得鎖。大內核鎖用於保護整個內核,而自旋鎖用於保護非常特定的某一共享資源。進程保持大內核鎖時可以發生調度,具體實現是:在執行schedule時,schedule將檢查進程是否擁有大內核鎖,如果有,它將被釋放,以致於其它的進程能夠獲得該鎖,而當輪到該進程運行時,再讓它重新獲得大內核鎖。注意在保持自旋鎖期間是不運行發生調度的。
需要特別指出,整個內核只有一個大內核鎖,其實不難理解,內核只有一個,而大內核鎖是保護整個內核的,當然有且只有一個就足夠了。
還需要特別指出的是,大內核鎖是歷史遺留,內核中用的非常少,一般保持該鎖的時間較長,因此不提倡使用它。從2.6.11內核起,大內核鎖可以通過配置內核使其變得可搶占(自旋鎖是不可搶占的),這時它實質上是一個互斥鎖,使用信號量實現。
大內核鎖的API包括:
void lock_kernel(void);
該函數用於得到大內核鎖。它可以遞歸調用而不會導致死鎖。
void unlock_kernel(void);
該函數用於釋放大內核鎖。當然必須與lock_kernel配對使用,調用了多少次lock_kernel,就需要調用多少次unlock_kernel。
大內核鎖的API使用非常簡單,按照以下方式使用就可以了:
lock_kernel();
//對被保護的共享資源的訪問
…
unlock_kernel();
七、讀寫鎖(rwlock)
讀寫鎖實際是一種特殊的自旋鎖,它把對共享資源的訪問者劃分成讀者和寫者,讀者只對共享資源進行讀訪問,寫者則需要對共享資源進行寫操作。這種鎖相對於自旋鎖而言,能提高並發性,因為在多處理器系統中,它允許同時有多個讀者來訪問共享資源,最大可能的讀者數為實際的邏輯CPU數。寫者是排他性的,一個讀寫鎖同時只能有一個寫者或多個讀者(與CPU數相關),但不能同時既有讀者又有寫者。
在讀寫鎖保持期間也是搶占失效的。
如果讀寫鎖當前沒有讀者,也沒有寫者,那麼寫者可以立刻獲得讀寫鎖,否則它必須自旋在那裡,直到沒有任何寫者或讀者。如果讀寫鎖沒有寫者,那麼讀者可以立即獲得該讀寫鎖,否則讀者必須自旋在那裡,直到寫者釋放該讀寫鎖。
讀寫鎖的API看上去與自旋鎖很象,只是讀者和寫者需要不同的獲得和釋放鎖的API。下面是讀寫鎖API清單:
rwlock_init(x)
該宏用於動態初始化讀寫鎖x。
DEFINE_RWLOCK(x)
該宏聲明一個讀寫鎖並對其進行初始化。它用於靜態初始化。
RW_LOCK_UNLOCKED
它用於靜態初始化一個讀寫鎖。
DEFINE_RWLOCK(x)等同於rwlock_t x = 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配對使用。
讀寫鎖的獲得和釋放鎖的方法也有許多版本,具體用哪個與自旋鎖一樣,因此參考自旋鎖部分就可以了。只是需要區分讀者與寫者,讀者要用讀者版本,而寫者必須用寫者版本。
八、大讀者鎖(brlock-Big Reader Lock)
大讀者鎖是讀寫鎖的高性能版,讀者可以非常快地獲得鎖,但寫者獲得鎖的開銷比較大。大讀者鎖只存在於2.4內核中,在2.6中已經沒有這種鎖(提醒讀者特別注意)。它們的使用與讀寫鎖的使用類似,只是所有的大讀者鎖都是事先已經定義好的。這種鎖適合於讀多寫少的情況,它在這種情況下遠好於讀寫鎖。
大讀者鎖的實現機制是:每一個大讀者鎖在所有CPU上都有一個本地讀者寫者鎖,一個讀者僅需要獲得本地CPU的讀者鎖,而寫者必須獲得所有CPU上的鎖。
大讀者鎖的API非常類似於讀寫鎖,只是鎖變量為預定義的鎖ID。
void br_read_lock (enum brlock_indices idx);
讀者使用該函數來獲得大讀者鎖idx,在2.4內核中,預定義的idx允許的值有兩個:BR_GLOBALIRQ_LOCK和BR_NETPROTO_LOCK,當然,用戶可以添加自己定義的大讀者鎖ID 。
void br_read_unlock (enum brlock_indices idx);
讀者使用該函數釋放大讀者鎖idx。
void br_write_lock (enum brlock_indices idx);
寫者使用它來獲得大讀者鎖idx。
void br_write_unlock (enum brlock_indices idx);
寫者使用它來釋放大讀者鎖idx。
br_read_lock_irqsave(idx, flags)
讀者也可以使用該宏來獲得大讀者鎖idx,與br_read_lock不同的是,該宏還同時把寄存器的值保存到變量flags中,並且失效本地中斷。
br_read_lock_irq(idx)
讀者也可以使用該宏來獲得大讀者鎖idx,與br_read_lock不同的是,該宏還同時失效本地中斷。與br_read_lock_irqsave不同的是,該宏不保存標志寄存器。
br_read_lock_bh(idx)
讀者也可以使用該宏來獲得大讀者鎖idx,與br_read_lock不同的是,該宏還同時失效本地軟中斷。
br_write_lock_irqsave(idx, flags)
寫者也可以使用該宏來獲得大讀者鎖idx,與br_write_lock不同的是,該宏還同時把寄存器的值保存到變量flags中,並且失效本地中斷。
br_write_lock_irq(idx)
寫者也可以使用該宏來獲得大讀者鎖idx,與br_write_lock不同的是,該宏還同時失效本地中斷。與br_write_lock_irqsave不同的是,該宏不保存標志寄存器。
br_write_lock_bh(idx)
寫者也可以使用該宏來獲得大讀者鎖idx,與br_write_lock不同的是,該宏還同時失效本地軟中斷。
br_read_unlock_irqrestore(idx, flags)
讀者也使用該宏來釋放大讀者鎖idx,它與br_read_unlock不同之處是,該宏還同時把變量flags的值恢復到標志寄存器。
br_read_unlock_irq(idx)
讀者也使用該宏來釋放大讀者鎖idx,它與br_read_unlock不同之處是,該宏還同時使能本地中斷。
br_read_unlock_bh(idx)
讀者也使用該宏來釋放大讀者鎖idx,它與br_read_unlock不同之處是,該宏還同時使能本地軟中斷。
br_write_unlock_irqrestore(idx, flags)
寫者也使用該宏來釋放大讀者鎖idx,它與br_write_unlock不同之處是,該宏還同時把變量flags的值恢復到標志寄存器。
br_write_unlock_irq(idx)
寫者也使用該宏來釋放大讀者鎖idx,它與br_write_unlock不同之處是,該宏還同時使能本地中斷。
br_write_unlock_bh(idx)
寫者也使用該宏來釋放大讀者鎖idx,它與br_write_unlock不同之處是,該宏還同時使能本地軟中斷。
這些API的使用與讀寫鎖完全一致。
九、RCU(Read-Copy Update)
RCU(Read-Copy Update),顧名思義就是讀-拷貝修改,它是基於其原理命名的。對於被RCU保護的共享數據結構,讀者不需要獲得任何鎖就可以訪問它,但寫者在訪問它時首先拷貝一個副本,然後對副本進行修改,最後使用一個回調(callback)機制在適當的時機把指向原來數據的指針重新指向新的被修改的數據。這個時機就是所有引用該數據的CPU都退出對共享數據的操作。
RCU也是讀寫鎖的高性能版本,但是它比大讀者鎖具有更好的擴展性和性能。RCU既允許多個讀者同時訪問被保護的數據,又允許多個讀者和多個寫者同時訪問被保護的數據(注意:是否可以有多個寫者並行訪問取決於寫者之間使用的同步機制),讀者沒有任何同步開銷,而寫者的同步開銷則取決於使用的寫者間同步機制。但RCU不能替代讀寫鎖,因為如果寫比較多時,對讀者的性能提高不能彌補寫者導致的損失。
RCU的API如下;
rcu_read_lock()
讀者在讀取由RCU保護的共享數據時使用該函數標記它進入讀端臨界區。
rcu_read_unlock()
該函數與rcu_read_lock配對使用,用以標記讀者退出讀端臨界區。
synchronize_rcu()
該函數由RCU寫端調用,它將阻塞寫者,直到經過grace period後,即所有的讀者已經完成讀端臨界區,寫者才可以繼續下一步操作。如果有多個RCU寫端調用該函數,他們將在一個grace period之後全部被喚醒。
synchronize_kernel()
其他非RCU的內核代碼使用該函數來等待所有CPU處在可搶占狀態,目前功能等同於synchronize_rcu,但現在已經不建議使用,而使用synchronize_sched。
synchronize_sched()
該函數用於等待所有CPU都處在可搶占狀態,它能保證正在運行的中斷處理函數處理完畢,但不能保證正在運行的softirq處理完畢。注意,synchronize_rcu只保證所有CPU都處理完正在運行的讀端臨界區。
void fastcall call_rcu(struct rcu_head *head,
void (*func)(struct rcu_head *rcu))
struct rcu_head {
struct rcu_head *next;
void (*func)(struct rcu_head *head);
};
函數call_rcu也由RCU寫端調用,它不會使寫者阻塞,因而可以在中斷上下文或softirq使用。該函數將把函數func掛接到RCU回調函數鏈上,然後立即返回。一旦所有的CPU都已經完成端臨界區操作,該函數將被調用來釋放刪除的將絕不在被應用的數據。參數head用於記錄回調函數func,一般該結構會作為被RCU保護的數據結構的一個字段,以便省去單獨為該結構分配內存的操作。需要指出的是,函數synchronize_rcu的實現實際上使用函數call_rcu。
void fastcall call_rcu_bh(struct rcu_head *head,
void (*func)(struct rcu_head *rcu))
函數call_ruc_bh功能幾乎與call_rcu完全相同,唯一差別就是它把softirq的完成也當作經歷一個quiescent state,因此如果寫端使用了該函數,在進程上下文的讀端必須使用rcu_read_lock_bh。
#define rcu_dereference(p) ({ \
typeof(p) _________p1 = p; \
smp_read_barrier_depends(); \
(_________p1); \
})
該宏用於在RCU讀端臨界區獲得一個RCU保護的指針,該指針可以在以後安全地引用,內存柵只在alpha架構上才使用。
除了這些API,RCU還增加了鏈表操作的RCU版本,因為對於RCU,對共享數據的操作必須保證能夠被沒有使用同步機制的讀者看到,所以內存柵是非常必要的。
static inline void list_add_rcu(struct list_head *new, struct list_head *head)
該函數把鏈表項new插入到RCU保護的鏈表head的開頭。使用內存柵保證了在引用這個新插入的鏈表項之前,新鏈表項的鏈接指針的修改對所有讀者是可見的。
static inline void list_add_tail_rcu(struct list_head *new,
struct list_head *head)
該函數類似於list_add_rcu,它將把新的鏈表項new添加到被RCU保護的鏈表的末尾。
static inline void list_del_rcu(struct list_head *entry)
該函數從RCU保護的鏈表中移走指定的鏈表項entry,並且把entry的prev指針設置為LIST_POISON2,但是並沒有把entry的next指針設置為LIST_POISON1,因為該指針可能仍然在被讀者用於便利該鏈表。
static inline void list_replace_rcu(struct list_head *old, struct list_head *new)
該函數是RCU新添加的函數,並不存在非RCU版本。它使用新的鏈表項new取代舊的鏈表項old,內存柵保證在引用新的鏈表項之前,它的鏈接指針的修正對所有讀者可見。
list_for_each_rcu(pos, head)
該宏用於遍歷由RCU保護的鏈表head,只要在讀端臨界區使用該函數,它就可以安全地和其它_rcu鏈表操作函數(如list_add_rcu)並發運行。
list_for_each_safe_rcu(pos, n, head)
該宏類似於list_for_each_rcu,但不同之處在於它允許安全地刪除當前鏈表項pos。
list_for_each_entry_rcu(pos, head, member)
該宏類似於list_for_each_rcu,不同之處在於它用於遍歷指定類型的數據結構鏈表,當前鏈表項pos為一包含struct list_head結構的特定的數據結構。
list_for_each_continue_rcu(pos, head)
該宏用於在退出點之後繼續遍歷由RCU保護的鏈表head。
static inline void hlist_del_rcu(struct hlist_node *n)
它從由RCU保護的哈希鏈表中移走鏈表項n,並設置n的ppre指針為LIST_POISON2,但並沒有設置next為LIST_POISON1,因為該指針可能被讀者使用用於遍利鏈表。
static inline void hlist_add_head_rcu(struct hlist_node *n,
struct hlist_head *h)
該函數用於把鏈表項n插入到被RCU保護的哈希鏈表的開頭,但同時允許讀者對該哈希鏈表的遍歷。內存柵確保在引用新鏈表項之前,它的指針修正對所有讀者可見。
hlist_for_each_rcu(pos, head)
該宏用於遍歷由RCU保護的哈希鏈表head,只要在讀端臨界區使用該函數,它就可以安全地和其它_rcu哈希鏈表操作函數(如hlist_add_rcu)並發運行。
hlist_for_each_entry_rcu(tpos, pos, head, member)
類似於hlist_for_each_rcu,不同之處在於它用於遍歷指定類型的數據結構哈希鏈表,當前鏈表項pos為一包含struct list_head結構的特定的數據結構。
對於RCU更詳細的原理、實現機制以及應用請參看作者專門針對RCU發表的一篇文章,"Linux 2.6內核中新的鎖機制--RCU(Read-Copy Update)"。
十、順序鎖(seqlock)
順序鎖也是對讀寫鎖的一種優化,對於順序鎖,讀者絕不會被寫者阻塞,也就說,讀者可以在寫者對被順序鎖保護的共享資源進行寫操作時仍然可以繼續讀,而不必等待寫者完成寫操作,寫者也不需要等待所有讀者完成讀操作才去進行寫操作。但是,寫者與寫者之間仍然是互斥的,即如果有寫者在進行寫操作,其他寫者必須自旋在那裡,直到寫者釋放了順序鎖。
這種鎖有一個限制,它必須要求被保護的共享資源不含有指針,因為寫者可能使得指針失效,但讀者如果正要訪問該指針,將導致OOPs。
如果讀者在讀操作期間,寫者已經發生了寫操作,那麼,讀者必須重新讀取數據,以便確保得到的數據是完整的。
這種鎖對於讀寫同時進行的概率比較小的情況,性能是非常好的,而且它允許讀寫同時進行,因而更大地提高了並發性。
順序鎖的API如下:
void write_seqlock(seqlock_t *sl);
寫者在訪問被順序鎖s1保護的共享資源前需要調用該函數來獲得順序鎖s1。它實際功能上等同於spin_lock,只是增加了一個對順序鎖順序號的加1操作,以便讀者能夠檢查出是否在讀期間有寫者訪問過。
void write_sequnlock(seqlock_t *sl);
寫者在訪問完被順序鎖s1保護的共享資源後需要調用該函數來釋放順序鎖s1。它實際功能上等同於spin_unlock,只是增加了一個對順序鎖順序號的加1操作,以便讀者能夠檢查出是否在讀期間有寫者訪問過。
寫者使用順序鎖的模式如下:
write_seqlock(&seqlock_a);
//寫操作代碼塊
…
write_sequnlock(&seqlock_a);
因此,對寫者而言,它的使用與spinlock相同。
int write_tryseqlock(seqlock_t *sl);
寫者在訪問被順序鎖s1保護的共享資源前也可以調用該函數來獲得順序鎖s1。它實際功能上等同於spin_trylock,只是如果成功獲得鎖後,該函數增加了一個對順序鎖順序號的加1操作,以便讀者能夠檢查出是否在讀期間有寫者訪問過。
unsigned read_seqbegin(const seqlock_t *sl);
讀者在對被順序鎖s1保護的共享資源進行訪問前需要調用該函數。讀者實際沒有任何得到鎖和釋放鎖的開銷,該函數只是返回順序鎖s1的當前順序號。
int read_seqretry(const seqlock_t *sl, unsigned iv);
讀者在訪問完被順序鎖s1保護的共享資源後需要調用該函數來檢查,在讀訪問期間是否有寫者訪問了該共享資源,如果是,讀者就需要重新進行讀操作,否則,讀者成功完成了讀操作。
因此,讀者使用順序鎖的模式如下:
do {
seqnum = read_seqbegin(&seqlock_a);
//讀操作代碼塊
...
} while (read_seqretry(&seqlock_a, seqnum));
write_seqlock_irqsave(lock, flags)
寫者也可以用該宏來獲得順序鎖lock,與write_seqlock不同的是,該宏同時還把標志寄存器的值保存到變量flags中,並且失效了本地中斷。
write_seqlock_irq(lock)
寫者也可以用該宏來獲得順序鎖lock,與write_seqlock不同的是,該宏同時還失效了本地中斷。與write_seqlock_irqsave不同的是,該宏不保存標志寄存器。
write_seqlock_bh(lock)
寫者也可以用該宏來獲得順序鎖lock,與write_seqlock不同的是,該宏同時還失效了本地軟中斷。
write_sequnlock_irqrestore(lock, flags)
寫者也可以用該宏來釋放順序鎖lock,與write_sequnlock不同的是,該宏同時還把標志寄存器的值恢復為變量flags的值。它必須與write_seqlock_irqsave配對使用。
write_sequnlock_irq(lock)
寫者也可以用該宏來釋放順序鎖lock,與write_sequnlock不同的是,該宏同時還使能本地中斷。它必須與write_seqlock_irq配對使用。
write_sequnlock_bh(lock)
寫者也可以用該宏來釋放順序鎖lock,與write_sequnlock不同的是,該宏同時還使能本地軟中斷。它必須與write_seqlock_bh配對使用。
read_seqbegin_irqsave(lock, flags)
讀者在對被順序鎖lock保護的共享資源進行訪問前也可以使用該宏來獲得順序鎖lock的當前順序號,與read_seqbegin不同的是,它同時還把標志寄存器的值保存到變量flags中,並且失效了本地中斷。注意,它必須與read_seqretry_irqrestore配對使用。
read_seqretry_irqrestore(lock, iv, flags)
讀者在訪問完被順序鎖lock保護的共享資源進行訪問後也可以使用該宏來檢查,在讀訪問期間是否有寫者訪問了該共享資源,如果是,讀者就需要重新進行讀操作,否則,讀者成功完成了讀操作。它與read_seqretry不同的是,該宏同時還把標志寄存器的值恢復為變量flags的值。注意,它必須與read_seqbegin_irqsave配對使用。
因此,讀者使用順序鎖的模式也可以為:
do {
seqnum = read_seqbegin_irqsave(&seqlock_a, flags);
//讀操作代碼塊
...
} while (read_seqretry_irqrestore(&seqlock_a, seqnum, flags));
讀者和寫者所使用的API的幾個版本應該如何使用與自旋鎖的類似。
如果寫者在操作被順序鎖保護的共享資源時已經保持了互斥鎖保護對共享數據的寫操作,即寫者與寫者之間已經是互斥的,但讀者仍然可以與寫者同時訪問,那麼這種情況僅需要使用順序計數(seqcount),而不必要spinlock。
順序計數的API如下:
unsigned read_seqcount_begin(const seqcount_t *s);
讀者在對被順序計數保護的共享資源進行讀訪問前需要使用該函數來獲得當前的順序號。
int read_seqcount_retry(const seqcount_t *s, unsigned iv);
讀者在訪問完被順序計數s保護的共享資源後需要調用該函數來檢查,在讀訪問期間是否有寫者訪問了該共享資源,如果是,讀者就需要重新進行讀操作,否則,讀者成功完成了讀操作。
因此,讀者使用順序計數的模式如下:
do {
seqnum = read_seqbegin_count(&seqcount_a);
//讀操作代碼塊
...
} while (read_seqretry(&seqcount_a, seqnum));
void write_seqcount_begin(seqcount_t *s);
寫者在訪問被順序計數保護的共享資源前需要調用該函數來對順序計數的順序號加1,以便讀者能夠檢查出是否在讀期間有寫者訪問過。
void write_seqcount_end(seqcount_t *s);
寫者在訪問完被順序計數保護的共享資源後需要調用該函數來對順序計數的順序號加1,以便讀者能夠檢查出是否在讀期間有寫者訪問過。
寫者使用順序計數的模式為:
write_seqcount_begin(&seqcount_a);
//寫操作代碼塊
…
write_seqcount_end(&seqcount_a);
需要特別提醒,順序計數的使用必須非常謹慎,只有確定在訪問共享數據時已經保持了互斥鎖才可以使用。
小結
自linux 2.4以來,內核對SMP的支持越來越好,很大程度上,對SMP的支持,這些鎖機制是非常必要和重要的。基本上,內核開發者在開發中都會需要使用一些同步機制,本文通過詳細地講解內核中所有的同步機制,使得讀者能夠對內核鎖機制有全面的了解和把握。