很多時候,當一個進程為了等待mutex而剛剛進入睡眠的時候,mutex已經被釋放了,如果能在第一時間感知mutex被釋放那是再好不過的了,解決該問題的方式就是用自旋忙等而不是阻塞等待,是這樣嗎?
關於競態,當初由於不能忍受頻繁的睡眠/喚醒而引入了自旋鎖,然後又因為自旋鎖在即時系統中會導致其它進程長時間延遲造成輸送量下降而在即時系統中又恢複 了睡眠/喚醒,即時系統中的首要特性不是節省開銷,而是保證運行,睡眠/喚醒機制保證了爭搶鎖的進程不會影響到別的進程從而保證了輸送量不會被降低,但是 自旋鎖用mutex實現的代價是客觀存在的,它必須實現優先順序繼承或者優先順序置頂以保證不會發生優先順序逆轉和死結,因為現在自旋鎖可以睡眠了,而核心又是 可以搶佔的,一旦有高優先順序的進程就緒,那麼它就會搶佔低優先順序的進程,這是如果低優先順序的進程正好持有那一把鎖,那麼就會被阻塞而不能運行(單cpu或 者進程/cpu綁定的情況),此時一個中間優先順序的進程搶佔了低優先順序的進程,那麼這會導致高優先順序的進程的阻塞時間過於長,在即時系統中必須避免這種情 況,因此在即時系統核心中,比如linux核心中,就實現了優先順序繼承協議-PIP,協議的實現引入了大量的資料結構和演算法,這就引入了管理開銷,這些開 銷會抵消掉一部分睡眠/喚醒帶來的輸送量的提高和其它進程延遲的降低,因此,下面的一杆秤就需要在這二者之間撥弄秤砣了。
傳統自旋鎖的實現保證了自旋鎖的持有人不會被打斷,這就可以保證即使高優先順序的進程被破延遲也只是在自旋,而自旋時間內持有鎖的進程會一直運行,而且運行 邏輯一直和這把鎖有關,即使它釋放了鎖之後沒有被等待者搶到,那麼搶到鎖的進程也不會做別的事(在ticket spin lock中這種混亂得到了改善),雖然延遲是有的,但是起碼都是在圍繞著鎖作正經事而不會被別的進程打斷,如果你真的在持有自旋鎖的時候調用了一個 schedule,那麼只能怪寫代碼的人了。因此傳統的自旋鎖不用任何開銷就可以避免優先順序逆轉之類的令人難堪的局面,是的,沒有任何開銷,把搶佔一關鎖 一占完事,剩下的就盡情執行吧,不會被打擾,然而這樣的話雖然避免了優先順序逆轉帶來的爭搶鎖高優先順序的進程延遲但是會引入不爭搶鎖的所有高優先順序進程的延 遲,因為自旋鎖簡單的關閉了搶佔(簡單無開銷)。因此傳統的自旋鎖和睡眠/喚醒機制都不適合即時系統,帶有優先順序繼承協議的mutex機制實現的自旋鎖是 可以的,但是管理優先順序繼承協議的資料結構和演算法也夠嗆,這麼多的但是降到底如何是好,現在有三種方案實現即時系統自旋鎖,第一就是傳統自旋鎖,第二就是 傳統的睡眠/喚醒機制,第三就是實現PIP的mutex機制,前兩種都基本沒有管理開銷但是因為影響系統輸送量和延遲,第二種還會引入睡眠/喚醒開銷,這 導致這兩種不能用,第三個方案由於不怎麼影響系統延遲但是運行開銷很大,運行開銷有睡眠/喚醒的開銷,管理複雜資料結構和演算法的開銷。因此,將這三種方案 的優勢組合然後避免它們的劣勢是最好的結果了,其實這三種方案正交化一下就是:自旋鎖,睡眠/喚醒,PIP協議,其中PIP是不可省略的,因此選擇自旋鎖 的低開銷和睡眠/喚醒的高輸送量,因此結果就是自適應自旋鎖,也就是說它會在某些情況下自旋而在另外一些情況下睡眠,這就是它的優勢。那麼在何種情況下自 旋呢?理想的情況就是自動學習,起初可能會很影響效能,畢竟要學習嘛,多次嘗試自旋以後,會得到一些統計值,比如得到鎖的平均延遲,平均自旋次數,n次自 旋內得到鎖的次數,系統根據這些統計值決定下一次是自旋還是睡眠。然而這種實現合理嗎?看似很合理啊,也很智能,幾乎不用怎麼配置就可以自動啟動並執行很好, 但是想想看,這畢竟是在核心,核心不是秀演算法的地方,智能的,複雜的演算法還是在使用者空間秀比較好,核心演算法和資料結構的特徵就是簡單,高效,因此核心實現 的自適應鎖還是要另外開闢新的方案。
linux的核心高效的原因之一就是它採用了約定的方式來約束開發,而不是強制的方式在運行時驗證,這樣會節省很大的管理開銷,在linux中,進大門的 人很自覺,壞人一般不會進入,核心信任能進入的都是好人,因此就節省了兩個門衛,同時也節省了查證的時間,linux為何如此信任代碼呢?因為linux 核心是開源的,任何不合法的東西都逃不過開發人員的慧眼。靜態東西總是比動態更加高效,因為最起碼它節省了計算的時間,那麼對於自適應鎖,linux是 怎麼約定的呢?其實只要是鎖,linux都會建議擁有者盡量趕快完畢和鎖相關的事然後釋放掉鎖,畢竟鎖是公用的,因此linux核心相信所有的代碼都接受 了這個建議約定,因此,一個持有鎖的執行緒會在最快的時間內放掉鎖,僅此一點還不能讓爭搶鎖的執行緒為之自旋,mutex實現的自旋鎖是可以被搶佔的,當 然還是不建議主動睡眠,mutex實現的自旋鎖可睡眠可被搶佔只是為了即時,為了高優先順序的進程不自旋可以馬上搶佔它,它可不是睡眠的溫床,因此,不要在 mutex自旋鎖中睡眠。因為持有鎖的進程可能會被搶佔,那麼在這種情況下爭搶鎖的進程自旋是沒有多大幾率成功獲得鎖的,因為會涉及到很複雜的操作,比如 被搶佔的進程可能被調度到別的cpu上,但是不知道是哪個cpu上,真的被調度到別的cpu上了嗎?也就是它真的正在運行嗎(努力執行釋放鎖)?不得而 知,因此補丁中會有如下判定:
while (waiter->spin && !lock->waiters) {
struct thread_info *owner;
owner = ACCESS_ONCE(lock->owner); //在這個owner上自旋
if (owner && !mutex_spin_on_owner(waiter, owner))
break;
cpu_relax(); //自旋
}
以下是mutex_spin_on_owner的邏輯
while (waiter->spin && !lock->waiters) {
if (lock->owner != owner) //owner換了,重新開始判定,進入上一層自旋
break;
if (task_thread_info(rq->curr) != owner) { //鎖的owner沒有運行在owner原來的cpu上,不再自旋
ret = 0;
break;
}
if (need_resched()) { //爭搶鎖的cpu上有更高優先順序的進程就緒,不再自旋
ret = 0;
break;
}
cpu_relax(); //自旋
}
我們看到有兩層的自旋,外層的自旋是為了在不同owner中爭搶鎖,因為一個owner把鎖釋放了,並不代表我們就是下一個得到鎖的,所以需要外層的自 旋,內層的自旋才是真正的自旋。其實實現自適應自旋鎖的原始方式就是自旋counter次,然後睡眠,這不過是一種簡單的實現方式罷了,沒有添加任何的策 略,沒有跟蹤鎖的owner的情況。
可以看出,系統設計中充滿了矛盾,關鍵不是解除矛盾,矛盾是解除不了的,解除矛盾意味著功能的缺失,這也比較符合我們的世界,怕翻車就走路...設計當中 最最關鍵的就是權衡矛盾,最後得到最好的折中,比如時間和空間就是一對矛盾,用時間換空間還是用空間換時間取決於你的應用,另外,對於我今天討論的自旋鎖 的實現,延遲和輸送量以及管理開銷也是一對矛盾,傳統的自旋鎖輸送量小,但是造成其他進程延遲,而睡眠/喚醒輸送量大,也不會影響太多的延遲,但是開銷過 於大,因此必須做好權衡,取它們的優點和缺點作比較。在自適應自旋鎖的設計中,輸送量和延遲的矛盾不在自旋造成的輸送量下降和睡眠/喚醒本身的延遲,而在於自適應鎖帶來的別的進程的延遲減少和管理複雜資料結構的開銷造成的輸送量下降之間的矛盾。