Kernel同步機制的底層實現(xiàn)(超詳細(xì)~)

原子操作
通常我們代碼中的a = a + 1這樣的一行語句,翻譯成匯編后蘊含著3條指令:
即?
(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。
那么系統(tǒng)如果沒有LSE擴展呢,即armv8.0,其實現(xiàn)的原型如下所示,這段代碼中最核心的也就是ldxr、stxr指令了。
那么在armv8.0之前呢,如armv7是怎樣實現(xiàn)的?如下所示, 這段代碼中最核心的也就是ldrex、strex指令了。
總結(jié):
在很早期,使用arm的exclusive機制來實現(xiàn)的原子操作,exclusive相關(guān)的指令也就是ldrex、strex了,但在armv8后,exclusive機制的指令發(fā)生了變化變成了ldxr、stxr。但是又由于在一個大系統(tǒng)中,處理器是非常多的,競爭也激烈,使用獨占的存儲和加載指令可能要多次嘗試才能成功,性能也就變得很差,在armv8.1為了解決該問題,增加了ldadd等相關(guān)的原子操作指令。
【文章福利】小編推薦自己的Linux內(nèi)核技術(shù)交流群:【891587639】整理了一些個人覺得比較好的學(xué)習(xí)書籍、視頻資料共享在群文件里面,有需要的可以自行添加哦?。。。ê曨l教程、電子書、實戰(zhàn)項目及代碼)? ? ? ?


早期旋鎖的設(shè)計
早期的spinlock的設(shè)計是鎖的擁有者加鎖時將鎖的值設(shè)置為1,釋放鎖時將鎖的值設(shè)置為0,這樣做的缺點是會出現(xiàn) 先來搶占鎖的進(jìn)程一直搶占不到鎖,而后來的進(jìn)程可能一來 就能獲取到鎖。導(dǎo)致這個原因的是先搶占的進(jìn)程和后搶占的進(jìn)程在搶占鎖時并沒有一個先后關(guān)系,最終就是離鎖所在的內(nèi)存最近的cpu節(jié)點就有更多的機會搶占鎖,離鎖所在內(nèi)存遠(yuǎn)的節(jié)點可能一直搶占不到。
新版旋鎖設(shè)計
為了解決這個spinlock的不公平問題,linux 2.6.25內(nèi)核以后,spinlock采用了一種“FIFO ticket-based”算法的spinlock機制,可以很好的實現(xiàn)先來先搶占的思想。具體的做法如下:
spinlock的核心字段有owner和next,在初始時,owner=next=0
當(dāng)?shù)谝粋€進(jìn)程搶占spinlock時,會在進(jìn)程函數(shù)本地保存下next的值,也就是next=0,并將spinlock的next字段加1;
當(dāng)獲取spinlock的進(jìn)程的本地next和spinlock的owner相等時,該進(jìn)程就獲取到spinlock;
由于第一個進(jìn)程本地的next=0,并且spinlock的owner為0,所以第一個CPU獲取到spinlock;
接著當(dāng)?shù)诙€進(jìn)程搶占spinlock,此時spinlock的next值為1,保存到本地,然后將spinlock的next字段加1。而spinlock的owner字段依然為0,第二個進(jìn)程的本地next 不等于spinlock的owner,所以一直自旋等待spinlock;
第三個進(jìn)程搶占spinlock,得到本地next值為2,然后將spinlock的next字段加1。此時spinlock的owner字段還是為0,所以第三個進(jìn)程自旋等待。
當(dāng)?shù)谝粋€進(jìn)程處理完臨界區(qū)以后,就釋放spinlock,執(zhí)行的操作是將spinlock的owner字段加1;
由于第二個進(jìn)程和第三個進(jìn)程都還在等待spinlock,他們會不停第獲取spinlock的owner字段,并和自己本地的next值進(jìn)行比較。當(dāng)?shù)诙€進(jìn)程發(fā)現(xiàn)自己的next值和spinlock的owner字段相等時(此時next == owner == 2),第二個進(jìn)程就獲取到spinlock。第三個進(jìn)程的本地next值是3,和spinlock的owner字段不相等,所以繼續(xù)等待;
只有在第二個進(jìn)程釋放了spinlock,就會將spinlock的owner字段加1,第三個進(jìn)程才有機會獲取spinlock。
我在舉個例子,如下:

T1 : 進(jìn)程1調(diào)用spin_lock,此時next=0, owner=0獲得該鎖,在arch_spin_lock()底層實現(xiàn)中,會next++?
T2 : 進(jìn)程2調(diào)用spin_lock,此時next=1, owner=0沒有獲得該鎖,while(1)中調(diào)用wfe指令standby在那里,等待owner==next成立.?
T3 : 進(jìn)程3調(diào)用spin_lock,此時next=2, owner=0沒有獲得該鎖,while(1)中調(diào)用wfe指令standby在那里,等待owner==next成立.?
T4&T5 : 進(jìn)程1調(diào)用spin_unlock,此時owner++,即owner=1,接著調(diào)用sev指令,讓進(jìn)程2和進(jìn)程3退出standby狀態(tài),走while(1)流程,重新檢查owner==next條件。此時進(jìn)程2條件成立,進(jìn)程3繼續(xù)等待。進(jìn)程2獲得該鎖,進(jìn)程3繼續(xù)等待。
Linux Kernel中的SpinLock的實現(xiàn)

對于arch_spin_lock()、arch_spin_unlock()的底層實現(xiàn),不同的kernel版本也一直在變化。
對于kernel4.4這個版本,還是比較好理解的,最核心的也就是ldaxr、ldaxr獨占指令 ,以及stlrh release指令
