Linux多核並行編程關鍵技術

多核並行編程的背景

在摩爾定律失效以前,提高處理器性能經過主頻提高、硬件超線程等技術就能知足應用須要。隨着主頻提高慢慢接近撞上光速這道牆,摩爾定律開始逐漸失效,多核集成爲處理器性能提高的主流手段。如今市面上已經很難看到單核的處理器,就是這一發展趨勢的佐證。要充分發揮多核豐富的計算資源優點,多核下的並行編程就不可避免,Linux kernel就是一典型的多核並行編程場景。但多核下的並行編程卻挑戰多多。編程

多核並行編程的挑戰

目前主流的計算機都是馮諾依曼架構,即共享內存的計算模型,這種過程計算模型對並行計算並不友好。下圖是一種典型的計算機硬件體系架構。數組

 

 這種架構中,有以下設計特色:緩存

  • 多個CPU核改善處理器的計算處理能力;
  • 多級cache改善CPU訪問主存的效率;
  • 各個CPU都有本地內存(NUMA(非一致性內存訪問)),進一步改善CPU訪問主存的效率;
  • store buffer模塊改善cache write因爲應答延遲而形成的寫停頓問題;
  • invalidate queue模塊改善使無效應答的時延,把使無效命令放入queue後就當即發送應答;
  • 外設DMA支持直接訪問主存,改善CPU使用效率;

這些硬件體系設計特色也引入不少問題,最大的問題就是cache一致性問題和亂序執行問題。數據結構

cache一致性問題由cache一致性協議MESI解決,MESI由硬件保證,對軟件來講是透明的。MESI協議保證全部CPU對單個cache line中單個變量修改的順序保持一致,但不保證不一樣變量的修改在全部CPU上看到的是相同順序。這就形成了亂序。不只如此,亂序的緣由還有不少:架構

  • store buffer引發的延遲處理,會形成亂序;
  • invalidate queue引發的延遲處理,會形成亂序;
  • 編譯優化,會形成亂序;
  • 分支預測、多流水線等CPU硬件優化技術,會形成亂序;
  • 外設DMA,會形成數據亂序;

這種狀況形成,就連簡單的++運算操做的原子性都沒法保證。這些問題必須採用多核並行編程新的技術手段來解決。併發

多核並行編程關鍵技術

鎖技術

Linux kernel提供了多種鎖機制,如自旋鎖、信號量、互斥量、讀寫鎖、順序鎖等。各類鎖的簡單比較以下,具體實現和使用細節這裏就不展開了,能夠參考《Linux內核設計與實現》等書的相關章節。函數式編程

  • 自旋鎖,不休眠,無進程上下文切換開銷,能夠用在中斷上下文和臨界區小的場合;
  • 信號量,會休眠,支持同時多個併發體進入臨界區,能夠用在可能休眠或者長的臨界區的場合;
  • 互斥量,相似與信號量,但只支持同時只有一個併發體進入臨界區;
  • 讀寫鎖,支持讀併發,寫寫/讀寫間互斥,讀會延遲寫,對讀友好,適用讀側重場合;
  • 順序鎖,支持讀併發,寫寫/讀寫間互斥,寫會延遲讀,對寫友好,適用寫側重場合;

鎖技術雖然能有效地提供並行執行下的競態保護,但鎖的並行可擴展性不好,沒法充分發揮多核的性能優點。鎖的粒度太粗會限制擴展性,粒度太細會致使巨大的系統開銷,並且設計難度大,容易形成死鎖。除了併發可擴展性差和死鎖外,鎖還會引入不少其餘問題,如鎖驚羣、活鎖、飢餓、不公平鎖、優先級反轉等。不過也有一些技術手段或指導原則能解決或減輕這些問題的風險。函數

  • 按統一的順序使用鎖(鎖的層次),解決死鎖問題;
  • 指數後退,解決活鎖/飢餓問題;
  • 範圍鎖(樹狀鎖),解決鎖驚羣問題;
  • 優先級繼承,解決優先級反轉問題 ;

原子技術

原子技術主要是解決cache和內存不一致性和亂序執行對原子訪問的破壞問題。Linux kernel中主要的原子原語有:性能

  • ACCESS_ONCE()、READ_ONCE() and WRITE_ONCE():禁止編譯器對數據訪問的優化,強制從內存而不是緩存中獲取數據;
  • barrier():亂序訪問內存屏障,限制編譯器的亂序優化;
  • smb_wmb():寫內存屏障,刷新store buffer,同時限制編譯器和CPU的亂序優化;
  • smb_rmb():讀內存屏障,刷新invalidate queue,同時限制編譯器和CPU的亂序優化;
  • smb_mb():讀寫內存屏障,同時刷新store buffer和invalidate queue,同時限制編譯器和CPU的亂序優化;
  • atomic_inc()/atomic_read()等:整型原子操做;

嚴格來講,Linux kernel做爲系統軟件,實現受硬件影響很大,不一樣硬件有不一樣的內存模型,所以,不一樣於高級語言,Linux kernel的原子原語語義並無一個統一模型。好比在SMP的ARM64 CPU上,barrier、smb_wmb、smb_rmb的實現與smb_mb都是同樣的,都是volatile ("" ::: "memory")。優化

另外,再多提一句的是,atomic_inc()原語爲了保證原子性,須要對cache進行刷新,而緩存行在多核體系下傳播至關耗時,其多核下的並行可擴展性差。

無鎖技術

上一小節中所提到的原子技術,是無鎖技術中的一種,除此以外,無鎖技術還包括RCUHazard pointer等。值得一提的是,這些無鎖技術都基於內存屏障實現的。

  • Hazard pointer主要用於對象的生命週期管理,相似引用計數,但比引用計數有更好的並行可擴展性;
  • RCU適用的場景不少,其能夠替代:讀寫鎖、引用計數、垃圾回收器、等待事物結束等,並且有更好的並行擴展性。但RCU也有一些不適用的場景,如寫側重;臨界區長;臨界區內休眠等場景。

不過,全部的無鎖原語也只能解決讀端的並行可擴展性問題,寫端的並行可擴展性只能經過數據分割技術來解決。

數據分割技術

分割數據結構,減小共享數據,是解決並行可擴展性的根本辦法。對分割友好(即並行友好)的數據結構有:

  • 數組
  • 哈希表
  • 基樹(Radix Tree)/稀疏數組
  • 跳躍列表(skip list)

使用這些便於分割的數據結構,有利於咱們經過數據分割來改善並行可擴展性。

除了使用合適的數據結構外,合理的分割指導規則也很重要:

  • 讀寫分割:以讀爲主的數據與以寫爲主的數據分開;
  • 路徑分割:按獨立的代碼執行路徑來分割數據;
  • 專項分割:把常常更新的數據綁定到指定的CPU/線程中;
  • 全部權分割:按CPU/線程個數對數據結構進行分割,把數據分割到per-cpu/per-thread中;

4種分割規則中,全部權分割是分割最完全的。

以上這些多核並行編程內容基本上涵蓋了Linux kernel中全部的併發編程關鍵技術。固然並行編程還有不少其餘技術沒有應用到Linux kernel中的,如無反作用的並行函數式編程技術(Erlang/Go等)、消息傳遞、MapReduce等等。

相關文章
相關標籤/搜索