Linux内核自旋锁
自旋鎖
自旋鎖(spinlock)是用在多個CPU系統中的鎖機制,當一個CPU正訪問自旋鎖保護的臨界區時,臨界區將被鎖上,其他需要訪問此臨界區的CPU只能忙等待,直到前面的CPU已訪問完臨界區,將臨界區開鎖。自旋鎖上鎖后讓等待線程進行忙等待而不是睡眠阻塞,而信號量是讓等待線程睡眠阻塞。自旋鎖的忙等待浪費了處理器的時間,但時間通常很短,在1毫秒以下。
自旋鎖用于多個CPU系統中,在單處理器系統中,自旋鎖不起鎖的作用,只是禁止或啟用內核搶占。在自旋鎖忙等待期間,內核搶占機制還是有效的,等待自旋鎖釋放的線程可能被更高優先級的線程搶占CPU。
自旋鎖基于共享變量。一個線程通過給共享變量設置一個值來獲取鎖,其他等待線程查詢共享變量是否為0來確定鎖現是否可用,然后在忙等待的循環中"自旋"直到鎖可用為止。
通用自旋鎖
自旋鎖的狀態值為1表示解鎖狀態,說明有1個資源可用;0或負值表示加鎖狀態,0說明可用資源數為0。Linux內核為通用自旋鎖提供了API函數初始化、測試和設置自旋鎖。API函數功能說明如表5。
表5 通用自旋鎖API函數功能說明| 宏定義 | 功能說明 |
| spin_lock_init(lock) | 初始化自旋鎖,將自旋鎖設置為1,表示有一個資源可用。 |
| spin_is_locked(lock) | 如果自旋鎖被置為1(未鎖),返回0,否則返回1。 |
| spin_unlock_wait(lock) | 等待直到自旋鎖解鎖(為1),返回0;否則返回1。 |
| spin_trylock(lock) | 嘗試鎖上自旋鎖(置0),如果原來鎖的值為1,返回1,否則返回0。 |
| spin_lock(lock) | 循環等待直到自旋鎖解鎖(置為1),然后,將自旋鎖鎖上(置為0)。 |
| spin_unlock(lock) | 將自旋鎖解鎖(置為1)。 |
| spin_lock_irqsave(lock, flags) | 循環等待直到自旋鎖解鎖(置為1),然后,將自旋鎖鎖上(置為0)。關中斷,將狀態寄存器值存入flags。 |
| spin_unlock_irqrestore(lock, flags) | 將自旋鎖解鎖(置為1)。開中斷,將狀態寄存器值從flags存入狀態寄存器。 |
| spin_lock_irq(lock) | 循環等待直到自旋鎖解鎖(置為1),然后,將自旋鎖鎖上(置為0)。關中斷。 |
| spin_unlock_irq(lock) | 將自旋鎖解鎖(置為1)。開中斷。 |
| spin_unlock_bh(lock) | 將自旋鎖解鎖(置為1)。開啟底半部的執行。 |
| spin_lock_bh(lock) | 循環等待直到自旋鎖解鎖(置為1),然后,將自旋鎖鎖上(置為0)。阻止軟中斷的底半部的執行。 |
下面用一個使用自旋鎖鎖住鏈表的樣例,代碼列出如下(在arch/x386/mm/pgtable.c中):
spinlock_t pgd_lock = SPIN_LOCK_UNLOCKED; //鎖初始化 void pgd_dtor(void *pgd, kmem_cache_t *cache, unsigned long unused) { unsigned long flags; //能從中斷上下文中被調用 ? spin_lock_irqsave(&pgd_lock, flags);//加鎖 pgd_list_del(pgd); spin_unlock_irqrestore(&pgd_lock, flags);//解鎖 }自旋鎖用結構spinlock_t描述,在include/linux/spinlock.h中有類型 spinlock_t定義,列出如下:
typedef struct {raw_spinlock_t raw_lock; #ifdef CONFIG_GENERIC_LOCKBREAK /*引入另一個自旋鎖*/unsigned int break_lock; #endif #ifdef CONFIG_DEBUG_SPINLOCK /*用于調試自旋鎖*/ unsigned int magic, owner_cpu; void *owner; #endif #ifdef CONFIG_DEBUG_LOCK_ALLOC struct lockdep_map dep_map; /*映射lock實例到lock-class對象 #endif } spinlock_t;由于自旋鎖的性能嚴重地影響著操作系統的性能,Linux內核提供了Lock-class和Lockdep跟蹤自旋鎖的使用對象和鎖的狀態,并可從/proc文件系統查詢自旋鎖的狀態信息。自旋鎖的調試通過配置項CONFIG_DEBUG_*項打開。
對于對稱多處理器系統(SMP),slock為一個int數據類型,對于單個處理器系統,slock定義為空。SMP的slock定義列出如下(在include/linux/spinlock_types.h):
typedef struct {volatile unsigned int slock; } raw_spinlock_t;自旋鎖的實現機制類型,下面僅分析自旋鎖API函數spin_lock_init、spin_lock_irqsave和spin_unlock_irqrestore。
(1)spin_lock_init
函數spin_lock_init將自旋鎖狀態值設置為1,表示未鎖狀態。其列出如下(在include/linux/spinlock.h中):
# define spin_lock_init(lock) /do { *(lock) = SPIN_LOCK_UNLOCKED; } while (0)宏__SPIN_LOCK_UNLOCKED列出如下(在include/linux/spinlock_types.h中):
# define __SPIN_LOCK_UNLOCKED(lockname) /(spinlock_t) { .raw_lock = __RAW_SPIN_LOCK_UNLOCKED, /SPIN_DEP_MAP_INIT(lockname) }#define __RAW_SPIN_LOCK_UNLOCKED { 1 }(2)函數spin_lock_irqsave
函數spin_lock_irqsave等待直到自旋鎖解鎖,即自旋鎖值為1,它還關閉本地處理器上的中斷。其列出如下(在include/linux/spinlock.h中):
#define spin_lock_irqsave(lock, flags) flags = _spin_lock_irqsave(lock)函數spin_lock_irqsave分析如下(在kernel/spinlock.c中):
unsigned long __lockfunc _spin_lock_irqsave(spinlock_t *lock) { unsigned long flags; ? local_irq_save(flags); //將狀態寄存器的值寫入flags保存 preempt_disable(); //關閉內核搶占,內核搶占鎖加1 spin_acquire(&lock->dep_map, 0, 0, _RET_IP_); ? #ifdef CONFIG_LOCKDEP LOCK_CONTENDED(lock, _raw_spin_trylock, _raw_spin_lock); #else _raw_spin_lock_flags(lock, &flags); #endif return flags; }宏定義local_irq_save保存了狀態寄存器的內容到x中,同時關中斷。這個宏定義列出如下:
#define local_irq_save(x) __asm__ __volatile__("pushfl?; popl?%0?; cli":"=g" (x): /* no input */?:"memory")上述語句中,指令pushfl將當前處理器的狀態寄存器的內容壓入堆棧保護。指令popl?%0 將狀態寄存器的內容存入x中,其中%0這里指x。
函數_raw_spin_lock_flags空操作等待直到自旋鎖的值為1,表示有資源可用,就跳出循環等待,準備執行本函數后面的操作。其列出如下:
# define _raw_spin_lock_flags(lock, flags) /__raw_spin_lock_flags(&(lock)->raw_lock, *(flags�函數__raw_spin_lock_flags列出如下(在include/asm-x86/spinlock.h中):
#define __raw_spin_lock_flags(lock, flags) __raw_spin_lock(lock) static __always_inline void __raw_spin_lock(raw_spinlock_t *lock) { int inc = 0x00010000; int tmp; /*指令前綴lock用來鎖住內存控制器,不讓其他處理器訪問,保證指令執行的原子性*/ asm volatile("lock?; xaddl?%0,?%1/n" // lock->slock=lock->slock+inc "movzwl?%w0,?%2/n/t" //tmp=inc "shrl $16,?%0/n/t" //inc >> 16 后,inc=1 "1:/t" "cmpl?%0,?%2/n/t" //比較inc與lock->slock "je 2f/n/t" //如果inc與lock->slock相等,跳轉到2 "rep?; nop/n/t" //空操作 "movzwl?%1,?%2/n/t" //tmp=lock->slock /* 這里不需要讀內存屏障指令lfence,因為裝載是排序的*/ "jmp 1b/n" //跳轉到1 "2:" : "+Q" (inc), "+m" (lock->slock), "=r" (tmp) : : "memory", "cc"); }(3)函數spin_unlock_irqrestore
宏定義spin_unlock_irqrestore是解鎖,開中斷,并把flags值存入到狀態寄存器中,這個宏定義分析如下:
#define spin_unlock_irqrestore(lock, flags) _spin_unlock_irqrestore(lock, flags)函數_spin_unlock_irqrestore列出如下(在kernel/spinlock.c中):
void __lockfunc _spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) { spin_release(&lock->dep_map, 1, _RET_IP_); _raw_spin_unlock(lock); //解鎖 local_irq_restore(flags); //開中斷,將flag的值存入狀態寄存器 preempt_enable(); //開啟內核搶占 } ? # define _raw_spin_unlock(lock) __raw_spin_unlock(&(lock)->raw_lock)函數__raw_spin_unlock將自旋鎖狀態值加1,表示有1個資源可用,從而釋放自旋鎖,其列出如下(在include/asm-x86/spinlock.h中):
static __always_inline void __raw_spin_unlock(raw_spinlock_t *lock) { asm volatile(UNLOCK_LOCK_PREFIX "incw?%0" // lock->slock= lock->slock +1 : "+m" (lock->slock) : : "memory", "cc"); }讀/寫自旋鎖
"讀/寫自旋鎖"用來解決讀者/寫者問題。如果有多個線程(進程、中斷處理程序、底半部例程)以只讀的方式訪問一個臨界區數據,讀/寫自旋鎖允許多個線程同時讀取數據。如果一個線程需要對臨界區數據進行寫操作,它必須獲取寫鎖,只有在沒有讀者或寫者進行操作時,寫者才獨占臨界區數據進行寫操作。讀操作時需要獲取讀鎖,寫操作時需要獲取寫鎖。
Linux內核為讀/寫自旋鎖提供了操作API函數初始化、測試和設置自旋鎖。API函數功能說明如表5。
表5 讀/寫自旋鎖API函數功能說明| 宏定義 | 功能說明 |
| rwlock_init(lock) | 初始化自旋鎖值為0x01000000(未鎖)。 |
| read_lock(lock) | 加讀者鎖,即將讀者計數加1。 |
| read_lock_irqsave(lock, flags) | 加讀者鎖,即將讀者計數加1。并且關中斷,存儲狀態標識到flags中。 |
| read_lock_irq(lock) | 加讀者鎖,即將讀者計數加1。并且關中斷。 |
| read_unlock(lock) | 解讀者鎖,即將讀者計數減1。 |
| read_unlock_irqrestore(lock, flags) | 解讀者鎖,即將讀者計數減1。并且開中斷,將狀態標識從flags讀到狀態寄存器中。 |
| read_unlock_irq(lock) | 解讀者鎖,即將讀者計數減1。并且開中斷。 |
| write_lock(lock) | 加寫者鎖,即將寫者鎖置0。 |
| write_lock_irqrestore(lock, flags) | 加寫者鎖,即將寫者鎖置0。并且關中斷,存儲狀態標識到flags中。 |
| write_lock_irq(lock) | 加寫者鎖,即將寫者鎖置0。并且關中斷。 |
| write_unlock(lock) | 解寫者鎖,即將寫者鎖置1。 |
| write_unlock_irqrestore(lock, flags) | 解寫者鎖,即將寫者鎖置1。并且開中斷,將狀態標識從flags讀到狀態寄存器中。 |
| write_unlock_irq(lock) | 解寫者鎖,即將寫者鎖置1。并且開中斷。 |
用戶使用讀/寫自旋鎖,應先自旋鎖的狀態值初始化為鎖初始化為RW_LOCK_BIAS,即0x01000000,表示為未鎖狀態。
讀/寫自旋鎖用結構rwlock_t描述,它的主要成員為鎖狀態值變量lock,結構rwlock_t列出如下(在include/linux/spinlock_types.h中):
typedef struct {raw_rwlock_t raw_lock;…… } rwlock_t; ? typedef struct { unsigned int lock; } raw_rwlock_t;在結構raw_rwlock_t中,讀/寫自旋鎖狀態變量lock為32位,它分為2個部分,0~23位是一個24位計數器,表示對臨界數據區進行并發讀操作的線程數,線程數以補碼形式存入計數器;第24位為表示"未鎖"的狀態位,在沒有線程讀或寫臨界區時,設置為1,否則,設置為0。
如果自旋鎖設置了"未鎖"狀態且無讀者,那么lock值為0x01000000;如果寫者已獲得自旋鎖且無讀者,則未鎖狀態位清0,lock值為0x00000000。如果有一個、2個或多個線程獲取鎖對臨界數據區進行讀操作,則lock值為0x00ffffff、0x00fffffe等(第24位清0表示未鎖,第0~23位為讀者個數的補碼)。
下面說明讀/寫自旋鎖API函數的實現:
(1)函數rwlock_init
函數rwlock_init將讀/寫自旋鎖狀態值設為0x01000000,其列出如下(在include/linux/spinlock.h中):
# define rwlock_init(lock) /do { *(lock) = RW_LOCK_UNLOCKED; } while (0)#define RW_LOCK_UNLOCKED __RW_LOCK_UNLOCKED(old_style_rw_init) #define __RW_LOCK_UNLOCKED(lockname) /(rwlock_t) { .raw_lock = __RAW_RW_LOCK_UNLOCKED, /RW_DEP_MAP_INIT(lockname) }#define __RAW_RW_LOCK_UNLOCKED { RW_LOCK_BIAS } #define RW_LOCK_BIAS 0x01000000(2)函數read_lock和read_unlock
函數read_lock用于加讀者鎖,函數read_unlock用于解讀者鎖,兩函數需要配對使用。下面分別進行說明:
函數read_lock
讀/寫自旋鎖lock空閑值為0x01000000,當有一個讀者進行讀操作時,它加讀者鎖,執行運算lock=lock-1,lock值為0x00ffffff;當接著有第二個讀者進行讀操作時,可以進行并發的讀,再執行運算lock=lock-1,lock值為0x00fffffe;依此類推,可支持多個讀者同時讀操作。
如果在讀操作正進行(如:有2個讀者正進行操作,lock值為0x00fffffe)時,有一個寫者請求寫操作時,寫操作必須等待讀者全部完成操作,每個讀者完成操作時,執行運算lock=lock+1,當2個讀者的操作完成后,lock值為0x01000000,表示寫鎖空閑,可以進行寫操作或并發的讀操作。
如果一個寫操作正進行時,執行運算lock=lock-0x01000000,lock值為0x00000000,表示寫者鎖已加鎖,另一個寫者無法對臨界區數據進行訪問。此時,如果有一個讀者進行讀操作請求時,執行運算lock=lock-1,結果為負數,則狀態寄存器符號位置為1,加讀者鎖失敗,將lock還原(lock=lock+1),讀者循環等待,直到寫操作完成(即lock值為0x01000000)時。
寫操作完成時,lock值為0x01000000,表示寫鎖空閑,可以進行寫操作或并發的讀操作。這時,正等待的讀者執行運算lock=lock-1,結果為0x00ffffff,則狀態寄存器符號位置為0,跳出加讀者鎖的等待循環,加鎖成功,讀者進行讀操作。
函數read_lock關閉內核搶占,加讀者鎖,即將讀者數增加1,其列出如下(在include/linux/spinlock.h中):
#define read_lock(lock) _read_lock(lock)函數_read_lock列出如下(在kernel/spinlock.c中):
void __lockfunc _read_lock(rwlock_t *lock) {preempt_disable(); //關閉內核搶占 rwlock_acquire_read(&lock->dep_map, 0, 0, _RET_IP_); /*用于自旋鎖調試*/ /*下面語句相當于_raw_read_lock(lock)*/ LOCK_CONTENDED(lock, _raw_read_trylock, _raw_read_lock); } # define _raw_read_lock(rwlock) __raw_read_lock(&(rwlock)->raw_lock)函數__raw_read_lock增加讀鎖,即鎖狀態值rw減1,由于讀者計數以補碼形式存放在鎖狀態值中,因此,減1表示讀者計數增加1。其列出如下(在include/asm-x86/spinglock.h中):
static inline void __raw_read_lock(raw_rwlock_t *rw) { asm volatile(LOCK_PREFIX " subl $1,(%0)/n/t" //*rw=*rw-1 "jns 1f/n" //如果符號位為0,跳轉到1 "call __read_lock_failed/n/t" "1:/n" ::LOCK_PTR_REG (rw) : "memory"); }函數__read_lock_failed進行加讀者鎖失敗后的循環等待操作。加讀者鎖失敗,說明有一個寫者正在寫操作,因此,鎖狀態值為*rw=0x00000000,函數__raw_read_lock在執行*rw=*rw-1后,rw值為0xffffffff,即傳入函數__read_lock_failed的rw值為0xffffffff。
函數__read_lock_failed執行*rw=*rw+1后,鎖狀態值為*rw=0x00000000,然后,進入循環等待狀態,直到,寫者完成寫操作后將鎖狀態值*rw置為0x01000000。這時,函數__read_lock_failed才跳出循環等待狀態,加讀者鎖成功。
函數__read_lock_failed列出如下(在include/asm-x86/lib/rwlock_64.h中):
/* rdi指向rwlock_t */ ENTRY(__read_lock_failed)CFI_STARTPROC //即:#define CFI_STARTPROC .cfi_startproc LOCK_PREFIX incl (%rdi) // *rw=*rw+1,值為0x00000000 1: rep //循環等待*rw值被寫者修改為0x01000000 nop cmpl $1,(%rdi) // *rw-1 js 1b //如果符號位為1,表明*rw值還為0x00000000,跳轉到1進行循環等待 LOCK_PREFIX /* 運行到這里,說明寫者操作完成,*rw值為0x01000000 */ decl (%rdi) //執行加讀者鎖操作*rw=*rw-1 js __read_lock_failed//如果符號位為1,表明*rw值為0x00000000,跳轉到函數開頭進行循環等待 ret CFI_ENDPROC //即:#define CFI_ENDPROC .cfi_endproc END(__read_lock_failed)由于匯編語言程序無法產生幀信息,由用戶手動添加指示語句。上述代碼中,指示語句.cfi_startproc用于調試時的調用幀信息處理,在每個函數的開始處使用,它在.eh_frame中生成一個條目,初始化一些內部數據結構,并發出構架依賴的初始CFI(Call Frame Information)指令。在函數結束處使用.cfi_endproc關閉該功能。
函數read_unlock
函數read_unlock開讀者鎖,即將鎖狀態值減1,由于讀者計數以補碼形式存放在鎖狀態值中,因此,加1表示讀者計數減1。其列出如下
# define read_unlock(lock) / do {__raw_read_unlock(&(lock)->raw_lock); __release(lock); } while (0) # define __release(x) __context__(x,-1) static inline void __raw_read_unlock(raw_rwlock_t *rw) { /* rw->lock= rw->lock +1*/ asm volatile(LOCK_PREFIX "incl?%0" :"+m" (rw->lock) : : "memory"); }(3)函數write_lock和write_unlock
函數write_lock和write_unlock分別加寫者鎖和解寫者鎖,分別說明如下:
函數write_lock
只有在沒有讀者或寫者對臨界區數據進行操作時,加寫者鎖才會成功,即:只有鎖狀態值lock值為0x01000000時,寫者加鎖才能成功,執行運行lock=lock-0x01000000運算。
當有讀者或寫者操作臨界區數據時,lock值為比0x01000000小的正數,如果值為0x00000000表示有一個寫者正在寫操作,如果值為0x00ffffff,表示有1個讀者在進行讀操作,如果值為0x00fffffe,表示有2個讀者在進行讀操作,依此類推。此時,寫者只能循環等待,直到lock值為0x01000000。
函數write_lock關閉內核搶占,加寫者鎖,其列出如下(在include/linux/spinlock.h中):
#define write_lock(lock) _write_lock(lock)函數_write_lock列出如下(在kernel/spinlock.c中):
void __lockfunc _write_lock(rwlock_t *lock) {preempt_disable(); /*關閉內核搶占*/ rwlock_acquire(&lock->dep_map, 0, 0, _RET_IP_); /*用于自旋鎖調試*/ /*下面語句相當于_raw_write_lock(lock)*/ LOCK_CONTENDED(lock, _raw_write_trylock, _raw_write_lock); } ? # define _raw_write_lock(rwlock) __raw_write_lock(&(rwlock)->raw_lock) ? static inline void __raw_write_lock(raw_rwlock_t *rw) { asm volatile(LOCK_PREFIX " subl?%1,(%0)/n/t" /* RW_LOCK_BIAS-rw*/ /* 如果沒有讀者或寫者,*rw為0x01000000,RW_LOCK_BIAS-rw為0 */ "jz 1f/n" /*值為0,跳轉到1*/ "call __write_lock_failed/n/t" /*加寫者鎖失敗*/ "1:/n" /* RW_LOCK_BIAS定義為0x01000000*/ ::LOCK_PTR_REG (rw), "i" (RW_LOCK_BIAS) : "memory"); }運行函數__write_lock_failed時,說明加寫者鎖失敗。如果加寫者鎖失敗,說明有讀者或寫者正在訪問臨界區數據,*rw值為一個小于0x01000000的正數。此時,函數__write_lock_failed循環等待直到,讀者或寫者完成操作,鎖變為空閑,即*rw值為0x01000000。
函數__write_lock_failed列出如下(在include/asm-x86/lib/rwlock_64.h中):
/* rdi: pointer to rwlock_t */ ENTRY(__write_lock_failed)CFI_STARTPROC /*用于調試時將調用幀信息寫入 LOCK_PREFIX addl $RW_LOCK_BIAS,(%rdi) // *rw=*rw+$RW_LOCK_BIAS,還原為嘗試加鎖前的狀態值 1: rep nop cmpl $RW_LOCK_BIAS,(%rdi) //比較結果 = *rw-$RW_LOCK_BIAS jne 1b //比較結果不為0,說明有寫者或讀者在訪問臨界區,跳轉到1進行循環等待 LOCK_PREFIX //鎖內存管理器,確保原子操作 /*運行到這里,說明鎖空閑,*rw值為0x010000,執行加寫者鎖操作*/ subl $RW_LOCK_BIAS,(%rdi) //*rw=*rw-RW_LOCK_BIAS jnz __write_lock_failed /*如果*rw不為0,說明加寫者鎖失敗,跳轉到函數頭循環等待*/ ret CFI_ENDPROC END(__write_lock_failed)函數write_unlock
函數write_unlock在寫者操作完后解寫者鎖,讀/寫自旋鎖變為空閑,鎖狀態值lock變為: 0x00000000+0x01000000。以后,讀者或寫者可以訪問臨界區數據了。
函數write_unlock列出如下:
# define write_unlock(lock) /do {__raw_write_unlock(&(lock)->raw_lock); __release(lock); } while (0)函數_write_unlock列出如下(在kernel/spinlock.c中):
void __lockfunc _write_unlock(rwlock_t *lock) {rwlock_release(&lock->dep_map, 1, _RET_IP_); _raw_write_unlock(lock); preempt_enable(); /*打開內核搶占*/ } ? # define _raw_write_unlock(rwlock) __raw_write_unlock(&(rwlock)->raw_lock)函數__raw_write_unlock開寫者鎖,即將鎖狀態值加上RW_LOCK_BIAS,其列出如下(在include/asm-x86/spinlock.h中):
static inline void __raw_write_unlock(raw_rwlock_t *rw) { asm volatile(LOCK_PREFIX "addl?%1,?%0" /* RW_LOCK_BIAS+rw*/ : "+m" (rw->lock) : "i" (RW_LOCK_BIAS) : "memory"); }順序鎖
當使用讀/寫鎖時,讀者必須等待寫者完成時才能讀,寫者必須等待讀者完成時才能寫,兩者的優先權是平等的。順序鎖是對讀/寫鎖的優化,它允許讀寫同時進行,提高了并發性,讀寫操作同時進行的概率較小時,其性能很好。順序鎖對讀/寫鎖進行了下面的改進:
- 寫者不會阻塞讀者,即寫操作時,讀者仍可以進行讀操作。
- 寫者不需要等待所有讀者完成讀操作后才進行寫操作。
- 寫者與寫者之間互斥,即如果有寫者在寫操作時,其他寫者必須自旋等待。
- 如果在讀者進行讀操作期間,有寫者進行寫操作,那么讀者必須重新讀取數據,確保讀取正確的數據。
- 要求臨界區的共享資源不含指針,因為如果寫者使指針失效,讀者訪問該指針,將導致崩潰。
順序鎖實際上由一個自旋鎖和一個順序計數器組成,有的應用已包括自旋鎖,只需要一個順序計數器配合就可以實現順序鎖。針對這兩種情況,Linux內核給順序鎖提供了兩套API函數。一套API函數為*seq*,完整地實現了順序鎖;另一套API函數為*seqcount*,只包含了順序計數器,需要與用戶的自旋鎖配套實現順序鎖。順序鎖API函數的功能說明如表5所示。
表5 順序鎖API函數功能說明| 函數名 | 功能說明 |
| seqlock_init(x) | 初始化順序鎖,將順序計數器置0。 |
| write_seqlock(seqlock_t *sl) | 加順序鎖,將順序號加1。寫者獲取順序鎖s1訪問臨界區,它使用了函數spin_lock。 |
| write_sequnlock(seqlock_t *sl) | 解順序鎖,使用了函數spin_unlock,順序號加1。 |
| write_tryseqlock(seqlock_t *sl) | 功能上等同于spin_trylock,順序號加1。 |
| read_seqbegin(const seqlock_t *sl) | 返回順序鎖s1的當前順序號,讀者沒有開鎖和釋放鎖的開銷。 |
| read_seqretry(const seqlock_t *sl, unsigned start) | 檢查讀操作期間是否有寫者訪問了共享資源,如果是,讀者就需要重新進行讀操作,否則,讀者成功完成了讀操作。 |
| seqcount_init(x) | 初始化順序號。 |
| read_seqcount_begin(const seqcount_t *s) | 讀者在讀操作前用此函數獲取當前的順序號。 |
| read_seqcount_retry(const seqcount_t *s, unsigned start) | 讀者在訪問完后調用此函數檢查在讀期間是否有寫者訪問臨界區。如果有,讀者需要重新進行讀操作,否則,完成讀操作。 |
| write_seqcount_begin(seqcount_t *s) | 寫者在訪問臨界區前調用此函數將順序號加1,以便讀者檢查是否在讀期間有寫者訪問過。 |
| write_seqcount_end(seqcount_t *s) | 寫者寫完成后調用此函數將順序號加1,以便讀者能檢查出是否在讀期間有寫者訪問過。 |
用戶使用順序鎖時,寫操作加鎖方法與自旋鎖一樣,但讀操作需要使用循環查詢,使用順序鎖的讀操作樣例列出如下(在kernel/time.c中):
u64 get_jiffies_64(void) {unsigned long seq; u64 ret; ? do { seq = read_seqbegin(&xtime_lock); //獲取當前的順序號 ret = jiffies_64; //讀取臨界區數據 /*檢查seq值與當前順序號是否相等,若不等,說明有寫者開始工作,函數read_seqretry返回1,繼續循環*/ } while (read_seqretry(&xtime_lock, seq)); return ret; }在非SMP系統上,自旋鎖消失,但寫者還必須遞增順序變量,因為中斷例程可能改變數據的狀態。
下面分析順序鎖的數據結構及API函數:
(1)順序鎖結構seqlock_t
順序鎖用結構seqlock_t描述,它包括順序計數器sequence和自旋鎖lock。結構seqlock_t列出如下(在include/linux/seqlock.h中):
typedef struct {unsigned sequence;spinlock_t lock; } seqlock_t;在結構seqlock_t中,順序計數器sequence存放順序號,每個讀者在讀數據前后兩次讀順序計數器,并檢查兩次讀到的順序號是否相同。如果不相同,說明新的寫者已經開始寫并增加了順序計數器,表明剛讀到的數據無效。
寫者通過調用函數write_seqlock獲取順序鎖,將順序號加1,調用函數write_sequnlock釋放順序鎖,再將順序號加1。這樣,寫者正在寫操作時,順序號為奇數,寫完臨界區數據后,順序號為偶數。
讀者應以循環查詢方法讀取臨界區數據,讀者執行的臨界區代碼的方法列出如下:
do {seq = read_seqbegin(&foo); //返回當前的順序號 ... //臨界區數據操作 } while (read_seqretry(&foo, seq));在上述代碼中,讀者在讀臨界區數據之前,先調用函數read_seqbegin獲致當前的順序號,如果順序號seq為奇數,說明寫者正寫臨界區數據,或者seq值與順序號當前值不等,表明讀者正讀時,寫者開始寫,函數read_seqretry返回1,讀者繼續循環等待寫者完成。
(2)順序鎖初始化函數seqlock_init
函數seqlock_init初始化順序鎖,順序鎖實際上由一個自旋鎖和一個順序計數器組成。其列出如下:
#define seqlock_init(x) /do { /(x)->sequence = 0; /spin_lock_init(&(x)->lock); /} while (0)(3)寫者加鎖函數write_seqlock
函數write_seqlock加順序鎖。方法是:它先加自旋鎖,然后將順序號加1,此時,順序號值為奇數。此函數不需要關閉內核搶占,因為自旋鎖加鎖時已關閉了內核搶占。其列出如下:
static inline void write_seqlock(seqlock_t *sl) { spin_lock(&sl->lock); ++sl->sequence; smp_wmb(); }(4)寫者解鎖函數write_sequnlock
函數write_sequnlock表示寫者解順序鎖,它將順序號加1,然后解開自旋鎖。此時,順序號應為偶數。其列出如下(在include/linux/seqlock.h中):
static inline void write_sequnlock(seqlock_t *sl) { smp_wmb(); //加上SMP寫內存屏障 sl->sequence++; //順序號加1 spin_unlock(&sl->lock); //解開自旋鎖 }(5)讀操作開始時讀順序號函數read_seqbegin
函數read_seqbegin讀取順序號,如果順序號為奇數,說明寫者正在寫操作,處理器執行空操作,進行循環等待,否則,函數返回讀取的順序號值。其列出如下:
static __always_inline unsigned read_seqbegin(const seqlock_t *sl) { unsigned ret; ? repeat: ret = sl->sequence; smp_rmb(); //加上SMP讀內存屏障 if (unlikely(ret & 1)) { //如果ret & 1為true,表示順序號為奇數,寫者正在寫操作 cpu_relax(); //空操作 goto repeat; } ? return ret; }(6)讀操作完成時順序號檢查函數read_seqretry
函數read_seqretry用于讀操作完成后檢測讀的數據是否有效。如果讀操作完成后的順序號與讀操作開始前的順序號不一致,函數返回1,說明有寫者更改了臨界區數據,因此,調用者必須重新讀臨界者數據。
函數read_seqretry列出如下:
static __always_inline int read_seqretry(const seqlock_t *sl, unsigned start) { smp_rmb();//加上SMP讀內存屏障 ? return (sl->sequence != start); //順序鎖的順序號值與讀操作開始時的順序號值start不一致 }大內核鎖
Linux內核因歷史原因還保留著大內核鎖(Big Kernel Lock,BKL),它在內核中的用途越來越小。大內核鎖用于同步整個內核,鎖的保持時間較長,嚴重地影響延遲,不提倡使用。
大內核鎖本質上是自旋鎖,它由一個自旋鎖和一個鎖深度變量組成。自旋鎖不能遞歸獲得鎖的,否則導致死鎖。大內核鎖進行了改進,它可以遞歸獲得鎖,還實現了搶占。整個內核只有一個大內核鎖,因為內核只有一個,用于保護整個內核。
鎖深度變量定義列出如下(在include/linux/sched.h):
struct task_struct {……int lock_depth; /* BKL鎖深度*/ …… }自旋鎖定義列出如下(在lib/kernel_lock.c中):
static __cacheline_aligned_in_smp DEFINE_SPINLOCK(kernel_flag);大內核鎖的API函數為lock_kernel(void)和unlock_kernel(void),函數lock_kernel為獲取大內核鎖,可以遞歸調用而不導致死鎖;函數unlock_kernel釋放大內核鎖。它們的用法與一般自旋鎖類似。
?
posted on 2018-10-08 09:51 公眾號;嵌入式Linux 閱讀(...) 評論(...) 編輯 收藏總結
以上是生活随笔為你收集整理的Linux内核自旋锁的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: web前端js上传文件
- 下一篇: 2021年的工作日记模板准备好了