通常我們代碼中的a = a + 1這樣的一行語句,翻譯成匯編后蘊含著3條指令:
ldr x0, &aadd x0,x0,#1str x0,&a
即 (1)從內(nèi)存中讀取a變量到X0寄存器 (2)X0寄存器加1 (3)將X0寫入到內(nèi)存a中
既然是3條指令,那么就有可能并發(fā),也就意味著返回的結(jié)果可能不是預(yù)期的。
然后在linux kernel的操作系統(tǒng)中,提供訪問原子變量的函數(shù),用來解決上述問題。其中部分原子操作的API如下:
atomic_read atomic_add_return(i,v) atomic_add(i,v) atomic_inc(v) atomic_add_unless(v,a,u) atomic_inc_not_zero(v) atomic_sub_return(i,v) atomic_sub_and_test(i,v) atomic_sub(i,v) atomic_dec(v) atomic_cmpxchg(v,舊,新)
那么操作系統(tǒng)(僅僅是軟件而已)是如何保證原子操作的呢?(還是得靠硬件),硬件原理是什么呢?
以上的那些API函數(shù),在底層調(diào)用的其實都是如下__lse_atomic_add_return##name宏的封裝,這段代碼中最核心的也就是ldadd指令了,這是armv8.1增加的LSE(Large System Extension)feature。
(linux/arch/arm64/include/asm/atomic_lse.h)static inline int __lse_atomic_add_return##name(int i, atomic_t *v) { u32 tmp; asm volatile( __LSE_PREAMBLE ” ldadd” #mb ” %w[i], %w[tmp], %[v]” ” add %w[i], %w[i], %w[tmp]” : [i] “+r” (i), [v] “+Q” (v->counter), [tmp] “=&r” (tmp) : “r” (v) : cl); return i; }
那么系統(tǒng)如果沒有LSE擴展呢,即armv8.0,其實現(xiàn)的原型如下所示,這段代碼中最核心的也就是ldxr、stxr指令了。
(linux/arch/arm64/include/asm/atomic_ll_sc.h)static inline void __ll_sc_atomic_##op(int i, atomic_t *v){ unsigned long tmp; int result; asm volatile(“// atomic_” #op “” __LL_SC_FALLBACK( ” prfm pstl1strm, %2″ “1: ldxr %w0, %2″ ” ” #asm_op ” %w0, %w0, %w3″ ” stxr %w1, %w0, %2″ ” cbnz %w1, 1b”) : “=&r” (result), “=&r” (tmp), “+Q” (v->counter) : __stringify(constraint) “r” (i)); }
那么在armv8.0之前呢,如armv7是怎樣實現(xiàn)的?如下所示, 這段代碼中最核心的也就是ldrex、strex指令了。
(linux/arch/arm/include/asm/atomic.h)static inline void atomic_##op(int i, atomic_t *v) { unsigned long tmp; int result; prefetchw(&v->counter); __asm__ __volatile__(“@ atomic_” #op “” “1: ldrex %0, [%3]” ” ” #asm_op ” %0, %0, %4″ ” strex %1, %0, [%3]” ” teq %1, #0″ ” bne 1b” : “=&r” (result), “=&r” (tmp), “+Qo” (v->counter) : “r” (&v->counter), “Ir” (i) : “cc”); }
總結(jié):
在很早期,使用arm的exclusive機制來實現(xiàn)的原子操作,exclusive相關(guān)的指令也就是ldrex、strex了,但在armv8后,exclusive機制的指令發(fā)生了變化變成了ldxr、stxr。但是又由于在一個大系統(tǒng)中,處理器是非常多的,競爭也激烈,使用獨占的存儲和加載指令可能要多次嘗試才能成功,性能也就變得很差,在armv8.1為了解決該問題,增加了ldadd等相關(guān)的原子操作指令。
更多l(xiāng)inux內(nèi)核視頻教程文檔資料免費領(lǐng)取后臺私信【內(nèi)核】自行獲取.
Linux內(nèi)核源碼/內(nèi)存調(diào)優(yōu)/文件系統(tǒng)/進程管理/設(shè)備驅(qū)動/網(wǎng)絡(luò)協(xié)議棧-學(xué)習(xí)視頻教程-騰訊課堂
自旋鎖
早期旋鎖的設(shè)計
早期的spinlock的設(shè)計是鎖的擁有者加鎖時將鎖的值設(shè)置為1,釋放鎖時將鎖的值設(shè)置為0,這樣做的缺點是會出現(xiàn) 先來搶占鎖的進程一直搶占不到鎖,而后來的進程可能一來 就能獲取到鎖。導(dǎo)致這個原因的是先搶占的進程和后搶占的進程在搶占鎖時并沒有一個先后關(guān)系,最終就是離鎖所在的內(nèi)存最近的cpu節(jié)點就有更多的機會搶占鎖,離鎖所在內(nèi)存遠的節(jié)點可能一直搶占不到。
新版旋鎖設(shè)計
為了解決這個spinlock的不公平問題,linux 2.6.25內(nèi)核以后,spinlock采用了一種“FIFO ticket-based”算法的spinlock機制,可以很好的實現(xiàn)先來先搶占的思想。具體的做法如下:
我在舉個例子,如下:
T1 : 進程1調(diào)用spin_lock,此時next=0, owner=0獲得該鎖,在arch_spin_lock()底層實現(xiàn)中,會next++ T2 : 進程2調(diào)用spin_lock,此時next=1, owner=0沒有獲得該鎖,while(1)中調(diào)用wfe指令standby在那里,等待owner==next成立. T3 : 進程3調(diào)用spin_lock,此時next=2, owner=0沒有獲得該鎖,while(1)中調(diào)用wfe指令standby在那里,等待owner==next成立. T4&T5 : 進程1調(diào)用spin_unlock,此時owner++,即owner=1,接著調(diào)用sev指令,讓進程2和進程3退出standby狀態(tài),走while(1)流程,重新檢查owner==next條件。此時進程2條件成立,進程3繼續(xù)等待。進程2獲得該鎖,進程3繼續(xù)等待。
Linux Kernel中的SpinLock的實現(xiàn)
(linux/include/linux/spinlock.h)static __always_inline void spin_unlock(spinlock_t *lock){ raw_spin_unlock(&lock->rlock);}static __always_inline void spin_lock(spinlock_t *lock){ raw_spin_lock(&lock->rlock);}(linux/include/linux/spinlock.h)#define raw_spin_lock_irq(lock) _raw_spin_lock_irq(lock)#define raw_spin_lock_bh(lock) _raw_spin_lock_bh(lock)#define raw_spin_unlock(lock) _raw_spin_unlock(lock)#define raw_spin_unlock_irq(lock) _raw_spin_unlock_irq(lock)#define raw_spin_lock(lock) _raw_spin_lock(lock)(linux/kernel/locking/spinlock.c)#ifdef CONFIG_UNINLINE_SPIN_UNLOCKvoid __lockfunc _raw_spin_unlock(raw_spinlock_t *lock){ __raw_spin_unlock(lock);}EXPORT_SYMBOL(_raw_spin_unlock);#endif#ifndef CONFIG_INLINE_SPIN_LOCKvoid __lockfunc _raw_spin_lock(raw_spinlock_t *lock){ __raw_spin_lock(lock);}EXPORT_SYMBOL(_raw_spin_lock);#endif(linux/include/linux/spinlock_api_smp.h)static inline void __raw_spin_unlock(raw_spinlock_t *lock){ spin_release(&lock->dep_map, _RET_IP_); do_raw_spin_unlock(lock); preempt_enable();}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);}(linux/include/linux/spinlock.h)static inline void do_raw_spin_unlock(raw_spinlock_t *lock) __releases(lock){ mmiowb_spin_unlock(); arch_spin_unlock(&lock->raw_lock); __release(lock);}static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock){ __acquire(lock); arch_spin_lock(&lock->raw_lock); mmiowb_spin_lock();}
對于arch_spin_lock()、arch_spin_unlock()的底層實現(xiàn),不同的kernel版本也一直在變化。
對于kernel4.4這個版本,還是比較好理解的,最核心的也就是ldaxr、ldaxr獨占指令 ,以及stlrh release指令
(linux/arch/arm64/include/asm/spinlock.h)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. */ ARM64_LSE_ATOMIC_INSN( /* LL/SC */” prfm pstl1strm, %3″”1: ldaxr %w0, %3″” add %w1, %w0, %w5″” stxr %w2, %w1, %3″” cbnz %w2, 1b”, /* LSE atomics */” mov %w2, %w5″” ldadda %w2, %w0, %3″” nop”” nop”” nop” ) /* Did we get the lock? */” eor %w1, %w0, %w0, ror #16″” cbz %w1, 3f” /* * No: spin on the owner. Send a local event to avoid missing an * unlock before the exclusive load. */” sevl””2: wfe”” ldaxrh %w2, %4″” eor %w1, %w2, %w0, lsr #16″” cbnz %w1, 2b” /* We got the lock. Critical section starts here. */”3:” : “=&r” (lockval), “=&r” (newval), “=&r” (tmp), “+Q” (*lock) : “Q” (lock->owner), “I” (1 <owner), "=&r" (tmp) : : "memory");}