Linux核心的同步機制

來源:互聯網
上載者:User

本文詳細的介紹了Linux核心中的同步機制:原子操作、訊號量、讀寫訊號量和自旋鎖的API,使用要求以及一些典型樣本

一、引言

在現代作業系統裡,同一時間可能有多個核心執行流在執行,因此核心其實象多進程多線程編程一樣也需要一些同步機制來同步各執行單元對共用資料的訪問。尤其是在多處理器系統上,更需要一些同步機制來同步不同處理器上的執行單元對共用的資料的訪問。

在主流的Linux核心中包含了幾乎所有現代的作業系統具有的同步機制,這些同步機制包括:原子操作、訊號量(semaphore)、讀寫訊號量(rw_semaphore)、spinlock、BKL(Big Kernel Lock)、rwlock、brlock(只包含在2.4核心中)、RCU(只包含在2.6核心中)和seqlock(只包含在2.6核心中)。

二、原子操作

所謂原子操作,就是該操作絕不會在執行完畢前被任何其他任務或事件打斷,也就說,它的最小的執行單位,不可能有比它更小的執行單位,因此這裡的原子實際是使用了物理學裡的物質微粒的概念。
原子操作需要硬體的支援,因此是架構相關的,其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_UNLOCKEDspin_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上非強制中斷或中斷對共用資源的非同步訪問。
參考資料
Kernel Locking Techniques,http://www.linuxjournal.com/article/5833
Redhat 9.0 kernel source tree 
kernel.org 2.6.12 source tree 
Linux 2.6核心中新的鎖機制--RCU(Read-Copy Update), 
http://www.ibm.com/developerworks/cn/linux/l-rcu/
Unreliable Guide To Locking.

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.