Linux內(nèi)核中的各種鎖
在LInux操作系統(tǒng)里,同一時(shí)間可能有多個(gè)內(nèi)核執(zhí)行流在執(zhí)行,因此內(nèi)核其實(shí)象多進(jìn)程多線程編程一樣也需要一些同步機(jī)制來(lái)同步各執(zhí)行單元對(duì)共享數(shù)據(jù)的訪問(wèn)。尤其是在多處理器系統(tǒng)上,更需要一些同步機(jī)制來(lái)同步不同處理器上的執(zhí)行單元對(duì)共享的數(shù)據(jù)的訪問(wèn)。
linux內(nèi)核
在主流的Linux內(nèi)核中包含了幾乎所有現(xiàn)代的操作系統(tǒng)具有的同步機(jī)制,這些同步機(jī)制包括:原子操作、信號(hào)量(semaphore)、讀寫信號(hào)量(rw_semaphore)、spinlock、BKL(Big Kernel Lock)、rwlock ,RCU(在開(kāi)發(fā)內(nèi)核2.5.43中引入該技術(shù)的并正式包含在2.6內(nèi)核中)和seqlock(只包含在2.6以后內(nèi)核中)。
首先明確鎖的引入不可避免的引起性能的損失,研究表明隨著計(jì)算機(jī)硬件的快速發(fā)展,獲得這種鎖的開(kāi)銷相對(duì)于CPU的速度在成倍地增加,原因很簡(jiǎn)單,CPU的速度與訪問(wèn)內(nèi)存的速度差距越來(lái)越大,而這種鎖使用了原子操作指令,它需要原子地訪問(wèn)內(nèi)存,也就說(shuō)獲得鎖的開(kāi)銷與訪存速度相關(guān),另外在大部分非x86架構(gòu)上獲取鎖使用了內(nèi)存柵(Memory Barrier),這會(huì)導(dǎo)致處理器流水線停滯或刷新,因此它的開(kāi)銷相對(duì)于CPU速度而言就影響
越來(lái)越大。
因此對(duì)于以上每種鎖都要明確其特定的應(yīng)用場(chǎng)合,使用不當(dāng)反而會(huì)影響性能甚至錯(cuò)誤。
原子操作
原子操作簡(jiǎn)介
什么是原子性,就是不可再分,該操作絕不會(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)?a href="http://wenjunhu.com/v/tag/1743/" target="_blank">C語(yǔ)言并不能實(shí)現(xiàn)這樣的操作。
原子操作主要用于實(shí)現(xiàn)資源計(jì)數(shù),很多引用計(jì)數(shù)(refcnt)就是通過(guò)原子操作實(shí)現(xiàn)的。
原子類型定義如下:
typedef struct { volatile int counter; } atomic_t;1
volatile修飾字段告訴gcc不要對(duì)該類型的數(shù)據(jù)做優(yōu)化處理,對(duì)它的訪問(wèn)都是對(duì)內(nèi)存的訪問(wèn),而不是對(duì)寄存器的訪問(wèn)。
api如下:
atomic_read(atomic_t * v);1
該函數(shù)對(duì)原子類型的變量進(jìn)行原子讀操作,它返回原子類型的變量v的值。
atomic_set(atomic_t * v, int i);1
該函數(shù)設(shè)置原子類型的變量v的值為i。
void atomic_add(int i, atomic_t *v);1
該函數(shù)給原子類型的變量v增加值i。
atomic_sub(int i, atomic_t *v);1
該函數(shù)從原子類型的變量v中減去i。
int atomic_sub_and_test(int i, atomic_t *v);1
該函數(shù)從原子類型的變量v中減去i,并判斷結(jié)果是否為0,如果為0,返回真,否則返回假。
void atomic_inc(atomic_t *v);1
該函數(shù)對(duì)原子類型變量v原子地增加1。
void atomic_dec(atomic_t *v);1
該函數(shù)對(duì)原子類型的變量v原子地減1。
int atomic_dec_and_test(atomic_t *v);1
該函數(shù)對(duì)原子類型的變量v原子地減1,并判斷結(jié)果是否為0,如果為0,返回真,否則返回假。
int atomic_inc_and_test(atomic_t *v);1
該函數(shù)對(duì)原子類型的變量v原子地增加1,并判斷結(jié)果是否為0,如果為0,返回真,否則返回假。
int atomic_add_negative(int i, atomic_t *v);1
該函數(shù)對(duì)原子類型的變量v原子地增加i,并判斷結(jié)果是否為負(fù)數(shù),如果是,返回真,否則返回假。
int atomic_add_return(int i, atomic_t *v);1
該函數(shù)對(duì)原子類型的變量v原子地增加i,并且返回指向v的指針。
int atomic_sub_return(int i, atomic_t *v);1
該函數(shù)從原子類型的變量v中減去i,并且返回指向v的指針。
int atomic_inc_return(atomic_t * v);1
該函數(shù)對(duì)原子類型的變量v原子地增加1并且返回指向v的指針。
int atomic_dec_return(atomic_t * v);1
該函數(shù)對(duì)原子類型的變量v原子地減1并且返回指向v的指針。
原子操作通常用于實(shí)現(xiàn)資源的引用計(jì)數(shù),在TCP/IP協(xié)議棧的IP碎片處理中,就使用了引用計(jì)數(shù),碎片隊(duì)列結(jié)構(gòu)struct ipq描述了一個(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))。
信號(hào)量(semaphore)
信號(hào)量簡(jiǎn)介
Linux內(nèi)核的信號(hào)量在概念和原理上與用戶態(tài)的System V的IPC機(jī)制信號(hào)量是一樣的,但是它絕不可能在內(nèi)核之外使用,因此它與System V的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介紹
DEFINE_MUTEX1
靜態(tài)定義和初始化一個(gè)互斥鎖. 1
void mutex_init(struct mutex *mutex);1
動(dòng)態(tài)初始化一個(gè)互斥鎖
void sema_init (struct semaphore *sem, int val);1
該函用于數(shù)初始化設(shè)置信號(hào)量的初值,它設(shè)置信號(hào)量sem的值為val。
void down(struct semaphore * sem);1
該函數(shù)用于獲得信號(hào)量sem,它會(huì)導(dǎo)致睡眠,因此不能在中斷上下文(包括IRQ上下文和softirq上下文)使用該函數(shù)。該函數(shù)首先判斷sem->count的值是否大于0,如果true則sem->count–,否者調(diào)用者將被掛起,直到別的任務(wù)釋放該信號(hào)量才能繼續(xù)運(yùn)行。
int down_interruptible(struct semaphore * sem);1
該函數(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);1
該函數(shù)試著獲得信號(hào)量sem,如果能夠立刻獲得,它就獲得該信號(hào)量并返回0,否則,表示不能獲得信號(hào)量sem,返回值為非0值。因此,它不會(huì)導(dǎo)致調(diào)用者睡眠,可以在中斷上下文使用。
void up(struct semaphore * sem);1
該函數(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ù)。
讀寫信號(hào)量(rw_semaphore)
讀寫信號(hào)量簡(jiǎn)介
讀寫信號(hào)量對(duì)訪問(wèn)者進(jìn)行了細(xì)分,或者為讀者,或者為寫者,讀者在保持讀寫信號(hào)量期間只能對(duì)該讀寫信號(hào)量保護(hù)的共享資源進(jìn)行讀訪問(wèn),如果一個(gè)任務(wù)除了需要讀,可能還需要寫,那么它必須被歸類為寫者,它在對(duì)共享資源訪問(wèn)之前必須先獲得寫者身份,寫者在發(fā)現(xiàn)自己不需要寫訪問(wèn)的情況下可以降級(jí)為讀者。讀寫信號(hào)量的訪問(wèn)規(guī)則:
讀寫信號(hào)量同時(shí)擁有的讀者數(shù)不受限制,也就說(shuō)可以有任意多個(gè)讀者同時(shí)擁有一個(gè)讀寫信號(hào)量。
如果一個(gè)讀寫信號(hào)量當(dāng)前沒(méi)有被寫者擁有并且也沒(méi)有寫者等待讀者釋放信號(hào)量,那么任何讀者都可以成功獲得該讀寫信號(hào)量;否則,讀者必須被掛起直到寫者釋放該信號(hào)量。
如果一個(gè)讀寫信號(hào)量當(dāng)前沒(méi)有被讀者或?qū)懻邠碛胁⑶乙矝](méi)有寫者等待該信號(hào)量,那么一個(gè)寫者可以成功獲得該讀寫信號(hào)量,否則寫者將被掛起,直到?jīng)]有任何訪問(wèn)者。
因此,寫者是排他性的,獨(dú)占性的。
讀寫信號(hào)量有兩種實(shí)現(xiàn),一種是通用的,不依賴于硬件架構(gòu),因此,增加新的架構(gòu)不需要重新實(shí)現(xiàn)它,但缺點(diǎn)是性能低,獲得和釋放讀寫信號(hào)量的開(kāi)銷大;另一種是架構(gòu)相關(guān)的,因此性能高,獲取和釋放讀寫信號(hào)量的開(kāi)銷小,但增加新的架構(gòu)需要重新實(shí)現(xiàn)。在內(nèi)核配置時(shí),可以通過(guò)選項(xiàng)去控制使用哪一種實(shí)現(xiàn)。
讀寫信號(hào)量api介紹
DECLARE_RWSEM(name)1
該宏聲明一個(gè)讀寫信號(hào)量name并對(duì)其進(jìn)行初始化。
void init_rwsem(struct rw_semaphore *sem);1
該函數(shù)對(duì)讀寫信號(hào)量sem進(jìn)行初始化。
void down_read(struct rw_semaphore *sem);1
讀者調(diào)用該函數(shù)來(lái)得到讀寫信號(hào)量sem。該函數(shù)會(huì)導(dǎo)致調(diào)用者睡眠,因此只能在進(jìn)程上下文使用。
int down_read_trylock(struct rw_semaphore *sem);1
該函數(shù)類似于down_read,只是它不會(huì)導(dǎo)致調(diào)用者睡眠。它盡力得到讀寫信號(hào)量sem,如果能夠立即得到,它就得到該讀寫信號(hào)量,并且返回1,否則表示不能立刻得到該信號(hào)量,返回0。因此,它也可以在中斷上下文使用。
void down_write(struct rw_semaphore *sem);1
寫者使用該函數(shù)來(lái)得到讀寫信號(hào)量sem,它也會(huì)導(dǎo)致調(diào)用者睡眠,因此只能在進(jìn)程上下文使用。
int down_write_trylock(struct rw_semaphore *sem);1
該函數(shù)類似于down_write,只是它不會(huì)導(dǎo)致調(diào)用者睡眠。該函數(shù)盡力得到讀寫信號(hào)量,如果能夠立刻獲得,就獲得該讀寫信號(hào)量并且返回1,否則表示無(wú)法立刻獲得,返回0。它可以在中斷上下文使用。
void up_read(struct rw_semaphore *sem);1
讀者使用該函數(shù)釋放讀寫信號(hào)量sem。它與down_read或down_read_trylock配對(duì)使用。如果down_read_trylock返回0,不需要調(diào)用up_read來(lái)釋放讀寫信號(hào)量,因?yàn)楦揪蜎](méi)有獲得信號(hào)量。
void up_write(struct rw_semaphore *sem);1
寫者調(diào)用該函數(shù)釋放信號(hào)量sem。它與down_write或down_write_trylock配對(duì)使用。如果down_write_trylock返回0,不需要調(diào)用up_write,因?yàn)榉祷?表示沒(méi)有獲得該讀寫信號(hào)量。
void downgrade_write(struct rw_semaphore *sem);1
該函數(shù)用于把寫者降級(jí)為讀者,這有時(shí)是必要的。因?yàn)閷懻呤桥潘缘?,因此在寫者保持讀寫信號(hào)量期間,任何讀者或?qū)懻叨紝o(wú)法訪問(wèn)該讀寫信號(hào)量保護(hù)的共享資源,對(duì)于那些當(dāng)前條件下不需要寫訪問(wèn)的寫者,降級(jí)為讀者將使得等待訪問(wèn)的讀者能夠立刻訪問(wèn),從而增加了并發(fā)性,提高了效率。
讀寫信號(hào)量適于在讀多寫少的情況下使用,在linux內(nèi)核中對(duì)進(jìn)程的內(nèi)存映像描述結(jié)構(gòu)的訪問(wèn)就使用了讀寫信號(hào)量進(jìn)行保護(hù)。在Linux中,每一個(gè)進(jìn)程都用一個(gè)類型為task_t或struct task_struct的結(jié)構(gòu)來(lái)描述,該結(jié)構(gòu)的類型為struct mm_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è)讀寫信號(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)存分配或釋放操作需要以寫者身份獲得讀寫信號(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)。
自旋鎖(spinlock)
自旋鎖簡(jiǎn)介
自旋鎖與互斥鎖有點(diǎn)類似,只是自旋鎖不會(huì)引起調(diào)用者睡眠,如果自旋鎖已經(jīng)被別的執(zhí)行單元保持,調(diào)用者就一直循環(huán)在那里看是否該自旋鎖的保持者已經(jīng)釋放了鎖,”自旋”一詞就是因此而得名。由于自旋鎖使用者一般保持鎖時(shí)間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠(yuǎn)高于互斥鎖。
信號(hào)量和讀寫信號(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)量和讀寫信號(hào)量保持期間是可以被搶占的。自旋鎖只有在內(nèi)核可搶占或SMP的情況下才真正需要,在單CPU且不可搶占的內(nèi)核下,自旋鎖的所有操作都是空操作,在單CPU且可搶占的內(nèi)核下,自旋鎖實(shí)際上只進(jìn)行開(kāi)啟和關(guān)閉內(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)1
該宏用于初始化自旋鎖x。自旋鎖在真正使用前必須先初始化。該宏用于動(dòng)態(tài)初始化。
DEFINE_SPINLOCK(x)1
該宏聲明一個(gè)自旋鎖x并初始化它。該宏在2.6.11中第一次被定義,在先前的內(nèi)核中并沒(méi)有該宏。
SPIN_LOCK_UNLOCKED1
該宏用于靜態(tài)初始化一個(gè)自旋鎖。
DEFINE_SPINLOCK(x)等同于spinlock_t x = SPIN_LOCK_UNLOCKED
spin_is_locked(x)1
該宏用于判斷自旋鎖x是否已經(jīng)被某執(zhí)行單元保持(即被鎖),如果是,返回真,否則返回假。
spin_unlock_wait(x)1
該宏用于等待自旋鎖x變得沒(méi)有被任何執(zhí)行單元保持,如果沒(méi)有任何執(zhí)行單元保持該自旋鎖,該宏立即返回,否則將循環(huán)在那里,直到該自旋鎖被保持者釋放。
spin_trylock(lock)1
該宏盡力獲得自旋鎖lock,如果能立即獲得鎖,它獲得鎖并返回真,否則不能立即獲得鎖,立即返回假。它不會(huì)自旋等待lock被釋放。
spin_lock(lock)1
該宏用于獲得自旋鎖lock,如果能夠立即獲得鎖,它就馬上返回,否則,它將自旋在那里,直到該自旋鎖的保持者釋放,這時(shí),它獲得鎖并返回。總之,只有它獲得鎖才返回。
spin_lock_irqsave(lock, flags)1
該宏獲得自旋鎖的同時(shí)把標(biāo)志寄存器的值保存到變量flags中并失效本地中斷。
spin_lock_irq(lock)1
該宏類似于spin_lock_irqsave,只是該宏不保存標(biāo)志寄存器的值。
spin_lock_bh(lock)1
該宏在得到自旋鎖的同時(shí)失效本地軟中斷。
spin_unlock(lock)1
該宏釋放自旋鎖lock,它與spin_trylock或spin_lock配對(duì)使用。如果spin_trylock返回假,表明沒(méi)有獲得自旋鎖,因此不必使用spin_unlock釋放。
spin_unlock_irqrestore(lock, flags)1
該宏釋放自旋鎖lock的同時(shí),也恢復(fù)標(biāo)志寄存器的值為變量flags保存的值。它與spin_lock_irqsave配對(duì)使用。
spin_unlock_irq(lock)1
該宏釋放自旋鎖lock的同時(shí),也使能本地中斷。它與spin_lock_irq配對(duì)應(yīng)用。
spin_unlock_bh(lock)1
該宏釋放自旋鎖lock的同時(shí),也使能本地的軟中斷。它與spin_lock_bh配對(duì)使用。
spin_trylock_irqsave(lock, flags)1
該宏如果獲得自旋鎖lock,它也將保存標(biāo)志寄存器的值到變量flags中,并且失效本地中斷,如果沒(méi)有獲得鎖,它什么也不做。因此如果能夠立即獲得鎖,它等同于spin_lock_irqsave,如果不能獲得鎖,它等同于spin_trylock。如果該宏獲得自旋鎖lock,那需要使用spin_unlock_irqrestore來(lái)釋放。
spin_trylock_irq(lock)1
該宏類似于spin_trylock_irqsave,只是該宏不保存標(biāo)志寄存器。如果該宏獲得自旋鎖lock,需要使用spin_unlock_irq來(lái)釋放。
spin_trylock_bh(lock)1
該宏如果獲得了自旋鎖,它也將失效本地軟中斷。如果得不到鎖,它什么也不做。因此,如果得到了鎖,它等同于spin_lock_bh,如果得不到鎖,它等同于spin_trylock。如果該宏得到了自旋鎖,需要使用spin_unlock_bh來(lái)釋放。
spin_can_lock(lock)1
該宏用于判斷自旋鎖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è)快。
舉例說(shuō)明:spinlock用在進(jìn)程上下文和中斷
進(jìn)程A中調(diào)用了spin_lock(&lock)然后進(jìn)入臨界區(qū),此時(shí)來(lái)了一個(gè)中斷(interrupt),該中斷也運(yùn)行在和進(jìn)程A相同的CPU上,并且在該中斷處理程序中恰巧也會(huì)spin_lock(&lock)試圖獲取同一個(gè)鎖。由于是在同一個(gè)CPU上被中斷,進(jìn)程A會(huì)被設(shè)置為TASK_INTERRUPT狀態(tài),
中斷處理程序無(wú)法獲得鎖,會(huì)不停的忙等,由于進(jìn)程A被設(shè)置為中斷狀態(tài),schedule()進(jìn)程調(diào)度就無(wú)法再調(diào)度進(jìn)程A運(yùn)行,這樣就導(dǎo)致了死鎖!但是如果該中斷處理程序運(yùn)行在不同的CPU上就不會(huì)觸發(fā)死鎖。因?yàn)樵诓煌腃PU上出現(xiàn)中斷不會(huì)導(dǎo)致進(jìn)程A的狀態(tài)被設(shè)為TASK_INTERRUPT,只是換出。當(dāng)中斷處理程序忙等被換出后,進(jìn)程A還是有機(jī)會(huì)獲得CPU,執(zhí)行并退出臨界區(qū),(有一個(gè)問(wèn)題沒(méi)有搞懂關(guān)中斷是關(guān)閉對(duì)所有cpu的中斷還是本地cpu的?從關(guān)閉中斷的函數(shù)來(lái)看似乎是針對(duì)本地的),所以在使用spin_lock時(shí)要明確知道該鎖不會(huì)在中斷處理程序中使用。如果有,那就需要使用spinlock_irq_save,該函數(shù)即會(huì)關(guān)搶占,也會(huì)關(guān)本地中斷(因?yàn)椴荒鼙WC打斷A的中斷和A不是一個(gè)cpu,因此spin_lock在多核cpu上使用還是要關(guān)中斷)。
被保護(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í)訪問(wèn)以及不同進(jìn)程上下文互相搶占導(dǎo)致的對(duì)共享資源的非同步訪問(wèn),而中斷失效和軟中斷失效卻是為了阻止在同一CPU上軟中斷或中斷對(duì)共享資源的非同步訪問(wèn)。
對(duì)于spin_lock用于阻止不同CPU上的執(zhí)行單元對(duì)共享資源的同時(shí)訪問(wèn)以及不同進(jìn)程上下文互相搶占導(dǎo)致的對(duì)共享資源的非同步訪問(wèn)在單核和多核cpu上的實(shí)現(xiàn)是不同的:
單核cpu
如果spin_lock不處于中斷上下文,則spin_lock鎖定的代碼只會(huì)在內(nèi)核發(fā)生搶占2的時(shí)候才會(huì)丟失CPU擁有權(quán)。所以,對(duì)于單核來(lái)說(shuō),需要在spin_lock獲得鎖的時(shí)候禁止搶占,釋放鎖的時(shí)候開(kāi)放搶占。因此這不是真正意義上的鎖。
內(nèi)核代碼實(shí)現(xiàn)如下:
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)可以看到僅禁止了內(nèi)核搶占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)
開(kāi)啟內(nèi)核搶占
多核
存在臨界區(qū)同時(shí)在多核上被執(zhí)行的情況,這時(shí)候才需要一個(gè)真正的鎖來(lái)宣告代碼對(duì)資源的占有。幾個(gè)核可能會(huì)同時(shí)access臨界區(qū),這時(shí)的spinlock是如何實(shí)現(xiàn)的呢?
要用到CPU提供的一些特殊指令,對(duì)lock變量進(jìn)行原子操作。
SMP中spin_lock的實(shí)現(xiàn)
實(shí)現(xiàn)在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上的實(shí)現(xiàn)被分解為三句話。
preempt_disable() 關(guān)搶占
spin_acquire()是sparse檢查需要
LOCK_CONTENDED()是一個(gè)宏,如果不考慮CONFIG_LOCK_STAT(該宏是為了統(tǒng)計(jì)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,我們明白這個(gè)函數(shù)是體系結(jié)構(gòu)相關(guān)的。這部分代碼使用匯編實(shí)現(xiàn)。(具體例子后續(xù)再加)
大內(nèi)核鎖(BKL–Big Kernel Lock)
大內(nèi)核鎖簡(jiǎn)介
大內(nèi)核鎖本質(zhì)上也是自旋鎖,但是它又不同于自旋鎖,自旋鎖是不可以遞歸獲得鎖的,因?yàn)槟菢訒?huì)導(dǎo)致死鎖。但大內(nèi)核鎖可以遞歸獲得鎖。大內(nèi)核鎖用于保護(hù)整個(gè)內(nèi)核,而自旋鎖用于保護(hù)非常特定的某一共享資源。進(jìn)程保持大內(nèi)核鎖時(shí)可以發(fā)生調(diào)度,具體實(shí)現(xiàn)是:在執(zhí)行schedule時(shí),schedule將檢查進(jìn)程是否擁有大內(nèi)核鎖,如果有,它將被釋放,以致于其它的進(jìn)程能夠獲得該鎖,而當(dāng)輪到該進(jìn)程運(yùn)行時(shí),再讓它重新獲得大內(nèi)核鎖。注意在保持自旋鎖期間是不允許發(fā)生調(diào)度的。
需要特別指出,整個(gè)內(nèi)核只有一個(gè)大內(nèi)核鎖,其實(shí)不難理解,內(nèi)核只有一個(gè),而大內(nèi)核鎖是保護(hù)整個(gè)內(nèi)核的,當(dāng)然有且只有一個(gè)就足夠了。
還需要特別指出的是,大內(nèi)核鎖是歷史遺留,內(nèi)核中用的非常少,一般保持該鎖的時(shí)間較長(zhǎng),因此不提倡使用它。從2.6.11內(nèi)核起,大內(nèi)核鎖可以通過(guò)配置內(nèi)核使其變得可搶占(自旋鎖是不可搶占的),這時(shí)它實(shí)質(zhì)上是一個(gè)互斥鎖,使用信號(hào)量實(shí)現(xiàn)。
大內(nèi)核鎖的API
void lock_kernel(void);1
該函數(shù)用于得到大內(nèi)核鎖。它可以遞歸調(diào)用而不會(huì)導(dǎo)致死鎖。
void unlock_kernel(void);1
該函數(shù)用于釋放大內(nèi)核鎖。當(dāng)然必須與lock_kernel配對(duì)使用,調(diào)用了多少次lock_kernel,就需要調(diào)用多少次unlock_kernel。
大內(nèi)核鎖的API使用非常簡(jiǎn)單,按照以下方式使用就可以了:
lock_kernel();
//對(duì)被保護(hù)的共享資源的訪問(wèn)
…
unlock_kernel();1234
(六)讀寫鎖(rwlock)
讀寫鎖簡(jiǎn)介
讀寫鎖實(shí)際是一種特殊的自旋鎖,它把對(duì)共享資源的訪問(wèn)者劃分成讀者和寫者,讀者只對(duì)共享資源進(jìn)行讀訪問(wèn),寫者則需要對(duì)共享資源進(jìn)行寫操作。這種鎖相對(duì)于自旋鎖而言,能提高并發(fā)性,因?yàn)樵诙嗵幚砥飨到y(tǒng)中,它允許同時(shí)有多個(gè)讀者來(lái)訪問(wèn)共享資源,最大可能的讀者數(shù)為實(shí)際的邏輯CPU數(shù)。寫者是排他性的,一個(gè)讀寫鎖同時(shí)只能有一個(gè)寫者或多個(gè)讀者(與CPU數(shù)相關(guān)),但不能同時(shí)既有讀者又有寫者。
在讀寫鎖保持期間也是搶占失效的。
讀寫鎖訪問(wèn)規(guī)則:
如果讀寫鎖當(dāng)前沒(méi)有讀者,也沒(méi)有寫者,那么寫者可以立刻獲得讀寫鎖,否則它必須自旋在那里,直到?jīng)]有任何寫者或讀者。
如果讀寫鎖沒(méi)有寫者,那么讀者可以立即獲得該讀寫鎖,否則讀者必須自旋在那里,直到寫者釋放該讀寫鎖。
讀寫鎖API
讀寫鎖的API看上去與自旋鎖很象,只是讀者和寫者需要不同的獲得和釋放鎖的API。
rwlock_init(x)1
該宏用于動(dòng)態(tài)初始化讀寫鎖x。
DEFINE_RWLOCK(x)1
該宏聲明一個(gè)讀寫鎖并對(duì)其進(jìn)行初始化。它用于靜態(tài)初始化。
RW_LOCK_UNLOCKED1
它用于靜態(tài)初始化一個(gè)讀寫鎖。
DEFINE_RWLOCK(x)等同于rwlock_t x = RW_LOCK_UNLOCKED
read_trylock(lock)1
讀者用它來(lái)盡力獲得讀寫鎖lock,如果能夠立即獲得讀寫鎖,它就獲得鎖并返回真,否則不能獲得鎖,返回假。無(wú)論是否能夠獲得鎖,它都將立即返回,絕不自旋在那里。
write_trylock(lock)1
寫者用它來(lái)盡力獲得讀寫鎖lock,如果能夠立即獲得讀寫鎖,它就獲得鎖并返回真,否則不能獲得鎖,返回假。無(wú)論是否能夠獲得鎖,它都將立即返回,絕不自旋在那里。
read_lock(lock)1
讀者要訪問(wèn)被讀寫鎖lock保護(hù)的共享資源,需要使用該宏來(lái)得到讀寫鎖lock。如果能夠立即獲得,它將立即獲得讀寫鎖并返回,否則,將自旋在那里,直到獲得該讀寫鎖。
write_lock(lock)1
寫者要想訪問(wèn)被讀寫鎖lock保護(hù)的共享資源,需要使用該宏來(lái)得到讀寫鎖lock。如果能夠立即獲得,它將立即獲得讀寫鎖并返回,否則,將自旋在那里,直到獲得該讀寫鎖。
read_lock_irqsave(lock, flags)1
讀者也可以使用該宏來(lái)獲得讀寫鎖,與read_lock不同的是,該宏還同時(shí)把標(biāo)志寄存器的值保存到了變量flags中,并失效了本地中斷。
write_lock_irqsave(lock, flags)1
寫者可以用它來(lái)獲得讀寫鎖,與write_lock不同的是,該宏還同時(shí)把標(biāo)志寄存器的值保存到了變量flags中,并失效了本地中斷。
read_lock_irq(lock)1
讀者也可以用它來(lái)獲得讀寫鎖,與read_lock不同的是,該宏還同時(shí)失效了本地中斷。該宏與read_lock_irqsave的不同之處是,它沒(méi)有保存標(biāo)志寄存器。
write_lock_irq(lock)1
寫者也可以用它來(lái)獲得鎖,與write_lock不同的是,該宏還同時(shí)失效了本地中斷。該宏與write_lock_irqsave的不同之處是,它沒(méi)有保存標(biāo)志寄存器。
read_lock_bh(lock)1
讀者也可以用它來(lái)獲得讀寫鎖,與與read_lock不同的是,該宏還同時(shí)失效了本地的軟中斷。
write_lock_bh(lock)1
寫者也可以用它來(lái)獲得讀寫鎖,與write_lock不同的是,該宏還同時(shí)失效了本地的軟中斷。
read_unlock(lock)1
讀者使用該宏來(lái)釋放讀寫鎖lock。它必須與read_lock配對(duì)使用。
write_unlock(lock)1
寫者使用該宏來(lái)釋放讀寫鎖lock。它必須與write_lock配對(duì)使用。
read_unlock_irqrestore(lock, flags)1
讀者也可以使用該宏來(lái)釋放讀寫鎖,與read_unlock不同的是,該宏還同時(shí)把標(biāo)志寄存器的值恢復(fù)為變量flags的值。它必須與read_lock_irqsave配對(duì)使用。
write_unlock_irqrestore(lock, flags)1
寫者也可以使用該宏來(lái)釋放讀寫鎖,與write_unlock不同的是,該宏還同時(shí)把標(biāo)志寄存器的值恢復(fù)為變量flags的值,并使能本地中斷。它必須與write_lock_irqsave配對(duì)使用。
read_unlock_irq(lock)1
讀者也可以使用該宏來(lái)釋放讀寫鎖,與read_unlock不同的是,該宏還同時(shí)使能本地中斷。它必須與read_lock_irq配對(duì)使用。
write_unlock_irq(lock)1
寫者也可以使用該宏來(lái)釋放讀寫鎖,與write_unlock不同的是,該宏還同時(shí)使能本地中斷。它必須與write_lock_irq配對(duì)使用。
read_unlock_bh(lock)1
讀者也可以使用該宏來(lái)釋放讀寫鎖,與read_unlock不同的是,該宏還同時(shí)使能本地軟中斷。它必須與read_lock_bh配對(duì)使用。
write_unlock_bh(lock)1
寫者也可以使用該宏來(lái)釋放讀寫鎖,與write_unlock不同的是,該宏還同時(shí)使能本地軟中斷。它必須與write_lock_bh配對(duì)使用。
讀寫鎖的獲得和釋放鎖的方法也有許多版本,具體用哪個(gè)與自旋鎖一樣,因此參考自旋鎖部分就可以了。只是需要區(qū)分讀者與寫者,讀者要用讀者版本,而寫者必須用寫者版本。
RCU(Read-Copy Update)
RCU簡(jiǎn)介
RCU(Read-Copy Update),顧名思義就是讀-拷貝修改,它是基于其原理命名的。對(duì)于被RCU保護(hù)的共享數(shù)據(jù)結(jié)構(gòu),讀者不需要獲得任何鎖就可以訪問(wèn)它,但寫者在訪問(wèn)它時(shí)首先拷貝一個(gè)副本,然后對(duì)副本進(jìn)行修改,最后使用一個(gè)回調(diào)(callback)機(jī)制在適當(dāng)?shù)臅r(shí)機(jī)把指向原來(lái)數(shù)據(jù)的指針重新指向新的被修改的數(shù)據(jù)。這個(gè)時(shí)機(jī)就是所有引用該數(shù)據(jù)的CPU都退出對(duì)共享數(shù)據(jù)的操作。
因此RCU實(shí)際上是一種改進(jìn)的rwlock,讀者幾乎沒(méi)有什么同步開(kāi)銷,它不需要鎖,不使用原子指令,而且在除alpha的所有架構(gòu)上也不需要內(nèi)存柵(Memory Barrier),因此不會(huì)導(dǎo)致鎖競(jìng)爭(zhēng),內(nèi)存延遲以及流水線停滯。不需要鎖也使得使用更容易,因?yàn)樗梨i問(wèn)題就不需要考慮了。寫者的同步開(kāi)銷比較大,它需要延遲數(shù)據(jù)結(jié)構(gòu)的釋放,復(fù)制被修改的數(shù)據(jù)結(jié)構(gòu),它也必須使用某種鎖機(jī)制同步并行的其它寫者的修改操作。讀者必須提供一個(gè)信號(hào)給寫者以便寫者能夠確定數(shù)據(jù)可以被安全地釋放或修改的時(shí)機(jī)。有一個(gè)專門的垃圾收集器來(lái)探測(cè)讀者的信號(hào),一旦所有的讀者都已經(jīng)發(fā)送信號(hào)告知它們都不在使用被RCU保護(hù)的數(shù)據(jù)結(jié)構(gòu),垃圾收集器就調(diào)用回調(diào)函數(shù)完成最后的數(shù)據(jù)釋放或修改操作。 RCU與rwlock的不同之處是:它既允許多個(gè)讀者同時(shí)訪問(wèn)被保護(hù)的數(shù)據(jù),又允許多個(gè)讀者和多個(gè)寫者同時(shí)訪問(wèn)被保護(hù)的數(shù)據(jù)(注意:是否可以有多個(gè)寫者并行訪問(wèn)取決于寫者之間使用的同步機(jī)制),讀者沒(méi)有任何同步開(kāi)銷,而寫者的同步開(kāi)銷則取決于使用的寫者間同步機(jī)制。但RCU不能替代rwlock,因?yàn)槿绻麑懕容^多時(shí),對(duì)讀者的性能提高不能彌補(bǔ)寫者導(dǎo)致的損失。
讀者在訪問(wèn)被RCU保護(hù)的共享數(shù)據(jù)期間不能被阻塞,這是RCU機(jī)制得以實(shí)現(xiàn)的一個(gè)基本前提,也就說(shuō)當(dāng)讀者在引用被RCU保護(hù)的共享數(shù)據(jù)期間,讀者所在的CPU不能發(fā)生上下文切換,spinlock和rwlock都需要這樣的前提。寫者在訪問(wèn)被RCU保護(hù)的共享數(shù)據(jù)時(shí)不需要和讀者競(jìng)爭(zhēng)任何鎖,只有在有多于一個(gè)寫者的情況下需要獲得某種鎖以與其他寫者同步。寫者修改數(shù)據(jù)前首先拷貝一個(gè)被修改元素的副本,然后在副本上進(jìn)行修改,修改完畢后它向垃圾回收器注冊(cè)一個(gè)回調(diào)函數(shù)以便在適當(dāng)?shù)臅r(shí)機(jī)執(zhí)行真正的修改操作。等待適當(dāng)時(shí)機(jī)的這一時(shí)期稱為寬限期(grace period),而CPU發(fā)生了上下文切換稱為經(jīng)歷一個(gè)quiescent state,grace period就是所有CPU都經(jīng)歷一次quiescent state所需要的等待的時(shí)間。垃圾收集器就是在grace period之后調(diào)用寫者注冊(cè)的回調(diào)函數(shù)來(lái)完成真正的數(shù)據(jù)修改或數(shù)據(jù)釋放操作的。
以下以鏈表元素刪除為例詳細(xì)說(shuō)明這一過(guò)程。
寫者要從鏈表中刪除元素 B,它首先遍歷該鏈表得到指向元素 B 的指針,然后修改元素 B 的前一個(gè)元素的 next 指針指向元素 B 的 next 指針指向的元素C,修改元素 B 的 next 指針指向的元素 C 的 prep 指針指向元素 B 的 prep指針指向的元素 A,在這期間可能有讀者訪問(wèn)該鏈表,修改指針指向的操作是原子的,所以不需要同步,而元素 B 的指針并沒(méi)有去修改,因?yàn)樽x者可能正在使用 B 元素來(lái)得到下一個(gè)或前一個(gè)元素。寫者完成這些操作后注冊(cè)一個(gè)回調(diào)函數(shù)以便在 grace period 之后刪除元素 B,然后就認(rèn)為已經(jīng)完成刪除操作。垃圾收集器在檢測(cè)到所有的CPU不在引用該鏈表后,即所有的 CPU 已經(jīng)經(jīng)歷了 quiescent state,grace period 已經(jīng)過(guò)去后,就調(diào)用剛才寫者注冊(cè)的回調(diào)函數(shù)刪除了元素 B。
使用 RCU 進(jìn)行鏈表刪除操作
RCU的API
rcu_read_lock()1
讀者在讀取由RCU保護(hù)的共享數(shù)據(jù)時(shí)使用該函數(shù)標(biāo)記它進(jìn)入讀端臨界區(qū)。
rcu_read_unlock()1
該函數(shù)與rcu_read_lock配對(duì)使用,用以標(biāo)記讀者退出讀端臨界區(qū)。
夾在這兩個(gè)函數(shù)之間的代碼區(qū)稱為”讀端臨界區(qū)”(read-side critical section)。讀端臨界區(qū)可以嵌套,如圖3,臨界區(qū)2被嵌套在臨界區(qū)1內(nèi)。
嵌套讀端臨界區(qū)示例
那么在讀端臨界區(qū)發(fā)生了什么?要回答這個(gè)問(wèn)題需要搞清楚rcu_read_lock和rcu_read_unlock做了什么操作,實(shí)際上即關(guān)閉內(nèi)核搶占和打開(kāi)內(nèi)核搶占
static inline void __rcu_read_lock(void)
{
preempt_disable();
}
static inline void __rcu_read_unlock(void)
{preempt_enable();}
即在讀端臨界區(qū)中時(shí)內(nèi)核是禁止搶占的。
那么這時(shí)是否度過(guò)寬限期(Grace Period)的判斷就比較簡(jiǎn)單:每個(gè)CPU都經(jīng)過(guò)一次搶占。因?yàn)榘l(fā)生搶占,就說(shuō)明不在rcu_read_lock和rcu_read_unlock之間,必然已經(jīng)完成訪問(wèn)或者還未開(kāi)始訪問(wèn)。
synchronize_rcu()1
該函數(shù)由RCU寫端調(diào)用,它將阻塞寫者,直到經(jīng)過(guò)grace period后,即所有的讀者已經(jīng)完成讀端臨界區(qū),寫者才可以繼續(xù)下一步操作。如果有多個(gè)RCU寫端調(diào)用該函數(shù),他們將在一個(gè)grace period之后全部被喚醒。注意,該函數(shù)在2.6.11及以前的2.6內(nèi)核版本中為synchronize_kernel,只是在2.6.12才更名為synchronize_rcu,但在2.6.12中也提供了synchronize_kernel和一個(gè)新的函數(shù)synchronize_sched,因?yàn)橐郧坝泻芏鄡?nèi)核開(kāi)發(fā)者使用synchronize_kernel用于等待所有CPU都退出不可搶占區(qū),而在RCU設(shè)計(jì)時(shí)該函數(shù)只是用于等待所有CPU都退出讀端臨界區(qū),它可能會(huì)隨著RCU實(shí)現(xiàn)的修改而發(fā)生語(yǔ)意變化,因此為了預(yù)先防止這種情況發(fā)生,在新的修改中增加了專門的用于其它內(nèi)核用戶的synchronize_sched函數(shù)和只用于RCU使用的synchronize_rcu,現(xiàn)在建議非RCU內(nèi)核代碼部分不使用synchronize_kernel而使用synchronize_sched,RCU代碼部分則使用synchronize_rcu,synchronize_kernel之所以存在是為了保證代碼兼容性。
synchronize_kernel()1
其他非RCU的內(nèi)核代碼使用該函數(shù)來(lái)等待所有CPU處在可搶占狀態(tài),目前功能等同于synchronize_rcu,但現(xiàn)在已經(jīng)不建議使用,而使用synchronize_sched。
synchronize_sched()1
該函數(shù)用于等待所有CPU都處在可搶占狀態(tài),它能保證正在運(yùn)行的中斷處理函數(shù)處理完畢,但不能保證正在運(yùn)行的softirq處理完畢。注意,synchronize_rcu只保證所有CPU都處理完正在運(yùn)行的讀端臨界區(qū)。
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
函數(shù)call_rcu也由RCU寫端調(diào)用,它不會(huì)使寫者阻塞,因而可以在中斷上下文或softirq使用。該函數(shù)將把函數(shù)func掛接到RCU回調(diào)函數(shù)鏈上,然后立即返回。一旦所有的CPU都已經(jīng)完成讀端臨界區(qū)操作,該函數(shù)將被調(diào)用來(lái)釋放刪除的將絕不在被應(yīng)用的數(shù)據(jù)。參數(shù)head用于記錄回調(diào)函數(shù)func,一般該結(jié)構(gòu)會(huì)作為被RCU保護(hù)的數(shù)據(jù)結(jié)構(gòu)的一個(gè)字段,以便省去單獨(dú)為該結(jié)構(gòu)分配內(nèi)存的操作。需要指出的是,函數(shù)synchronize_rcu的實(shí)現(xiàn)實(shí)際上使用函數(shù)call_rcu。
void fastcall call_rcu_bh(struct rcu_head *head,
void (*func)(struct rcu_head *rcu))12
函數(shù)call_ruc_bh功能幾乎與call_rcu完全相同,唯一差別就是它把softirq的完成也當(dāng)作經(jīng)歷一個(gè)quiescent state,因此如果寫端使用了該函數(shù),在進(jìn)程上下文的讀端必須使用rcu_read_lock_bh。
#define rcu_dereference(p) ({
typeof(p) _________p1 = p;
smp_read_barrier_depends();
(_________p1);
})12345
該宏用于在RCU讀端臨界區(qū)獲得一個(gè)RCU保護(hù)的指針,該指針可以在以后安全地引用,內(nèi)存柵只在alpha架構(gòu)上才使用。
除了這些API,RCU還增加了鏈表操作的RCU版本,因?yàn)閷?duì)于RCU,對(duì)共享數(shù)據(jù)的操作必須保證能夠被沒(méi)有使用同步機(jī)制的讀者看到,所以內(nèi)存柵是非常必要的。
static inline void list_add_rcu(struct list_head *new, struct list_head *head)1
該函數(shù)把鏈表項(xiàng)new插入到RCU保護(hù)的鏈表head的開(kāi)頭。使用內(nèi)存柵保證了在引用這個(gè)新插入的鏈表項(xiàng)之前,新鏈表項(xiàng)的鏈接指針的修改對(duì)所有讀者是可見(jiàn)的。
static inline void list_add_tail_rcu(struct list_head *new,
struct list_head *head)12
該函數(shù)類似于list_add_rcu,它將把新的鏈表項(xiàng)new添加到被RCU保護(hù)的鏈表的末尾。
static inline void list_del_rcu(struct list_head *entry)1
該函數(shù)從RCU保護(hù)的鏈表中移走指定的鏈表項(xiàng)entry,并且把entry的prev指針設(shè)置為L(zhǎng)IST_POISON2,但是并沒(méi)有把entry的next指針設(shè)置為L(zhǎng)IST_POISON1,因?yàn)樵撝羔樋赡苋匀辉诒蛔x者用于便利該鏈表。
static inline void list_replace_rcu(struct list_head *old, struct list_head *new)1
該函數(shù)是RCU新添加的函數(shù),并不存在非RCU版本。它使用新的鏈表項(xiàng)new取代舊的鏈表項(xiàng)old,內(nèi)存柵保證在引用新的鏈表項(xiàng)之前,它的鏈接指針的修正對(duì)所有讀者可見(jiàn)。
list_for_each_rcu(pos, head)1
該宏用于遍歷由RCU保護(hù)的鏈表head,只要在讀端臨界區(qū)使用該函數(shù),它就可以安全地和其它_rcu鏈表操作函數(shù)(如list_add_rcu)并發(fā)運(yùn)行。
list_for_each_safe_rcu(pos, n, head)1
該宏類似于list_for_each_rcu,但不同之處在于它允許安全地刪除當(dāng)前鏈表項(xiàng)pos。
list_for_each_entry_rcu(pos, head, member)1
該宏類似于list_for_each_rcu,不同之處在于它用于遍歷指定類型的數(shù)據(jù)結(jié)構(gòu)鏈表,當(dāng)前鏈表項(xiàng)pos為一包含struct list_head結(jié)構(gòu)的特定的數(shù)據(jù)結(jié)構(gòu)。
list_for_each_continue_rcu(pos, head)1
該宏用于在退出點(diǎn)之后繼續(xù)遍歷由RCU保護(hù)的鏈表head。
static inline void hlist_del_rcu(struct hlist_node *n)1
它從由RCU保護(hù)的哈希鏈表中移走鏈表項(xiàng)n,并設(shè)置n的ppre指針為L(zhǎng)IST_POISON2,但并沒(méi)有設(shè)置next為L(zhǎng)IST_POISON1,因?yàn)樵撝羔樋赡鼙蛔x者使用用于遍利鏈表。
static inline void hlist_add_head_rcu(struct hlist_node *n,
struct hlist_head *h)12
該函數(shù)用于把鏈表項(xiàng)n插入到被RCU保護(hù)的哈希鏈表的開(kāi)頭,但同時(shí)允許讀者對(duì)該哈希鏈表的遍歷。內(nèi)存柵確保在引用新鏈表項(xiàng)之前,它的指針修正對(duì)所有讀者可見(jiàn)。
hlist_for_each_rcu(pos, head)1
該宏用于遍歷由RCU保護(hù)的哈希鏈表head,只要在讀端臨界區(qū)使用該函數(shù),它就可以安全地和其它_rcu哈希鏈表操作函數(shù)(如hlist_add_rcu)并發(fā)運(yùn)行。
hlist_for_each_entry_rcu(tpos, pos, head, member)1
類似于hlist_for_each_rcu,不同之處在于它用于遍歷指定類型的數(shù)據(jù)結(jié)構(gòu)哈希鏈表,當(dāng)前鏈表項(xiàng)pos為一包含struct list_head結(jié)構(gòu)的特定的數(shù)據(jù)結(jié)構(gòu)。
下面部分將就 RCU 的幾種典型應(yīng)用情況詳細(xì)講解。
1. 只有增加和刪除的鏈表操作
在這種應(yīng)用情況下,絕大部分是對(duì)鏈表的遍歷,即讀操作,而很少出現(xiàn)的寫操作只有增加或刪除鏈表項(xiàng),并沒(méi)有對(duì)鏈表項(xiàng)的修改操作,這種情況使用RCU非常容易,從rwlock轉(zhuǎn)換成RCU非常自然。路由表的維護(hù)就是這種情況的典型應(yīng)用,對(duì)路由表的操作,絕大部分是路由表查詢,而對(duì)路由表的寫操作也僅僅是增加或刪除,因此使用RCU替換原來(lái)的rwlock順理成章。系統(tǒng)調(diào)用審計(jì)也是這樣的情況。
這是一段使用rwlock的系統(tǒng)調(diào)用審計(jì)部分的讀端代碼:
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;
}
這種轉(zhuǎn)換非常直接,使用rcu_read_lock和rcu_read_unlock分別替換read_lock和read_unlock,鏈表遍歷函數(shù)使用_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;
}
對(duì)于鏈表刪除操作,list_del替換為list_del_rcu和call_rcu,這是因?yàn)楸粍h除的鏈表項(xiàng)可能還在被別的讀者引用,所以不能立即刪除,必須等到所有讀者經(jīng)歷一個(gè)quiescent state才可以刪除。另外,list_for_each_entry并沒(méi)有被替換為list_for_each_entry_rcu,這是因?yàn)?,只有一個(gè)寫者在做鏈表刪除操作,因此沒(méi)有必要使用_rcu版本。
通常情況下,write_lock和write_unlock應(yīng)當(dāng)分別替換成spin_lock和spin_unlock,但是對(duì)于只是對(duì)鏈表進(jìn)行增加和刪除操作而且只有一個(gè)寫者的寫端,在使用了_rcu版本的鏈表操作API后,rwlock可以完全消除,不需要spinlock來(lái)同步讀者的訪問(wèn)。對(duì)于上面的例子,由于已經(jīng)有audit_netlink_sem被調(diào)用者保持,所以spinlock就沒(méi)有必要了。
這種情況允許修改結(jié)果延后一定時(shí)間才可見(jiàn),而且寫者對(duì)鏈表僅僅做增加和刪除操作,所以轉(zhuǎn)換成使用RCU非常容易。
2.寫端需要對(duì)鏈表?xiàng)l目進(jìn)行修改操作
如果寫者需要對(duì)鏈表?xiàng)l目進(jìn)行修改,那么就需要首先拷貝要修改的條目,然后修改條目的拷貝,等修改完畢后,再使用條目拷貝取代要修改的條目,要修改條目將被在經(jīng)歷一個(gè)grace period后安全刪除。
對(duì)于系統(tǒng)調(diào)用審計(jì)代碼,并沒(méi)有這種情況。這里假設(shè)有修改的情況,那么使用rwlock的修改代碼應(yīng)當(dāng)如下:
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,修改代碼應(yīng)當(dāng)為;
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.修改操作立即可見(jiàn)
前面兩種情況,讀者能夠容忍修改可以在一段時(shí)間后看到,也就說(shuō)讀者在修改后某一時(shí)間段內(nèi),仍然看到的是原來(lái)的數(shù)據(jù)。在很多情況下,讀者不能容忍看到舊的數(shù)據(jù),這種情況下,需要使用一些新措施,如System V IPC,它在每一個(gè)鏈表?xiàng)l目中增加了一個(gè)deleted字段,標(biāo)記該字段是否刪除,如果刪除了,就設(shè)置為真,否則設(shè)置為假,當(dāng)代碼在遍歷鏈表時(shí),核對(duì)每一個(gè)條目的deleted字段,如果為真,就認(rèn)為它是不存在的。
還是以系統(tǒng)調(diào)用審計(jì)代碼為例,如果它不能容忍舊數(shù)據(jù),那么,讀端代碼應(yīng)該修改為:
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;
}
注意,對(duì)于這種情況,每一個(gè)鏈表?xiàng)l目都需要一個(gè)spinlock保護(hù),因?yàn)閯h除操作將修改條目的deleted標(biāo)志。此外,該函數(shù)如果搜索到條目,返回時(shí)應(yīng)當(dāng)保持該條目的鎖,因?yàn)橹挥羞@樣,才能看到新的修改的數(shù)據(jù),否則,仍然可能看到舊的數(shù)據(jù)。
寫端的刪除操作將變成:
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 *
刪除條目時(shí),需要標(biāo)記該條目為已刪除。這樣讀者就可以通過(guò)該標(biāo)志立即得知條目是否已經(jīng)刪除.
RCU是2.6內(nèi)核引入的新的鎖機(jī)制,在絕大部分為讀而只有極少部分為寫的情況下,它是非常高效的,因此在路由表維護(hù)、系統(tǒng)調(diào)用審計(jì)、SELinux的AVC、dcache和IPC等代碼部分中,使用它來(lái)取代rwlock來(lái)獲得更高的性能。但是,它也有缺點(diǎn),延后的刪除或釋放將占用一些內(nèi)存,尤其是對(duì)嵌入式系統(tǒng),這可能是非常昂貴的內(nèi)存開(kāi)銷。此外,寫者的開(kāi)銷比較大,尤其是對(duì)于那些無(wú)法容忍舊數(shù)據(jù)的情況以及不只一個(gè)寫者的情況,寫者需要spinlock或其他的鎖機(jī)制來(lái)與其他寫者同步。
順序鎖(seqlock)
順序鎖簡(jiǎn)介
順序鎖也是對(duì)讀寫鎖的一種優(yōu)化,對(duì)于順序鎖,讀者絕不會(huì)被寫者阻塞,也就說(shuō),讀者可以在寫者對(duì)被順序鎖保護(hù)的共享資源進(jìn)行寫操作時(shí)仍然可以繼續(xù)讀,而不必等待寫者完成寫操作,寫者也不需要等待所有讀者完成讀操作才去進(jìn)行寫操作。但是,寫者與寫者之間仍然是互斥的,即如果有寫者在進(jìn)行寫操作,其他寫者必須自旋在那里,直到寫者釋放了順序鎖。
這種鎖有一個(gè)限制,它必須要求被保護(hù)的共享資源不含有指針,因?yàn)閷懻呖赡苁沟弥羔樖?,但讀者如果正要訪問(wèn)該指針,將導(dǎo)致OOPs。
如果讀者在讀操作期間,寫者已經(jīng)發(fā)生了寫操作,那么,讀者必須重新讀取數(shù)據(jù),以便確保得到的數(shù)據(jù)是完整的。
這種鎖對(duì)于讀寫同時(shí)進(jìn)行的概率比較小的情況,性能是非常好的,而且它允許讀寫同時(shí)進(jìn)行,因而更大地提高了并發(fā)性。
順序鎖的API
void write_seqlock(seqlock_t *sl);1
寫者在訪問(wèn)被順序鎖s1保護(hù)的共享資源前需要調(diào)用該函數(shù)來(lái)獲得順序鎖s1。它實(shí)際功能上等同于spin_lock,只是增加了一個(gè)對(duì)順序鎖順序號(hào)的加1操作,以便讀者能夠檢查出是否在讀期間有寫者訪問(wèn)過(guò)。
void write_sequnlock(seqlock_t *sl);1
寫者在訪問(wèn)完被順序鎖s1保護(hù)的共享資源后需要調(diào)用該函數(shù)來(lái)釋放順序鎖s1。它實(shí)際功能上等同于spin_unlock,只是增加了一個(gè)對(duì)順序鎖順序號(hào)的加1操作,以便讀者能夠檢查出是否在讀期間有寫者訪問(wèn)過(guò)。
寫者使用順序鎖的模式如下:
write_seqlock(&seqlock_a);
//寫操作代碼塊
…
write_sequnlock(&seqlock_a);
因此,對(duì)寫者而言,它的使用與spinlock相同。
int write_tryseqlock(seqlock_t *sl);1
寫者在訪問(wèn)被順序鎖s1保護(hù)的共享資源前也可以調(diào)用該函數(shù)來(lái)獲得順序鎖s1。它實(shí)際功能上等同于spin_trylock,只是如果成功獲得鎖后,該函數(shù)增加了一個(gè)對(duì)順序鎖順序號(hào)的加1操作,以便讀者能夠檢查出是否在讀期間有寫者訪問(wèn)過(guò)。
unsigned read_seqbegin(const seqlock_t *sl);1
讀者在對(duì)被順序鎖s1保護(hù)的共享資源進(jìn)行訪問(wèn)前需要調(diào)用該函數(shù)。讀者實(shí)際沒(méi)有任何得到鎖和釋放鎖的開(kāi)銷,該函數(shù)只是返回順序鎖s1的當(dāng)前順序號(hào)。
int read_seqretry(const seqlock_t *sl, unsigned iv);1
讀者在訪問(wèn)完被順序鎖s1保護(hù)的共享資源后需要調(diào)用該函數(shù)來(lái)檢查,在讀訪問(wèn)期間是否有寫者訪問(wèn)了該共享資源,如果是,讀者就需要重新進(jìn)行讀操作,否則,讀者成功完成了讀操作。
因此,讀者使用順序鎖的模式如下:
do {
seqnum = read_seqbegin(&seqlock_a);
//讀操作代碼塊
...
} while (read_seqretry(&seqlock_a, seqnum));
write_seqlock_irqsave(lock, flags)1
寫者也可以用該宏來(lái)獲得順序鎖lock,與write_seqlock不同的是,該宏同時(shí)還把標(biāo)志寄存器的值保存到變量flags中,并且失效了本地中斷。
write_seqlock_irq(lock)1
寫者也可以用該宏來(lái)獲得順序鎖lock,與write_seqlock不同的是,該宏同時(shí)還失效了本地中斷。與write_seqlock_irqsave不同的是,該宏不保存標(biāo)志寄存器。
write_seqlock_bh(lock)1
寫者也可以用該宏來(lái)獲得順序鎖lock,與write_seqlock不同的是,該宏同時(shí)還失效了本地軟中斷。
write_sequnlock_irqrestore(lock, flags)1
寫者也可以用該宏來(lái)釋放順序鎖lock,與write_sequnlock不同的是,該宏同時(shí)還把標(biāo)志寄存器的值恢復(fù)為變量flags的值。它必須與write_seqlock_irqsave配對(duì)使用。
write_sequnlock_irq(lock)1
寫者也可以用該宏來(lái)釋放順序鎖lock,與write_sequnlock不同的是,該宏同時(shí)還使能本地中斷。它必須與write_seqlock_irq配對(duì)使用。
write_sequnlock_bh(lock)1
寫者也可以用該宏來(lái)釋放順序鎖lock,與write_sequnlock不同的是,該宏同時(shí)還使能本地軟中斷。它必須與write_seqlock_bh配對(duì)使用。
read_seqbegin_irqsave(lock, flags)1
讀者在對(duì)被順序鎖lock保護(hù)的共享資源進(jìn)行訪問(wèn)前也可以使用該宏來(lái)獲得順序鎖lock的當(dāng)前順序號(hào),與read_seqbegin不同的是,它同時(shí)還把標(biāo)志寄存器的值保存到變量flags中,并且失效了本地中斷。注意,它必須與read_seqretry_irqrestore配對(duì)使用。
read_seqretry_irqrestore(lock, iv, flags)1
讀者在訪問(wèn)完被順序鎖lock保護(hù)的共享資源進(jìn)行訪問(wèn)后也可以使用該宏來(lái)檢查,在讀訪問(wèn)期間是否有寫者訪問(wèn)了該共享資源,如果是,讀者就需要重新進(jìn)行讀操作,否則,讀者成功完成了讀操作。它與read_seqretry不同的是,該宏同時(shí)還把標(biāo)志寄存器的值恢復(fù)為變量flags的值。注意,它必須與read_seqbegin_irqsave配對(duì)使用。
因此,讀者使用順序鎖的模式也可以為:
do {
seqnum = read_seqbegin_irqsave(&seqlock_a, flags);
//讀操作代碼塊
...
} while (read_seqretry_irqrestore(&seqlock_a, seqnum, flags));
讀者和寫者所使用的API的幾個(gè)版本應(yīng)該如何使用與自旋鎖的類似。
如果寫者在操作被順序鎖保護(hù)的共享資源時(shí)已經(jīng)保持了互斥鎖保護(hù)對(duì)共享數(shù)據(jù)的寫操作,即寫者與寫者之間已經(jīng)是互斥的,但讀者仍然可以與寫者同時(shí)訪問(wèn),那么這種情況僅需要使用順序計(jì)數(shù)(seqcount),而不必要spinlock。
順序計(jì)數(shù)的API
unsigned read_seqcount_begin(const seqcount_t *s);1
讀者在對(duì)被順序計(jì)數(shù)保護(hù)的共享資源進(jìn)行讀訪問(wèn)前需要使用該函數(shù)來(lái)獲得當(dāng)前的順序號(hào)。
int read_seqcount_retry(const seqcount_t *s, unsigned iv);1
讀者在訪問(wèn)完被順序計(jì)數(shù)s保護(hù)的共享資源后需要調(diào)用該函數(shù)來(lái)檢查,在讀訪問(wèn)期間是否有寫者訪問(wèn)了該共享資源,如果是,讀者就需要重新進(jìn)行讀操作,否則,讀者成功完成了讀操作。
因此,讀者使用順序計(jì)數(shù)的模式如下:
do {
seqnum = read_seqbegin_count(&seqcount_a);
//讀操作代碼塊
...
} while (read_seqretry(&seqcount_a, seqnum));
void write_seqcount_begin(seqcount_t *s);1
寫者在訪問(wèn)被順序計(jì)數(shù)保護(hù)的共享資源前需要調(diào)用該函數(shù)來(lái)對(duì)順序計(jì)數(shù)的順序號(hào)加1,以便讀者能夠檢查出是否在讀期間有寫者訪問(wèn)過(guò)。
void write_seqcount_end(seqcount_t *s);1
寫者在訪問(wèn)完被順序計(jì)數(shù)保護(hù)的共享資源后需要調(diào)用該函數(shù)來(lái)對(duì)順序計(jì)數(shù)的順序號(hào)加1,以便讀者能夠檢查出是否在讀期間有寫者訪問(wèn)過(guò)。
寫者使用順序計(jì)數(shù)的模式為:
write_seqcount_begin(&seqcount_a);
//寫操作代碼塊
…
write_seqcount_end(&seqcount_a);
需要特別提醒,順序計(jì)數(shù)的使用必須非常謹(jǐn)慎,只有確定在訪問(wèn)共享數(shù)據(jù)時(shí)已經(jīng)保持了互斥鎖才可以使用。
內(nèi)核在寫和讀netdevice的名字時(shí)分別使用dev_change_name和netdev_get_name中使用了該方法。而dev_change_name由dev_ifsioc調(diào)用,其如下所示使用互斥鎖進(jìn)行了保護(hù)。
rtnl_lock(); //內(nèi)部即互斥鎖
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()
可以看到這個(gè)與體系結(jié)構(gòu)有關(guān)
禁止內(nèi)核搶占就比較簡(jiǎn)單了,就是防止當(dāng)前進(jìn)程不會(huì)突然被另一個(gè)進(jìn)程搶占。在Linux的實(shí)現(xiàn)就是preempt_disable()和preempt_enable()函數(shù)
#define preempt_disable()
do {
//增加preempt_count
inc_preempt_count();
//保證先加了preempt_count才進(jìn)行以后的操作
barrier();
} while (0)
#define preempt_enable()
do {
preempt_enable_no_resched();
barrier();
//檢查當(dāng)前進(jìn)程是否可搶占
preempt_check_resched();
} while
不管是禁止中斷還是禁止內(nèi)核搶占,都是為了提供內(nèi)核同步,但是他們都沒(méi)有提供任何保護(hù)機(jī)制來(lái)防止其它處理器的并發(fā)訪問(wèn)。Linux支持多處理器,因此,內(nèi)核代碼一般都需要獲取某種鎖,防止來(lái)自其他處理器對(duì)共享數(shù)據(jù)的并發(fā)訪問(wèn),而禁止中斷提供保護(hù)機(jī)制,這是防止來(lái)自其他中斷處理程序的并發(fā)訪問(wèn)。
前面說(shuō)的都是概念,現(xiàn)在我們來(lái)討論幾個(gè)問(wèn)題
1.在單處理器條件下,為什么禁止中斷就可以禁止內(nèi)核搶占?
首先來(lái)回顧一下內(nèi)核搶占發(fā)生在哪些時(shí)候:
1. 在中斷返回內(nèi)核空間的時(shí)候,這個(gè)沒(méi)什么好說(shuō)的,跟中斷密切相關(guān),沒(méi)了中斷就不會(huì)發(fā)生
2. 內(nèi)核顯式調(diào)用schedule()(可搶占或阻塞)
我們先搞清楚一件事,就是我們說(shuō)禁止中斷可以禁止內(nèi)核搶占只是說(shuō)禁止任何意外的搶占,如果進(jìn)程自己要調(diào)用schedule函數(shù),那誰(shuí)也攔不住,事實(shí)上調(diào)用schedule這個(gè)函數(shù)本來(lái)就要禁止中斷,所以剩下的就是考慮創(chuàng)建或者喚醒一個(gè)更高優(yōu)先級(jí)的進(jìn)程,或者調(diào)用信號(hào)量、完成量,所有的這些情況都要通過(guò)try_to_wake_up函數(shù)喚醒另一個(gè)進(jìn)程,但是這個(gè)函數(shù)真正干的事只是設(shè)置了一下need_resched這個(gè)函數(shù),并沒(méi)有真的調(diào)用schedule函數(shù),調(diào)用是在系統(tǒng)調(diào)用返回用戶空間的時(shí)候進(jìn)行的,所以跟內(nèi)核搶占也沒(méi)啥關(guān)系,所以從這些方面來(lái)說(shuō),禁止中斷是可以禁止內(nèi)核搶占的
2.自旋鎖關(guān)中斷后,為什么要再禁止搶占?
假設(shè)有這么個(gè)情況:
1)CPU-1在進(jìn)程A的上下文調(diào)用了spin_lock_irqsave;
2)CPU-2調(diào)用wake_up_process喚醒了CPU-1上的進(jìn)程B,由于進(jìn)程B的優(yōu)先級(jí)高于進(jìn)程A,進(jìn)程A的TIF_NEED_RESCHED標(biāo)記被設(shè)置。(CPU-2還會(huì)用IPI通知CPU-1進(jìn)行resched,但是CPU-1禁用了中斷而不會(huì)響應(yīng));
3)CPU-1調(diào)用了某某函數(shù),這個(gè)函數(shù)包含了preempt_disable和preempt_enable(沒(méi)有規(guī)定關(guān)中斷的情況下不能調(diào)用這樣的函數(shù)吧~);
那么,如果spin_lock_irqsave沒(méi)有preempt_disable,第3步中的preempt_enable將觸發(fā)preempt_check_resched,從而讓進(jìn)程B搶占掉進(jìn)程A。
總之就是只有關(guān)了搶占,才能保證在臨界區(qū)成對(duì)出現(xiàn)的preempt_disable()/preempt_enable()(preempt_enable()也是一個(gè)潛在的主動(dòng)調(diào)度的測(cè)試點(diǎn))不會(huì)造成傷害。不然這種代碼就不能放在臨界區(qū)中了。
Linux可以使用互斥信號(hào)量來(lái)表示互斥鎖,那就是通過(guò)宏DECLARE_MUTEX來(lái)定義一個(gè)互斥信號(hào)量,因?yàn)镈ECLARE_MUTEX這個(gè)宏,Marcin Slusarz在08年提交的了一個(gè)patch,郵件地址為:https://lkml.org/lkml/2008/10/26/74,Marcin Slusarz認(rèn)為DECLARE_MUTEX宏會(huì)誤導(dǎo)開(kāi)發(fā)者,所以建議將DECLARE_MUTEX修改成DEFINE_SEMAPHORE,這個(gè)提議最終被內(nèi)核社區(qū)所接受,在2.6.36版本后的內(nèi)核就沒(méi)有DECLARE_MUTEX這個(gè)宏了,取而代之的是DEFINE_SEMAPHORE宏,在后來(lái)同互斥信號(hào)量相關(guān)的init_MUTEX、init_MUTEX_LOCKED也從文件中移除了。
雖然可以使用信號(hào)量來(lái)表示互斥鎖,但是互斥鎖其實(shí)是存在的,只是前面的宏DECLARE_MUTEX因?yàn)闀?huì)引起歧義,所以修改成了DEFINE_SEMAPHORE,mutex在2.6.16版本就融入到了主內(nèi)核中了,使用mutex需要包含頭文件.
禁止中斷和禁止搶占的簡(jiǎn)介
禁止中斷指的是Linux內(nèi)核提供了一組接口用于操作機(jī)器上的中斷狀態(tài)。這些接口為我們提供了能夠禁止當(dāng)前處理器的中斷系統(tǒng),或者屏蔽掉整個(gè)機(jī)器的一條中斷線的能力。通過(guò)禁止中斷,可以確保某個(gè)中斷處理程序不會(huì)搶占當(dāng)前的代碼??刂浦袛嘞到y(tǒng)在Linux的實(shí)現(xiàn)有很多,以local_irq_disable()和 local_irq_enable()函數(shù)為例
-
內(nèi)核
+關(guān)注
關(guān)注
3文章
1372瀏覽量
40289 -
cpu
+關(guān)注
關(guān)注
68文章
10863瀏覽量
211743 -
Linux
+關(guān)注
關(guān)注
87文章
11304瀏覽量
209476
發(fā)布評(píng)論請(qǐng)先 登錄
相關(guān)推薦
評(píng)論