轉自:https://blog.csdn.net/adaptiver/article/details/72389453
http://blog.csdn.net/longwang155069/article/details/52055876
自旋鎖的引入
原子變量適用在多核之間多單一共享變量進行互斥訪問,如果要保護多個變量,並且這些變量之間有邏輯關系時,原子變量就不適用了。例如:常見的雙向鏈表。假設有三個鏈表節點A、B、C。需要將節點B插入節點A、C之間。如果CPU A剛好將A節點的后向指針指向B,但是還沒有將B的后向指針指向C。此時CPU B要遍歷鏈表,這將會一個災難性的后果。
如果共享數據段在中斷上下文或者進程上下文被訪問呢? 如果在進程上下文被訪問,完全可以使用信號量semaphore機制來實現互斥。如果在中斷上下文被訪問呢? 就不能使用semaphore來實現互斥,因為semaphore會引起睡眠的。這時候就引入了spin_lock
spin_lock的實現思想
先說生活中一個示例,如果機智的你乘坐過火車的話,就一定知道早上6點-7點在火車上廁所的感受了。如果機智你的起來上廁所,發現一大堆人都等着上廁所,男女老少。接設你前面排了三個人,分別為A, B, C。
當A進入廁所之后,關閉了廁所的門,然后就會看見一個紅燈亮着“有人“,這時候B,C和機智的你都在等待。當A出來后,B進去不到20s就出來了。然后進去了C,然后你就苦苦的在等待,一直在觀察這什么時候紅燈熄滅,這讓機智的你等待了10min, 然后機智的你進去就10s搞定。好了關於生活的例子說完了,再回到spin_lock中。
可以將廁所當作臨界區。A, B, C, 機智的你是四個cpu, 紅燈是臨界區時候有cpu進入狀態。
當A進入臨界區(廁所),然后就會將進入狀態修改為忙(紅燈亮),然后B,C以及機智的你都會判斷當前狀態,如果是忙,就等待,不忙就讓B先進去,B進入之后同樣的操作。
spin_lock早期代碼分析
因為spin_lock在ARM平台上的實現策略發生過變化,所以先分析以前版本2.6.18的spin_lock。
主要是以SMP系統分析,后面會稍帶分析UP系統。
-
<include/linux/spinlock.h>
-
----------------------------------------------------------
-
-
-
<kernel/spinlock.c>
-
--------------------------------------------------------
-
void __lockfunc _spin_lock(spinlock_t *lock)
-
{
-
preempt_disable();
-
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
-
_raw_spin_lock(lock);
-
}
其中preempt_disable()是用來關閉掉搶占的。如果系統中打開了CONFIG_PREEMPT該選項的話,就是用來關閉系統的搶占,如果沒有開啟相當於什么都沒干,只是為了統一代碼。至於這里為什么需要關閉搶占,在后面會說。
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
這段代碼使用來調試使用的,沒有系統沒有開啟CONFIG_DEBUG_LOCK_ALLOC配置的話,這樣代碼也相當於什么都沒干。繼續往下。
-
define _raw_spin_lock (lock) __raw_spin_lock(&(lock)->raw_lock)
-
static inline void __raw_spin_lock(raw_spinlock_t *lock)
-
{
-
unsigned long tmp;
-
-
__asm__ __volatile__(
-
"1: ldrex %0, [%1]\n"
-
" teq %0, #0\n"
-
" strexeq %0, %2, [%1]\n"
-
" teqeq %0, #0\n"
-
" bne 1b"
-
: "=&r" (tmp)
-
: "r" (&lock->lock), "r" (1)
-
: "cc");
-
-
smp_mb();
-
}
回頭看看spinlock_t變量的定義:
-
typedef struct {
-
raw_spinlock_t raw_lock;
-
} spinlock_t;
-
-
typedef struct {
-
volatile unsigned int lock;
-
} raw_spinlock_t;
通過層層的調用,最后spinlock_t就是一個volatile unsigned int型變量。
匯編代碼 | C語言 | 解釋 |
---|---|---|
1: ldrex %0, [%1] | tmp=lock->lock | 讀取lock的狀態賦值給tmp |
teq %0, #0 | if(tmp == 0) | 判斷lock的狀態是否為0。如果是0說明可以獲得鎖;如果不為0,說明自旋鎖處於上鎖狀態,不能訪問,執行bne 1b指令,跳到標號1處不停執行。 |
strexeq %0, %2, [%1] | lock->lock=1 | 使用常量1來更新鎖的狀態,並將執行結果放入到tmp中 |
teqeq %0, #0 | if(tmp == 0) | 用來判斷tmp是否為0,如果為0,表明更新鎖的狀態成功;如果不為0表明鎖的狀態沒喲更新成功,執行”bne 1b”,跳轉到標號1繼續執行。 |
早期spin_lock存在的不公平性
還是回到火車上上廁所的故事中,某天早上去上廁所,發現有一大堆的人都在排隊。但是進去廁所的人已經進去了半個小時,后面的人已經開始等待不急了,有的謾罵起來,有人大喊憋不住了,機智你的剛好肚子疼,快憋不住了。剛好排在第一位是你的媳婦,然后你就插隊立馬上了廁所。你出來后,接着是你兒子,然后你全家。后面的人就一直等待了1個小時終於進入了廁所。
將這個現象轉移到程序中就是,在現代多核的cpu中,因為每個cpu都有chach的存在,導致不需要去訪問主存獲取lock,所以當當前獲取lock的cpu,釋放鎖后,使其他cpu的cache都失效,然后釋放的鎖在下一次就比較容易進入臨界去,導致出現了不公平。
ticket機制原理
先看最新的spin_lock的結構體定義:
-
typedef struct spinlock {
-
struct raw_spinlock rlock;
-
} spinlock_t;
-
-
typedef struct raw_spinlock {
-
arch_spinlock_t raw_lock;
-
} raw_spinlock_t;
-
-
typedef struct {
-
union {
-
u32 slock;
-
struct __raw_tickets {
-
-
u16 next;
-
u16 owner;
-
-
u16 owner;
-
u16 next;
-
-
} tickets;
-
};
-
} arch_spinlock_t;
在分析代碼之前,還需要解釋一下tickets中的owner和next的含義。詳細可見提交:
546c2896a42202dbc7d02f7c6ec9948ac1bf511b
因為有cache的作用,導致本次釋放lock的cpu在下一次就可以更快的獲取鎖。所以在ARMv6上引入了”票”算法來保證每個cpu都是像“FIFO“訪問臨界區。
還是說回到火車上廁所的事件,還是早上排隊上廁所。這時候好多人都插隊,導致沒有熟人的人一直上不了廁所,於是火車管理員(虛擬的,只是為了講解原理而已)出現了。火車管理員說“從現在開始不准插隊,我來監督,所有人排位一隊“。管理員站在廁所門口,讓大家都按次序排隊上廁所,這時候就沒有人插隊了。
將這個事件轉移到程序中的ticket中。剛開始的時候臨界區沒有cpu進入,狀態是空閑的。next和owner的值都是0,當cpu1進入臨界區后。將next++, 當cpu1從臨界區域執行完后,將owner++。這時候next和owner都為1,說明臨界區沒有cpu進入。這時候cpu2進入臨界區,將next++, 然后cpu2好像干的活比較多,當cpu3進來后,next++,這時候next已經是3了,當cpu2執行完畢后,owner++,owner的值變為2, 表示讓cpu2進入臨界區,這就保障了各個cpu之間都是先來后到的執行。
ARM32 上spin_lock代碼實現
-
static inline void arch_spin_lock(arch_spinlock_t *lock)
-
{
-
unsigned long tmp;
-
u32 newval;
-
arch_spinlock_t lockval;
-
-
prefetchw(&lock->slock);
-
__asm_ _ __volatile__(
-
"1: ldrex %0, [%3]\n"
-
" add %1, %0, %4\n"
-
" strex %2, %1, [%3]\n"
-
" teq %2, #0\n"
-
" bne 1b"
-
: "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
-
: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
-
: "cc");
-
-
while (lockval.tickets.next != lockval.tickets.owner) {
-
wfe();
-
lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);
-
}
-
smp_mb();
-
}
匯編 | C語言 | 解釋 |
---|---|---|
1: ldrex %0, [%3] | lockval = lock | 讀取鎖的值賦值給lockval |
add %1, %0, %4 | newval = lockval + (1 << 16) | 將next++之后的值存在newval中 |
strex %2, %1, [%3] | lock = newval | 將新的值存在lock中,將是否成功結果存入在tmp中 |
teq %2, #0 | if(tmp == 0) | 判斷上條指令是否成功,如果不成功執行”bne 1b”跳到標號1執行 |
-
while (lockval.tickets.next != lockval.tickets.owner) {
-
wfe() ;
-
lockval .tickets.owner = ACCESS_ONCE(lock->tickets.owner);
-
}
當tickets中的next和owner不相等的時候,說明臨界區在忙, 需要等待。然后cpu會執行wfe指令。當其他cpu忙完之后,會更新owner的值,如果owner的值如果與next值相同,那到next號的cpu執行。
ARM64 上spin_lock代碼實現
-
static inline void arch_spin_lock(arch_spinlock_t *lock)
-
{
-
unsigned int tmp;
-
arch_spinlock_t lockval, newval;
-
-
asm volatile(
-
/* Atomically increment the next ticket. */
-
" prfm pstl1strm, %3\n"
-
"1: ldaxr %w0, %3\n"
-
" add %w1, %w0, %w5\n"
-
" stxr %w2, %w1, %3\n"
-
" cbnz %w2, 1b\n"
-
/* Did we get the lock? */
-
" eor %w1, %w0, %w0, ror #16\n"
-
" cbz %w1, 3f\n"
-
/*
-
* No: spin on the owner. Send a local event to avoid missing an
-
* unlock before the exclusive load.
-
*/
-
" sevl\n"
-
"2: wfe\n"
-
" ldaxrh %w2, %4\n"
-
" eor %w1, %w2, %w0, lsr #16\n"
-
" cbnz %w1, 2b\n"
-
/* We got the lock. Critical section starts here. */
-
"3:"
-
: "=&r" (lockval), "=&r" (newval), "=&r" (tmp), "+Q" (*lock)
-
: "Q" (lock->owner), "I" (1 << TICKET_SHIFT)
-
: "memory");
-
}
-
匯編 | C語言 | 解釋 |
---|---|---|
prfm pstl1strm, %3 | 將lock變量讀到cache,增加訪問速度 | |
1: ldaxr %w0, %3 | lockval = lock | 將lock的值賦值給lockval |
add %w1, %w0, %w5 | newval=lockval + (1 << 16) | 將lock中的next++, 然后將結果賦值給newval |
stxr %w2, %w1, %3 | lock = newval | 將newval賦值給lock,同時將是否設置成功結果存放到tmp |
cbnz %w2, 1b | if(tmp != 0)goto 1 | 如果tmp不為0,跳到標號1執行 |
eor %w1, %w0, %w0, ror #16 | if(next == owner) | 判斷next是否等於owner |
cbz %w1, 3f | if(newval == 0) | 進入臨界區 |
2: wfe | 自旋等待 | |
ldaxrh %w2, %4 | tmp = lock->owner | 獲取當前的Owner值存放在tmp中 |
eor %w1, %w2, %w0, lsr #16 | if(next == owner) | 判斷next是否等於owner |
cbnz %w1, 2b | 如果不等跳到標號2自旋,負責進入臨界區域 |
ARM64 上spin_unlock代碼實現
-
static inline void arch_spin_unlock(arch_spinlock_t *lock)
-
{
-
asm volatile(
-
" stlrh %w1, %0\n"
-
: "=Q" (lock->owner)
-
: "r" (lock->owner + 1)
-
: "memory");
-
}
解鎖的操作相對簡單,就是給owner執行加1的操作。
http://blog.csdn.net/longwang155069/article/details/52211024
讀寫鎖引入
讀寫鎖的基本原理
加鎖操作
- 假設當前臨界區沒有任何進程,這時候read進程或者write進程都可以進來,但是只能是其一
- 如果當前臨界區只有一個read進程,這時候任意的read進程都可以進入,但是write進程不能進入
- 如果當前臨界區只有一個write進程,這時候任何read/write進程都無法進入。只能自旋等待
- 如果當前當前臨界區有好多個read進程,同時read進程依然還會進入,這時候進入的write進程只能等待。直到臨界區一個read進程都沒有,才可進入
解鎖操作
- 如果在read進程離開臨界區的時候,需要根據情況決定write進程是否需要進入。只有當臨界區沒有read進程了,write進程方可進入。
- 如果在write進程離開臨界區的時候,無論write進程或者read進程都可進入臨界區,因為write進程是排它的。
讀寫鎖的定義
- typedef struct {
- arch_rwlock_t raw_lock;
- } rwlock_t;
- typedef struct {
- volatile unsigned int lock;
- } arch_rwlock_t;
讀寫鎖API
加鎖API
- read_lock(lock)/write_lock(lock) #獲取指定的鎖
- read_trylock(lock)/write_trylock(lock) #嘗試獲取鎖,如果失敗不spin,直接返回
- read_lock_irq(lock)/write_lock_irq(lock) #獲取指定的鎖,同時關掉本地cpu中斷
- read_lock_irqsave(lock, flags)/write_lock_irqsave(lock, flags) #保存本地cpu的irq標志,然后關掉cpu中斷,獲取指定鎖
- read_lock_bh(lock)/read_lock_bh(lock) #獲取指定的鎖,同時關掉中斷下半部(bottom half)
解鎖API
- read_unlock(lock)/write_unlock(lock) #釋放指定的鎖
- read_unlock_irq(lock)/write_unlock_irq(lock) #釋放指定的鎖,同時使能cpu中斷
- read_unlock_irqrestore/write_unlock_irqrestore #釋放鎖,同時使能cpu中斷,恢復cpu的標識
- read_unlock_bh/write_unlock_bh #釋放鎖,同時使能cpu中斷的下半部
讀寫鎖的實現
寫入者加鎖操作:
- /*
- * Write lock implementation.
- *
- * Write locks set bit 31. Unlocking, is done by writing 0 since the lock is
- * exclusively held.
- *
- * The memory barriers are implicit with the load-acquire and store-release
- * instructions.
- */
- static inline void arch_write_lock(arch_rwlock_t *rw)
- {
- unsigned int tmp;
- asm volatile(
- " sevl\n"
- "1: wfe\n"
- "2: ldaxr %w0, %1\n"
- " cbnz %w0, 1b\n"
- " stxr %w0, %w2, %1\n"
- " cbnz %w0, 2b\n"
- : "=&r" (tmp), "+Q" (rw->lock)
- : "r" (0x80000000)
- : "memory");
- }
- " sevl\n"
- "1: wfe\n"
- 2: ldaxr %w0, %1\n
- cbnz %w0, 1b
- stxr %w0, %w2, %1
- cbnz %w0, 2b
寫入者解鎖操作:
- static inline void arch_write_unlock(arch_rwlock_t *rw)
- {
- asm volatile(
- " stlr %w1, %0\n"
- : "=Q" (rw->lock) : "r" (0) : "memory");
- }
讀取者加鎖操作:
- /*
- * Read lock implementation.
- *
- * It exclusively loads the lock value, increments it and stores the new value
- * back if positive and the CPU still exclusively owns the location. If the
- * value is negative, the lock is already held.
- *
- * During unlocking there may be multiple active read locks but no write lock.
- *
- * The memory barriers are implicit with the load-acquire and store-release
- * instructions.
- */
- static inline void arch_read_lock(arch_rwlock_t *rw)
- {
- unsigned int tmp, tmp2;
- asm volatile(
- " sevl\n"
- "1: wfe\n"
- "2: ldaxr %w0, %2\n"
- " add %w0, %w0, #1\n"
- " tbnz %w0, #31, 1b\n"
- " stxr %w1, %w0, %2\n"
- " cbnz %w1, 2b\n"
- : "=&r" (tmp), "=&r" (tmp2), "+Q" (rw->lock)
- :
- : "memory");
- }
- 2: ldaxr %w0, %2
- add %w0, %w0, #1
- tbnz %w0, #31, 1b
- stxr %w1, %w0, %2
- cbnz %w1, 2b
讀取者解鎖操作:
- static inline void arch_read_unlock(arch_rwlock_t *rw)
- {
- unsigned int tmp, tmp2;
- asm volatile(
- "1: ldxr %w0, %2\n"
- " sub %w0, %w0, #1\n"
- " stlxr %w1, %w0, %2\n"
- " cbnz %w1, 1b\n"
- : "=&r" (tmp), "=&r" (tmp2), "+Q" (rw->lock)
- :
- : "memory");
- }
- 1: ldxr %w0, %2
- sub %w0, %w0, #1
- stlxr %w1, %w0, %2
- cbnz %w1, 1b