Linux內核中的各種鎖
在LInux操作系統里,同一時間可能有多個內核執行流在執行,因此內核其實象多進程多線程編程一樣也需要一些同步機制來同步各執行單元對共享數據的訪問。尤其是在多處理器系統上,更需要一些同步機制來同步不同處理器上的執行單元對共享的數據的訪問。
linux內核
在主流的Linux內核中包含了幾乎所有現代的操作系統具有的同步機制,這些同步機制包括:原子操作、信號量(semaphore)、讀寫信號量(rw_semaphore)、spinlock、BKL(Big Kernel Lock)、rwlock ,RCU(在開發內核2.5.43中引入該技術的并正式包含在2.6內核中)和seqlock(只包含在2.6以后內核中)。
首先明確鎖的引入不可避免的引起性能的損失,研究表明隨著計算機硬件的快速發展,獲得這種鎖的開銷相對于CPU的速度在成倍地增加,原因很簡單,CPU的速度與訪問內存的速度差距越來越大,而這種鎖使用了原子操作指令,它需要原子地訪問內存,也就說獲得鎖的開銷與訪存速度相關,另外在大部分非x86架構上獲取鎖使用了內存柵(Memory Barrier),這會導致處理器流水線停滯或刷新,因此它的開銷相對于CPU速度而言就影響
越來越大。
因此對于以上每種鎖都要明確其特定的應用場合,使用不當反而會影響性能甚至錯誤。
原子操作
原子操作簡介
什么是原子性,就是不可再分,該操作絕不會在執行完畢前被任何其他任務或事件打斷,也就說,它的最小的執行單位,不可能有比它更小的執行單位,因此這里的原子實際是使用了物理學里的物質微粒的概念。
原子操作需要硬件的支持,因此是架構相關的,其API和原子類型的定義都定義在內核源碼樹的include/asm/atomic.h文件中,它們都使用匯編語言實現,因為C語言并不能實現這樣的操作。
原子操作主要用于實現資源計數,很多引用計數(refcnt)就是通過原子操作實現的。
原子類型定義如下:
typedef struct { volatile int counter; } atomic_t;1
volatile修飾字段告訴gcc不要對該類型的數據做優化處理,對它的訪問都是對內存的訪問,而不是對寄存器的訪問。
api如下:
atomic_read(atomic_t * v);1
該函數對原子類型的變量進行原子讀操作,它返回原子類型的變量v的值。
atomic_set(atomic_t * v, int i);1
該函數設置原子類型的變量v的值為i。
void atomic_add(int i, atomic_t *v);1
該函數給原子類型的變量v增加值i。
atomic_sub(int i, atomic_t *v);1
該函數從原子類型的變量v中減去i。
int atomic_sub_and_test(int i, atomic_t *v);1
該函數從原子類型的變量v中減去i,并判斷結果是否為0,如果為0,返回真,否則返回假。
void atomic_inc(atomic_t *v);1
該函數對原子類型變量v原子地增加1。
void atomic_dec(atomic_t *v);1
該函數對原子類型的變量v原子地減1。
int atomic_dec_and_test(atomic_t *v);1
該函數對原子類型的變量v原子地減1,并判斷結果是否為0,如果為0,返回真,否則返回假。
int atomic_inc_and_test(atomic_t *v);1
該函數對原子類型的變量v原子地增加1,并判斷結果是否為0,如果為0,返回真,否則返回假。
int atomic_add_negative(int i, atomic_t *v);1
該函數對原子類型的變量v原子地增加i,并判斷結果是否為負數,如果是,返回真,否則返回假。
int atomic_add_return(int i, atomic_t *v);1
該函數對原子類型的變量v原子地增加i,并且返回指向v的指針。
int atomic_sub_return(int i, atomic_t *v);1
該函數從原子類型的變量v中減去i,并且返回指向v的指針。
int atomic_inc_return(atomic_t * v);1
該函數對原子類型的變量v原子地增加1并且返回指向v的指針。
int atomic_dec_return(atomic_t * v);1
該函數對原子類型的變量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介紹
DEFINE_MUTEX1
靜態定義和初始化一個互斥鎖. 1
void mutex_init(struct mutex *mutex);1
動態初始化一個互斥鎖
void sema_init (struct semaphore *sem, int val);1
該函用于數初始化設置信號量的初值,它設置信號量sem的值為val。
void down(struct semaphore * sem);1
該函數用于獲得信號量sem,它會導致睡眠,因此不能在中斷上下文(包括IRQ上下文和softirq上下文)使用該函數。該函數首先判斷sem->count的值是否大于0,如果true則sem->count–,否者調用者將被掛起,直到別的任務釋放該信號量才能繼續運行。
int down_interruptible(struct semaphore * sem);1
該函數功能與down類似,不同之處為,down不會被信號(signal)打斷,但down_interruptible能被信號打斷,因此該函數有返回值來區分是正常返回還是被信號中斷,如果返回0,表示獲得信號量正常返回,如果被信號打斷,返回-EINTR。
int down_trylock(struct semaphore * sem);1
該函數試著獲得信號量sem,如果能夠立刻獲得,它就獲得該信號量并返回0,否則,表示不能獲得信號量sem,返回值為非0值。因此,它不會導致調用者睡眠,可以在中斷上下文使用。
void up(struct semaphore * sem);1
該函數釋放信號量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)1
該宏聲明一個讀寫信號量name并對其進行初始化。
void init_rwsem(struct rw_semaphore *sem);1
該函數對讀寫信號量sem進行初始化。
void down_read(struct rw_semaphore *sem);1
讀者調用該函數來得到讀寫信號量sem。該函數會導致調用者睡眠,因此只能在進程上下文使用。
int down_read_trylock(struct rw_semaphore *sem);1
該函數類似于down_read,只是它不會導致調用者睡眠。它盡力得到讀寫信號量sem,如果能夠立即得到,它就得到該讀寫信號量,并且返回1,否則表示不能立刻得到該信號量,返回0。因此,它也可以在中斷上下文使用。
void down_write(struct rw_semaphore *sem);1
寫者使用該函數來得到讀寫信號量sem,它也會導致調用者睡眠,因此只能在進程上下文使用。
int down_write_trylock(struct rw_semaphore *sem);1
該函數類似于down_write,只是它不會導致調用者睡眠。該函數盡力得到讀寫信號量,如果能夠立刻獲得,就獲得該讀寫信號量并且返回1,否則表示無法立刻獲得,返回0。它可以在中斷上下文使用。
void up_read(struct rw_semaphore *sem);1
讀者使用該函數釋放讀寫信號量sem。它與down_read或down_read_trylock配對使用。如果down_read_trylock返回0,不需要調用up_read來釋放讀寫信號量,因為根本就沒有獲得信號量。
void up_write(struct rw_semaphore *sem);1
寫者調用該函數釋放信號量sem。它與down_write或down_write_trylock配對使用。如果down_write_trylock返回0,不需要調用up_write,因為返回0表示沒有獲得該讀寫信號量。
void downgrade_write(struct rw_semaphore *sem);1
該函數用于把寫者降級為讀者,這有時是必要的。因為寫者是排他性的,因此在寫者保持讀寫信號量期間,任何讀者或寫者都將無法訪問該讀寫信號量保護的共享資源,對于那些當前條件下不需要寫訪問的寫者,降級為讀者將使得等待訪問的讀者能夠立刻訪問,從而增加了并發性,提高了效率。
讀寫信號量適于在讀多寫少的情況下使用,在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且不可搶占的內核下,自旋鎖的所有操作都是空操作,在單CPU且可搶占的內核下,自旋鎖實際上只進行開啟和關閉內核搶占的操作。
跟互斥鎖一樣,一個執行單元要想訪問被自旋鎖保護的共享資源,必須先得到鎖,在訪問完共享資源后,必須釋放鎖。如果在獲取自旋鎖時,沒有任何執行單元保持該鎖,那么將立即得到鎖;如果在獲取自旋鎖時鎖已經有保持者,那么獲取鎖操作將自旋在那里,直到該自旋鎖的保持者釋放了鎖。
無論是互斥鎖,還是自旋鎖,在任何時刻,最多只能有一個保持者,也就說,在任何時刻最多只能有一個執行單元獲得鎖。
自旋鎖的API
spin_lock_init(x)1
該宏用于初始化自旋鎖x。自旋鎖在真正使用前必須先初始化。該宏用于動態初始化。
DEFINE_SPINLOCK(x)1
該宏聲明一個自旋鎖x并初始化它。該宏在2.6.11中第一次被定義,在先前的內核中并沒有該宏。
SPIN_LOCK_UNLOCKED1
該宏用于靜態初始化一個自旋鎖。
DEFINE_SPINLOCK(x)等同于spinlock_t x = SPIN_LOCK_UNLOCKED
spin_is_locked(x)1
該宏用于判斷自旋鎖x是否已經被某執行單元保持(即被鎖),如果是,返回真,否則返回假。
spin_unlock_wait(x)1
該宏用于等待自旋鎖x變得沒有被任何執行單元保持,如果沒有任何執行單元保持該自旋鎖,該宏立即返回,否則將循環在那里,直到該自旋鎖被保持者釋放。
spin_trylock(lock)1
該宏盡力獲得自旋鎖lock,如果能立即獲得鎖,它獲得鎖并返回真,否則不能立即獲得鎖,立即返回假。它不會自旋等待lock被釋放。
spin_lock(lock)1
該宏用于獲得自旋鎖lock,如果能夠立即獲得鎖,它就馬上返回,否則,它將自旋在那里,直到該自旋鎖的保持者釋放,這時,它獲得鎖并返回。總之,只有它獲得鎖才返回。
spin_lock_irqsave(lock, flags)1
該宏獲得自旋鎖的同時把標志寄存器的值保存到變量flags中并失效本地中斷。
spin_lock_irq(lock)1
該宏類似于spin_lock_irqsave,只是該宏不保存標志寄存器的值。
spin_lock_bh(lock)1
該宏在得到自旋鎖的同時失效本地軟中斷。
spin_unlock(lock)1
該宏釋放自旋鎖lock,它與spin_trylock或spin_lock配對使用。如果spin_trylock返回假,表明沒有獲得自旋鎖,因此不必使用spin_unlock釋放。
spin_unlock_irqrestore(lock, flags)1
該宏釋放自旋鎖lock的同時,也恢復標志寄存器的值為變量flags保存的值。它與spin_lock_irqsave配對使用。
spin_unlock_irq(lock)1
該宏釋放自旋鎖lock的同時,也使能本地中斷。它與spin_lock_irq配對應用。
spin_unlock_bh(lock)1
該宏釋放自旋鎖lock的同時,也使能本地的軟中斷。它與spin_lock_bh配對使用。
spin_trylock_irqsave(lock, flags)1
該宏如果獲得自旋鎖lock,它也將保存標志寄存器的值到變量flags中,并且失效本地中斷,如果沒有獲得鎖,它什么也不做。因此如果能夠立即獲得鎖,它等同于spin_lock_irqsave,如果不能獲得鎖,它等同于spin_trylock。如果該宏獲得自旋鎖lock,那需要使用spin_unlock_irqrestore來釋放。
spin_trylock_irq(lock)1
該宏類似于spin_trylock_irqsave,只是該宏不保存標志寄存器。如果該宏獲得自旋鎖lock,需要使用spin_unlock_irq來釋放。
spin_trylock_bh(lock)1
該宏如果獲得了自旋鎖,它也將失效本地軟中斷。如果得不到鎖,它什么也不做。因此,如果得到了鎖,它等同于spin_lock_bh,如果得不到鎖,它等同于spin_trylock。如果該宏得到了自旋鎖,需要使用spin_unlock_bh來釋放。
spin_can_lock(lock)1
該宏用于判斷自旋鎖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是最恰當的,它比其他兩個快。
舉例說明:spinlock用在進程上下文和中斷
進程A中調用了spin_lock(&lock)然后進入臨界區,此時來了一個中斷(interrupt),該中斷也運行在和進程A相同的CPU上,并且在該中斷處理程序中恰巧也會spin_lock(&lock)試圖獲取同一個鎖。由于是在同一個CPU上被中斷,進程A會被設置為TASK_INTERRUPT狀態,
中斷處理程序無法獲得鎖,會不停的忙等,由于進程A被設置為中斷狀態,schedule()進程調度就無法再調度進程A運行,這樣就導致了死鎖!但是如果該中斷處理程序運行在不同的CPU上就不會觸發死鎖。因為在不同的CPU上出現中斷不會導致進程A的狀態被設為TASK_INTERRUPT,只是換出。當中斷處理程序忙等被換出后,進程A還是有機會獲得CPU,執行并退出臨界區,(有一個問題沒有搞懂關中斷是關閉對所有cpu的中斷還是本地cpu的?從關閉中斷的函數來看似乎是針對本地的),所以在使用spin_lock時要明確知道該鎖不會在中斷處理程序中使用。如果有,那就需要使用spinlock_irq_save,該函數即會關搶占,也會關本地中斷(因為不能保證打斷A的中斷和A不是一個cpu,因此spin_lock在多核cpu上使用還是要關中斷)。
被保護的共享資源只在進程上下文和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上軟中斷或中斷對共享資源的非同步訪問。
對于spin_lock用于阻止不同CPU上的執行單元對共享資源的同時訪問以及不同進程上下文互相搶占導致的對共享資源的非同步訪問在單核和多核cpu上的實現是不同的:
單核cpu
如果spin_lock不處于中斷上下文,則spin_lock鎖定的代碼只會在內核發生搶占2的時候才會丟失CPU擁有權。所以,對于單核來說,需要在spin_lock獲得鎖的時候禁止搶占,釋放鎖的時候開放搶占。因此這不是真正意義上的鎖。
內核代碼實現如下:
static inline void spin_lock(spinlock_t *lock){raw_spin_lock(&lock->rlock);}#define raw_spin_lock(lock) _raw_spin_lock(lock)#define _raw_spin_lock(lock) __LOCK(lock)#define __LOCK(lock) do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)可以看到僅禁止了內核搶占static inline void spin_unlock(spinlock_t *lock){raw_spin_unlock(&lock->rlock);}#define raw_spin_unlock(lock) _raw_spin_unlock(lock)#define _raw_spin_unlock(lock) __UNLOCK(lock)#define __UNLOCK(lock) do { preempt_enable(); __release(lock); (void)(lock); } while (0)
開啟內核搶占
多核
存在臨界區同時在多核上被執行的情況,這時候才需要一個真正的鎖來宣告代碼對資源的占有。幾個核可能會同時access臨界區,這時的spinlock是如何實現的呢?
要用到CPU提供的一些特殊指令,對lock變量進行原子操作。
SMP中spin_lock的實現
實現在include/linux/spinlock_api_smp.h
static inline void __raw_spin_lock(raw_spinlock_t*lock)
{
preempt_disable();
spin_acquire(&lock->dep_map, 0, 0,_RET_IP_);
LOCK_CONTENDED(lock, do_raw_spin_trylock,do_raw_spin_lock);}
SMP上的實現被分解為三句話。
preempt_disable() 關搶占
spin_acquire()是sparse檢查需要
LOCK_CONTENDED()是一個宏,如果不考慮CONFIG_LOCK_STAT(該宏是為了統計lock的操作),則:
#define LOCK_CONTENDED(_lock, try, lock) lock(_lock)
則第三句話等同于:
do_raw_spin_lock(lock)
而do_raw_spin_lock()則可以從spinlock.h中找到痕跡:
static inline intdo_raw_spin_trylock(raw_spinlock_t *lock){ return arch_spin_trylock(&(lock)->raw_lock);}
看到arch,我們明白這個函數是體系結構相關的。這部分代碼使用匯編實現。(具體例子后續再加)
大內核鎖(BKL–Big Kernel Lock)
大內核鎖簡介
大內核鎖本質上也是自旋鎖,但是它又不同于自旋鎖,自旋鎖是不可以遞歸獲得鎖的,因為那樣會導致死鎖。但大內核鎖可以遞歸獲得鎖。大內核鎖用于保護整個內核,而自旋鎖用于保護非常特定的某一共享資源。進程保持大內核鎖時可以發生調度,具體實現是:在執行schedule時,schedule將檢查進程是否擁有大內核鎖,如果有,它將被釋放,以致于其它的進程能夠獲得該鎖,而當輪到該進程運行時,再讓它重新獲得大內核鎖。注意在保持自旋鎖期間是不允許發生調度的。
需要特別指出,整個內核只有一個大內核鎖,其實不難理解,內核只有一個,而大內核鎖是保護整個內核的,當然有且只有一個就足夠了。
還需要特別指出的是,大內核鎖是歷史遺留,內核中用的非常少,一般保持該鎖的時間較長,因此不提倡使用它。從2.6.11內核起,大內核鎖可以通過配置內核使其變得可搶占(自旋鎖是不可搶占的),這時它實質上是一個互斥鎖,使用信號量實現。
大內核鎖的API
void lock_kernel(void);1
該函數用于得到大內核鎖。它可以遞歸調用而不會導致死鎖。
void unlock_kernel(void);1
該函數用于釋放大內核鎖。當然必須與lock_kernel配對使用,調用了多少次lock_kernel,就需要調用多少次unlock_kernel。
大內核鎖的API使用非常簡單,按照以下方式使用就可以了:
lock_kernel();
//對被保護的共享資源的訪問
…
unlock_kernel();1234
(六)讀寫鎖(rwlock)
讀寫鎖簡介
讀寫鎖實際是一種特殊的自旋鎖,它把對共享資源的訪問者劃分成讀者和寫者,讀者只對共享資源進行讀訪問,寫者則需要對共享資源進行寫操作。這種鎖相對于自旋鎖而言,能提高并發性,因為在多處理器系統中,它允許同時有多個讀者來訪問共享資源,最大可能的讀者數為實際的邏輯CPU數。寫者是排他性的,一個讀寫鎖同時只能有一個寫者或多個讀者(與CPU數相關),但不能同時既有讀者又有寫者。
在讀寫鎖保持期間也是搶占失效的。
讀寫鎖訪問規則:
如果讀寫鎖當前沒有讀者,也沒有寫者,那么寫者可以立刻獲得讀寫鎖,否則它必須自旋在那里,直到沒有任何寫者或讀者。
如果讀寫鎖沒有寫者,那么讀者可以立即獲得該讀寫鎖,否則讀者必須自旋在那里,直到寫者釋放該讀寫鎖。
讀寫鎖API
讀寫鎖的API看上去與自旋鎖很象,只是讀者和寫者需要不同的獲得和釋放鎖的API。
rwlock_init(x)1
該宏用于動態初始化讀寫鎖x。
DEFINE_RWLOCK(x)1
該宏聲明一個讀寫鎖并對其進行初始化。它用于靜態初始化。
RW_LOCK_UNLOCKED1
它用于靜態初始化一個讀寫鎖。
DEFINE_RWLOCK(x)等同于rwlock_t x = RW_LOCK_UNLOCKED
read_trylock(lock)1
讀者用它來盡力獲得讀寫鎖lock,如果能夠立即獲得讀寫鎖,它就獲得鎖并返回真,否則不能獲得鎖,返回假。無論是否能夠獲得鎖,它都將立即返回,絕不自旋在那里。
write_trylock(lock)1
寫者用它來盡力獲得讀寫鎖lock,如果能夠立即獲得讀寫鎖,它就獲得鎖并返回真,否則不能獲得鎖,返回假。無論是否能夠獲得鎖,它都將立即返回,絕不自旋在那里。
read_lock(lock)1
讀者要訪問被讀寫鎖lock保護的共享資源,需要使用該宏來得到讀寫鎖lock。如果能夠立即獲得,它將立即獲得讀寫鎖并返回,否則,將自旋在那里,直到獲得該讀寫鎖。
write_lock(lock)1
寫者要想訪問被讀寫鎖lock保護的共享資源,需要使用該宏來得到讀寫鎖lock。如果能夠立即獲得,它將立即獲得讀寫鎖并返回,否則,將自旋在那里,直到獲得該讀寫鎖。
read_lock_irqsave(lock, flags)1
讀者也可以使用該宏來獲得讀寫鎖,與read_lock不同的是,該宏還同時把標志寄存器的值保存到了變量flags中,并失效了本地中斷。
write_lock_irqsave(lock, flags)1
寫者可以用它來獲得讀寫鎖,與write_lock不同的是,該宏還同時把標志寄存器的值保存到了變量flags中,并失效了本地中斷。
read_lock_irq(lock)1
讀者也可以用它來獲得讀寫鎖,與read_lock不同的是,該宏還同時失效了本地中斷。該宏與read_lock_irqsave的不同之處是,它沒有保存標志寄存器。
write_lock_irq(lock)1
寫者也可以用它來獲得鎖,與write_lock不同的是,該宏還同時失效了本地中斷。該宏與write_lock_irqsave的不同之處是,它沒有保存標志寄存器。
read_lock_bh(lock)1
讀者也可以用它來獲得讀寫鎖,與與read_lock不同的是,該宏還同時失效了本地的軟中斷。
write_lock_bh(lock)1
寫者也可以用它來獲得讀寫鎖,與write_lock不同的是,該宏還同時失效了本地的軟中斷。
read_unlock(lock)1
讀者使用該宏來釋放讀寫鎖lock。它必須與read_lock配對使用。
write_unlock(lock)1
寫者使用該宏來釋放讀寫鎖lock。它必須與write_lock配對使用。
read_unlock_irqrestore(lock, flags)1
讀者也可以使用該宏來釋放讀寫鎖,與read_unlock不同的是,該宏還同時把標志寄存器的值恢復為變量flags的值。它必須與read_lock_irqsave配對使用。
write_unlock_irqrestore(lock, flags)1
寫者也可以使用該宏來釋放讀寫鎖,與write_unlock不同的是,該宏還同時把標志寄存器的值恢復為變量flags的值,并使能本地中斷。它必須與write_lock_irqsave配對使用。
read_unlock_irq(lock)1
讀者也可以使用該宏來釋放讀寫鎖,與read_unlock不同的是,該宏還同時使能本地中斷。它必須與read_lock_irq配對使用。
write_unlock_irq(lock)1
寫者也可以使用該宏來釋放讀寫鎖,與write_unlock不同的是,該宏還同時使能本地中斷。它必須與write_lock_irq配對使用。
read_unlock_bh(lock)1
讀者也可以使用該宏來釋放讀寫鎖,與read_unlock不同的是,該宏還同時使能本地軟中斷。它必須與read_lock_bh配對使用。
write_unlock_bh(lock)1
寫者也可以使用該宏來釋放讀寫鎖,與write_unlock不同的是,該宏還同時使能本地軟中斷。它必須與write_lock_bh配對使用。
讀寫鎖的獲得和釋放鎖的方法也有許多版本,具體用哪個與自旋鎖一樣,因此參考自旋鎖部分就可以了。只是需要區分讀者與寫者,讀者要用讀者版本,而寫者必須用寫者版本。
RCU(Read-Copy Update)
RCU簡介
RCU(Read-Copy Update),顧名思義就是讀-拷貝修改,它是基于其原理命名的。對于被RCU保護的共享數據結構,讀者不需要獲得任何鎖就可以訪問它,但寫者在訪問它時首先拷貝一個副本,然后對副本進行修改,最后使用一個回調(callback)機制在適當的時機把指向原來數據的指針重新指向新的被修改的數據。這個時機就是所有引用該數據的CPU都退出對共享數據的操作。
因此RCU實際上是一種改進的rwlock,讀者幾乎沒有什么同步開銷,它不需要鎖,不使用原子指令,而且在除alpha的所有架構上也不需要內存柵(Memory Barrier),因此不會導致鎖競爭,內存延遲以及流水線停滯。不需要鎖也使得使用更容易,因為死鎖問題就不需要考慮了。寫者的同步開銷比較大,它需要延遲數據結構的釋放,復制被修改的數據結構,它也必須使用某種鎖機制同步并行的其它寫者的修改操作。讀者必須提供一個信號給寫者以便寫者能夠確定數據可以被安全地釋放或修改的時機。有一個專門的垃圾收集器來探測讀者的信號,一旦所有的讀者都已經發送信號告知它們都不在使用被RCU保護的數據結構,垃圾收集器就調用回調函數完成最后的數據釋放或修改操作。 RCU與rwlock的不同之處是:它既允許多個讀者同時訪問被保護的數據,又允許多個讀者和多個寫者同時訪問被保護的數據(注意:是否可以有多個寫者并行訪問取決于寫者之間使用的同步機制),讀者沒有任何同步開銷,而寫者的同步開銷則取決于使用的寫者間同步機制。但RCU不能替代rwlock,因為如果寫比較多時,對讀者的性能提高不能彌補寫者導致的損失。
讀者在訪問被RCU保護的共享數據期間不能被阻塞,這是RCU機制得以實現的一個基本前提,也就說當讀者在引用被RCU保護的共享數據期間,讀者所在的CPU不能發生上下文切換,spinlock和rwlock都需要這樣的前提。寫者在訪問被RCU保護的共享數據時不需要和讀者競爭任何鎖,只有在有多于一個寫者的情況下需要獲得某種鎖以與其他寫者同步。寫者修改數據前首先拷貝一個被修改元素的副本,然后在副本上進行修改,修改完畢后它向垃圾回收器注冊一個回調函數以便在適當的時機執行真正的修改操作。等待適當時機的這一時期稱為寬限期(grace period),而CPU發生了上下文切換稱為經歷一個quiescent state,grace period就是所有CPU都經歷一次quiescent state所需要的等待的時間。垃圾收集器就是在grace period之后調用寫者注冊的回調函數來完成真正的數據修改或數據釋放操作的。
以下以鏈表元素刪除為例詳細說明這一過程。
寫者要從鏈表中刪除元素 B,它首先遍歷該鏈表得到指向元素 B 的指針,然后修改元素 B 的前一個元素的 next 指針指向元素 B 的 next 指針指向的元素C,修改元素 B 的 next 指針指向的元素 C 的 prep 指針指向元素 B 的 prep指針指向的元素 A,在這期間可能有讀者訪問該鏈表,修改指針指向的操作是原子的,所以不需要同步,而元素 B 的指針并沒有去修改,因為讀者可能正在使用 B 元素來得到下一個或前一個元素。寫者完成這些操作后注冊一個回調函數以便在 grace period 之后刪除元素 B,然后就認為已經完成刪除操作。垃圾收集器在檢測到所有的CPU不在引用該鏈表后,即所有的 CPU 已經經歷了 quiescent state,grace period 已經過去后,就調用剛才寫者注冊的回調函數刪除了元素 B。
使用 RCU 進行鏈表刪除操作
RCU的API
rcu_read_lock()1
讀者在讀取由RCU保護的共享數據時使用該函數標記它進入讀端臨界區。
rcu_read_unlock()1
該函數與rcu_read_lock配對使用,用以標記讀者退出讀端臨界區。
夾在這兩個函數之間的代碼區稱為”讀端臨界區”(read-side critical section)。讀端臨界區可以嵌套,如圖3,臨界區2被嵌套在臨界區1內。
嵌套讀端臨界區示例
那么在讀端臨界區發生了什么?要回答這個問題需要搞清楚rcu_read_lock和rcu_read_unlock做了什么操作,實際上即關閉內核搶占和打開內核搶占
static inline void __rcu_read_lock(void)
{
preempt_disable();
}
static inline void __rcu_read_unlock(void)
{preempt_enable();}
即在讀端臨界區中時內核是禁止搶占的。
那么這時是否度過寬限期(Grace Period)的判斷就比較簡單:每個CPU都經過一次搶占。因為發生搶占,就說明不在rcu_read_lock和rcu_read_unlock之間,必然已經完成訪問或者還未開始訪問。
synchronize_rcu()1
該函數由RCU寫端調用,它將阻塞寫者,直到經過grace period后,即所有的讀者已經完成讀端臨界區,寫者才可以繼續下一步操作。如果有多個RCU寫端調用該函數,他們將在一個grace period之后全部被喚醒。注意,該函數在2.6.11及以前的2.6內核版本中為synchronize_kernel,只是在2.6.12才更名為synchronize_rcu,但在2.6.12中也提供了synchronize_kernel和一個新的函數synchronize_sched,因為以前有很多內核開發者使用synchronize_kernel用于等待所有CPU都退出不可搶占區,而在RCU設計時該函數只是用于等待所有CPU都退出讀端臨界區,它可能會隨著RCU實現的修改而發生語意變化,因此為了預先防止這種情況發生,在新的修改中增加了專門的用于其它內核用戶的synchronize_sched函數和只用于RCU使用的synchronize_rcu,現在建議非RCU內核代碼部分不使用synchronize_kernel而使用synchronize_sched,RCU代碼部分則使用synchronize_rcu,synchronize_kernel之所以存在是為了保證代碼兼容性。
synchronize_kernel()1
其他非RCU的內核代碼使用該函數來等待所有CPU處在可搶占狀態,目前功能等同于synchronize_rcu,但現在已經不建議使用,而使用synchronize_sched。
synchronize_sched()1
該函數用于等待所有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);
};123456
函數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))12
函數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);
})12345
該宏用于在RCU讀端臨界區獲得一個RCU保護的指針,該指針可以在以后安全地引用,內存柵只在alpha架構上才使用。
除了這些API,RCU還增加了鏈表操作的RCU版本,因為對于RCU,對共享數據的操作必須保證能夠被沒有使用同步機制的讀者看到,所以內存柵是非常必要的。
static inline void list_add_rcu(struct list_head *new, struct list_head *head)1
該函數把鏈表項new插入到RCU保護的鏈表head的開頭。使用內存柵保證了在引用這個新插入的鏈表項之前,新鏈表項的鏈接指針的修改對所有讀者是可見的。
static inline void list_add_tail_rcu(struct list_head *new,
struct list_head *head)12
該函數類似于list_add_rcu,它將把新的鏈表項new添加到被RCU保護的鏈表的末尾。
static inline void list_del_rcu(struct list_head *entry)1
該函數從RCU保護的鏈表中移走指定的鏈表項entry,并且把entry的prev指針設置為LIST_POISON2,但是并沒有把entry的next指針設置為LIST_POISON1,因為該指針可能仍然在被讀者用于便利該鏈表。
static inline void list_replace_rcu(struct list_head *old, struct list_head *new)1
該函數是RCU新添加的函數,并不存在非RCU版本。它使用新的鏈表項new取代舊的鏈表項old,內存柵保證在引用新的鏈表項之前,它的鏈接指針的修正對所有讀者可見。
list_for_each_rcu(pos, head)1
該宏用于遍歷由RCU保護的鏈表head,只要在讀端臨界區使用該函數,它就可以安全地和其它_rcu鏈表操作函數(如list_add_rcu)并發運行。
list_for_each_safe_rcu(pos, n, head)1
該宏類似于list_for_each_rcu,但不同之處在于它允許安全地刪除當前鏈表項pos。
list_for_each_entry_rcu(pos, head, member)1
該宏類似于list_for_each_rcu,不同之處在于它用于遍歷指定類型的數據結構鏈表,當前鏈表項pos為一包含struct list_head結構的特定的數據結構。
list_for_each_continue_rcu(pos, head)1
該宏用于在退出點之后繼續遍歷由RCU保護的鏈表head。
static inline void hlist_del_rcu(struct hlist_node *n)1
它從由RCU保護的哈希鏈表中移走鏈表項n,并設置n的ppre指針為LIST_POISON2,但并沒有設置next為LIST_POISON1,因為該指針可能被讀者使用用于遍利鏈表。
static inline void hlist_add_head_rcu(struct hlist_node *n,
struct hlist_head *h)12
該函數用于把鏈表項n插入到被RCU保護的哈希鏈表的開頭,但同時允許讀者對該哈希鏈表的遍歷。內存柵確保在引用新鏈表項之前,它的指針修正對所有讀者可見。
hlist_for_each_rcu(pos, head)1
該宏用于遍歷由RCU保護的哈希鏈表head,只要在讀端臨界區使用該函數,它就可以安全地和其它_rcu哈希鏈表操作函數(如hlist_add_rcu)并發運行。
hlist_for_each_entry_rcu(tpos, pos, head, member)1
類似于hlist_for_each_rcu,不同之處在于它用于遍歷指定類型的數據結構哈希鏈表,當前鏈表項pos為一包含struct list_head結構的特定的數據結構。
下面部分將就 RCU 的幾種典型應用情況詳細講解。
1. 只有增加和刪除的鏈表操作
在這種應用情況下,絕大部分是對鏈表的遍歷,即讀操作,而很少出現的寫操作只有增加或刪除鏈表項,并沒有對鏈表項的修改操作,這種情況使用RCU非常容易,從rwlock轉換成RCU非常自然。路由表的維護就是這種情況的典型應用,對路由表的操作,絕大部分是路由表查詢,而對路由表的寫操作也僅僅是增加或刪除,因此使用RCU替換原來的rwlock順理成章。系統調用審計也是這樣的情況。
這是一段使用rwlock的系統調用審計部分的讀端代碼:
static enum audit_state audit_filter_task(struct task_struct *tsk)
{
struct audit_entry *e;
enum audit_state state;
read_lock(&auditsc_lock);
/* Note: audit_netlink_sem held by caller. */
list_for_each_entry(e, &audit_tsklist, list) {
if (audit_filter_rules(tsk, &e->rule, NULL, &state)) {
read_unlock(&auditsc_lock);
return state;
}
}
read_unlock(&auditsc_lock);
return AUDIT_BUILD_CONTEXT;
}
使用RCU后將變成:
static enum audit_state audit_filter_task(struct task_struct *tsk)
{
struct audit_entry *e;
enum audit_state state;
rcu_read_lock();
/* Note: audit_netlink_sem held by caller. */
list_for_each_entry_rcu(e, &audit_tsklist, list) {
if (audit_filter_rules(tsk, &e->rule, NULL, &state)) {
rcu_read_unlock();
return state;
}
}
rcu_read_unlock();
return AUDIT_BUILD_CONTEXT;
}
這種轉換非常直接,使用rcu_read_lock和rcu_read_unlock分別替換read_lock和read_unlock,鏈表遍歷函數使用_rcu版本替換就可以了。
使用rwlock的寫端代碼:
static inline int audit_del_rule(struct audit_rule *rule,
struct list_head *list)
{
struct audit_entry *e;
write_lock(&auditsc_lock);
list_for_each_entry(e, list, list) {
if (!audit_compare_rule(rule, &e->rule)) {
list_del(&e->list);
write_unlock(&auditsc_lock);
return 0;
}
}
write_unlock(&auditsc_lock);
return -EFAULT; /* No matching rule */
}
static inline int audit_add_rule(struct audit_entry *entry,
struct list_head *list)
{
write_lock(&auditsc_lock);
if (entry->rule.flags & AUDIT_PREPEND) {
entry->rule.flags &= ~AUDIT_PREPEND;
list_add(&entry->list, list);
} else {
list_add_tail(&entry->list, list);
}
write_unlock(&auditsc_lock);
return 0;
}
使用RCU后寫端代碼變成為
static inline int audit_del_rule(struct audit_rule *rule,
struct list_head *list)
{
struct audit_entry *e;
/* Do not use the _rcu iterator here, since this is the only
* deletion routine. */
list_for_each_entry(e, list, list) {
if (!audit_compare_rule(rule, &e->rule)) {
list_del_rcu(&e->list);
call_rcu(&e->rcu, audit_free_rule, e);
return 0;
}
}
return -EFAULT; /* No matching rule */
}
static inline int audit_add_rule(struct audit_entry *entry,
struct list_head *list)
{
if (entry->rule.flags & AUDIT_PREPEND) {
entry->rule.flags &= ~AUDIT_PREPEND;
list_add_rcu(&entry->list, list);
} else {
list_add_tail_rcu(&entry->list, list);
}
return 0;
}
對于鏈表刪除操作,list_del替換為list_del_rcu和call_rcu,這是因為被刪除的鏈表項可能還在被別的讀者引用,所以不能立即刪除,必須等到所有讀者經歷一個quiescent state才可以刪除。另外,list_for_each_entry并沒有被替換為list_for_each_entry_rcu,這是因為,只有一個寫者在做鏈表刪除操作,因此沒有必要使用_rcu版本。
通常情況下,write_lock和write_unlock應當分別替換成spin_lock和spin_unlock,但是對于只是對鏈表進行增加和刪除操作而且只有一個寫者的寫端,在使用了_rcu版本的鏈表操作API后,rwlock可以完全消除,不需要spinlock來同步讀者的訪問。對于上面的例子,由于已經有audit_netlink_sem被調用者保持,所以spinlock就沒有必要了。
這種情況允許修改結果延后一定時間才可見,而且寫者對鏈表僅僅做增加和刪除操作,所以轉換成使用RCU非常容易。
2.寫端需要對鏈表條目進行修改操作
如果寫者需要對鏈表條目進行修改,那么就需要首先拷貝要修改的條目,然后修改條目的拷貝,等修改完畢后,再使用條目拷貝取代要修改的條目,要修改條目將被在經歷一個grace period后安全刪除。
對于系統調用審計代碼,并沒有這種情況。這里假設有修改的情況,那么使用rwlock的修改代碼應當如下:
static inline int audit_upd_rule(struct audit_rule *rule,
struct list_head *list,
__u32 newaction,
__u32 newfield_count)
{
struct audit_entry *e;
struct audit_newentry *ne;
write_lock(&auditsc_lock);
/* Note: audit_netlink_sem held by caller. */
list_for_each_entry(e, list, list) {
if (!audit_compare_rule(rule, &e->rule)) {
e->rule.action = newaction;
e->rule.file_count = newfield_count;
write_unlock(&auditsc_lock);
return 0;
}
}
write_unlock(&auditsc_lock);
return -EFAULT; /* No matching rule */
}
如果使用RCU,修改代碼應當為;
static inline int audit_upd_rule(struct audit_rule *rule,
struct list_head *list,
__u32 newaction,
__u32 newfield_count)
{
struct audit_entry *e;
struct audit_newentry *ne;
list_for_each_entry(e, list, list) {
if (!audit_compare_rule(rule, &e->rule)) {
ne = kmalloc(sizeof(*entry), GFP_ATOMIC);
if (ne == NULL)
return -ENOMEM;
audit_copy_rule(&ne->rule, &e->rule);
ne->rule.action = newaction;
ne->rule.file_count = newfield_count;
list_replace_rcu(e, ne);
call_rcu(&e->rcu, audit_free_rule, e);
return 0;
}
}
return -EFAULT; /* No matching rule */
}
3.修改操作立即可見
前面兩種情況,讀者能夠容忍修改可以在一段時間后看到,也就說讀者在修改后某一時間段內,仍然看到的是原來的數據。在很多情況下,讀者不能容忍看到舊的數據,這種情況下,需要使用一些新措施,如System V IPC,它在每一個鏈表條目中增加了一個deleted字段,標記該字段是否刪除,如果刪除了,就設置為真,否則設置為假,當代碼在遍歷鏈表時,核對每一個條目的deleted字段,如果為真,就認為它是不存在的。
還是以系統調用審計代碼為例,如果它不能容忍舊數據,那么,讀端代碼應該修改為:
static enum audit_state audit_filter_task(struct task_struct *tsk)
{
struct audit_entry *e;
enum audit_state state;
rcu_read_lock();
list_for_each_entry_rcu(e, &audit_tsklist, list) {
if (audit_filter_rules(tsk, &e->rule, NULL, &state)) {
spin_lock(&e->lock);
if (e->deleted) {
spin_unlock(&e->lock);
rcu_read_unlock();
return AUDIT_BUILD_CONTEXT;
}
rcu_read_unlock();
return state;
}
}
rcu_read_unlock();
return AUDIT_BUILD_CONTEXT;
}
注意,對于這種情況,每一個鏈表條目都需要一個spinlock保護,因為刪除操作將修改條目的deleted標志。此外,該函數如果搜索到條目,返回時應當保持該條目的鎖,因為只有這樣,才能看到新的修改的數據,否則,仍然可能看到舊的數據。
寫端的刪除操作將變成:
static inline int audit_del_rule(struct audit_rule *rule,
struct list_head *list)
{
struct audit_entry *e;
/* Do not use the _rcu iterator here, since this is the only
* deletion routine. */
list_for_each_entry(e, list, list) {
if (!audit_compare_rule(rule, &e->rule)) {
spin_lock(&e->lock);
list_del_rcu(&e->list);
e->deleted = 1;
spin_unlock(&e->lock);
call_rcu(&e->rcu, audit_free_rule, e);
return 0;
}
}
return -EFAULT; /* No matching rule *
刪除條目時,需要標記該條目為已刪除。這樣讀者就可以通過該標志立即得知條目是否已經刪除.
RCU是2.6內核引入的新的鎖機制,在絕大部分為讀而只有極少部分為寫的情況下,它是非常高效的,因此在路由表維護、系統調用審計、SELinux的AVC、dcache和IPC等代碼部分中,使用它來取代rwlock來獲得更高的性能。但是,它也有缺點,延后的刪除或釋放將占用一些內存,尤其是對嵌入式系統,這可能是非常昂貴的內存開銷。此外,寫者的開銷比較大,尤其是對于那些無法容忍舊數據的情況以及不只一個寫者的情況,寫者需要spinlock或其他的鎖機制來與其他寫者同步。
順序鎖(seqlock)
順序鎖簡介
順序鎖也是對讀寫鎖的一種優化,對于順序鎖,讀者絕不會被寫者阻塞,也就說,讀者可以在寫者對被順序鎖保護的共享資源進行寫操作時仍然可以繼續讀,而不必等待寫者完成寫操作,寫者也不需要等待所有讀者完成讀操作才去進行寫操作。但是,寫者與寫者之間仍然是互斥的,即如果有寫者在進行寫操作,其他寫者必須自旋在那里,直到寫者釋放了順序鎖。
這種鎖有一個限制,它必須要求被保護的共享資源不含有指針,因為寫者可能使得指針失效,但讀者如果正要訪問該指針,將導致OOPs。
如果讀者在讀操作期間,寫者已經發生了寫操作,那么,讀者必須重新讀取數據,以便確保得到的數據是完整的。
這種鎖對于讀寫同時進行的概率比較小的情況,性能是非常好的,而且它允許讀寫同時進行,因而更大地提高了并發性。
順序鎖的API
void write_seqlock(seqlock_t *sl);1
寫者在訪問被順序鎖s1保護的共享資源前需要調用該函數來獲得順序鎖s1。它實際功能上等同于spin_lock,只是增加了一個對順序鎖順序號的加1操作,以便讀者能夠檢查出是否在讀期間有寫者訪問過。
void write_sequnlock(seqlock_t *sl);1
寫者在訪問完被順序鎖s1保護的共享資源后需要調用該函數來釋放順序鎖s1。它實際功能上等同于spin_unlock,只是增加了一個對順序鎖順序號的加1操作,以便讀者能夠檢查出是否在讀期間有寫者訪問過。
寫者使用順序鎖的模式如下:
write_seqlock(&seqlock_a);
//寫操作代碼塊
…
write_sequnlock(&seqlock_a);
因此,對寫者而言,它的使用與spinlock相同。
int write_tryseqlock(seqlock_t *sl);1
寫者在訪問被順序鎖s1保護的共享資源前也可以調用該函數來獲得順序鎖s1。它實際功能上等同于spin_trylock,只是如果成功獲得鎖后,該函數增加了一個對順序鎖順序號的加1操作,以便讀者能夠檢查出是否在讀期間有寫者訪問過。
unsigned read_seqbegin(const seqlock_t *sl);1
讀者在對被順序鎖s1保護的共享資源進行訪問前需要調用該函數。讀者實際沒有任何得到鎖和釋放鎖的開銷,該函數只是返回順序鎖s1的當前順序號。
int read_seqretry(const seqlock_t *sl, unsigned iv);1
讀者在訪問完被順序鎖s1保護的共享資源后需要調用該函數來檢查,在讀訪問期間是否有寫者訪問了該共享資源,如果是,讀者就需要重新進行讀操作,否則,讀者成功完成了讀操作。
因此,讀者使用順序鎖的模式如下:
do {
seqnum = read_seqbegin(&seqlock_a);
//讀操作代碼塊
...
} while (read_seqretry(&seqlock_a, seqnum));
write_seqlock_irqsave(lock, flags)1
寫者也可以用該宏來獲得順序鎖lock,與write_seqlock不同的是,該宏同時還把標志寄存器的值保存到變量flags中,并且失效了本地中斷。
write_seqlock_irq(lock)1
寫者也可以用該宏來獲得順序鎖lock,與write_seqlock不同的是,該宏同時還失效了本地中斷。與write_seqlock_irqsave不同的是,該宏不保存標志寄存器。
write_seqlock_bh(lock)1
寫者也可以用該宏來獲得順序鎖lock,與write_seqlock不同的是,該宏同時還失效了本地軟中斷。
write_sequnlock_irqrestore(lock, flags)1
寫者也可以用該宏來釋放順序鎖lock,與write_sequnlock不同的是,該宏同時還把標志寄存器的值恢復為變量flags的值。它必須與write_seqlock_irqsave配對使用。
write_sequnlock_irq(lock)1
寫者也可以用該宏來釋放順序鎖lock,與write_sequnlock不同的是,該宏同時還使能本地中斷。它必須與write_seqlock_irq配對使用。
write_sequnlock_bh(lock)1
寫者也可以用該宏來釋放順序鎖lock,與write_sequnlock不同的是,該宏同時還使能本地軟中斷。它必須與write_seqlock_bh配對使用。
read_seqbegin_irqsave(lock, flags)1
讀者在對被順序鎖lock保護的共享資源進行訪問前也可以使用該宏來獲得順序鎖lock的當前順序號,與read_seqbegin不同的是,它同時還把標志寄存器的值保存到變量flags中,并且失效了本地中斷。注意,它必須與read_seqretry_irqrestore配對使用。
read_seqretry_irqrestore(lock, iv, flags)1
讀者在訪問完被順序鎖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);1
讀者在對被順序計數保護的共享資源進行讀訪問前需要使用該函數來獲得當前的順序號。
int read_seqcount_retry(const seqcount_t *s, unsigned iv);1
讀者在訪問完被順序計數s保護的共享資源后需要調用該函數來檢查,在讀訪問期間是否有寫者訪問了該共享資源,如果是,讀者就需要重新進行讀操作,否則,讀者成功完成了讀操作。
因此,讀者使用順序計數的模式如下:
do {
seqnum = read_seqbegin_count(&seqcount_a);
//讀操作代碼塊
...
} while (read_seqretry(&seqcount_a, seqnum));
void write_seqcount_begin(seqcount_t *s);1
寫者在訪問被順序計數保護的共享資源前需要調用該函數來對順序計數的順序號加1,以便讀者能夠檢查出是否在讀期間有寫者訪問過。
void write_seqcount_end(seqcount_t *s);1
寫者在訪問完被順序計數保護的共享資源后需要調用該函數來對順序計數的順序號加1,以便讀者能夠檢查出是否在讀期間有寫者訪問過。
寫者使用順序計數的模式為:
write_seqcount_begin(&seqcount_a);
//寫操作代碼塊
…
write_seqcount_end(&seqcount_a);
需要特別提醒,順序計數的使用必須非常謹慎,只有確定在訪問共享數據時已經保持了互斥鎖才可以使用。
內核在寫和讀netdevice的名字時分別使用dev_change_name和netdev_get_name中使用了該方法。而dev_change_name由dev_ifsioc調用,其如下所示使用互斥鎖進行了保護。
rtnl_lock(); //內部即互斥鎖
ret = dev_ifsioc(net, &ifr, cmd)
rtnl_unlock();123
#define local_irq_enable() do { raw_local_irq_enable(); } while (0)
#define local_irq_disable() do { raw_local_irq_disable(); } while (0)
#define raw_local_irq_disable() arch_local_irq_disable()
#define raw_local_irq_enable() arch_local_irq_enable()
可以看到這個與體系結構有關
禁止內核搶占就比較簡單了,就是防止當前進程不會突然被另一個進程搶占。在Linux的實現就是preempt_disable()和preempt_enable()函數
#define preempt_disable()
do {
//增加preempt_count
inc_preempt_count();
//保證先加了preempt_count才進行以后的操作
barrier();
} while (0)
#define preempt_enable()
do {
preempt_enable_no_resched();
barrier();
//檢查當前進程是否可搶占
preempt_check_resched();
} while
不管是禁止中斷還是禁止內核搶占,都是為了提供內核同步,但是他們都沒有提供任何保護機制來防止其它處理器的并發訪問。Linux支持多處理器,因此,內核代碼一般都需要獲取某種鎖,防止來自其他處理器對共享數據的并發訪問,而禁止中斷提供保護機制,這是防止來自其他中斷處理程序的并發訪問。
前面說的都是概念,現在我們來討論幾個問題
1.在單處理器條件下,為什么禁止中斷就可以禁止內核搶占?
首先來回顧一下內核搶占發生在哪些時候:
1. 在中斷返回內核空間的時候,這個沒什么好說的,跟中斷密切相關,沒了中斷就不會發生
2. 內核顯式調用schedule()(可搶占或阻塞)
我們先搞清楚一件事,就是我們說禁止中斷可以禁止內核搶占只是說禁止任何意外的搶占,如果進程自己要調用schedule函數,那誰也攔不住,事實上調用schedule這個函數本來就要禁止中斷,所以剩下的就是考慮創建或者喚醒一個更高優先級的進程,或者調用信號量、完成量,所有的這些情況都要通過try_to_wake_up函數喚醒另一個進程,但是這個函數真正干的事只是設置了一下need_resched這個函數,并沒有真的調用schedule函數,調用是在系統調用返回用戶空間的時候進行的,所以跟內核搶占也沒啥關系,所以從這些方面來說,禁止中斷是可以禁止內核搶占的
2.自旋鎖關中斷后,為什么要再禁止搶占?
假設有這么個情況:
1)CPU-1在進程A的上下文調用了spin_lock_irqsave;
2)CPU-2調用wake_up_process喚醒了CPU-1上的進程B,由于進程B的優先級高于進程A,進程A的TIF_NEED_RESCHED標記被設置。(CPU-2還會用IPI通知CPU-1進行resched,但是CPU-1禁用了中斷而不會響應);
3)CPU-1調用了某某函數,這個函數包含了preempt_disable和preempt_enable(沒有規定關中斷的情況下不能調用這樣的函數吧~);
那么,如果spin_lock_irqsave沒有preempt_disable,第3步中的preempt_enable將觸發preempt_check_resched,從而讓進程B搶占掉進程A。
總之就是只有關了搶占,才能保證在臨界區成對出現的preempt_disable()/preempt_enable()(preempt_enable()也是一個潛在的主動調度的測試點)不會造成傷害。不然這種代碼就不能放在臨界區中了。
Linux可以使用互斥信號量來表示互斥鎖,那就是通過宏DECLARE_MUTEX來定義一個互斥信號量,因為DECLARE_MUTEX這個宏,Marcin Slusarz在08年提交的了一個patch,郵件地址為:https://lkml.org/lkml/2008/10/26/74,Marcin Slusarz認為DECLARE_MUTEX宏會誤導開發者,所以建議將DECLARE_MUTEX修改成DEFINE_SEMAPHORE,這個提議最終被內核社區所接受,在2.6.36版本后的內核就沒有DECLARE_MUTEX這個宏了,取而代之的是DEFINE_SEMAPHORE宏,在后來同互斥信號量相關的init_MUTEX、init_MUTEX_LOCKED也從文件中移除了。
雖然可以使用信號量來表示互斥鎖,但是互斥鎖其實是存在的,只是前面的宏DECLARE_MUTEX因為會引起歧義,所以修改成了DEFINE_SEMAPHORE,mutex在2.6.16版本就融入到了主內核中了,使用mutex需要包含頭文件.
禁止中斷和禁止搶占的簡介
禁止中斷指的是Linux內核提供了一組接口用于操作機器上的中斷狀態。這些接口為我們提供了能夠禁止當前處理器的中斷系統,或者屏蔽掉整個機器的一條中斷線的能力。通過禁止中斷,可以確保某個中斷處理程序不會搶占當前的代碼。控制中斷系統在Linux的實現有很多,以local_irq_disable()和 local_irq_enable()函數為例
-
內核
+關注
關注
3文章
1372瀏覽量
40278 -
cpu
+關注
關注
68文章
10854瀏覽量
211583 -
Linux
+關注
關注
87文章
11292瀏覽量
209329
發布評論請先 登錄
相關推薦
評論