1. 程式人生 > 其它 >os方向論文推薦:NrOS: Effective Replication and Sharing in an Operating System

os方向論文推薦:NrOS: Effective Replication and Sharing in an Operating System

關於論文NrOS: Effective Replication and Sharing in an Operating System的閱讀筆記

NrOS: Effective Replication and Sharing in an Operating System

源自osdi2021

整體總結:

第一張圖是關於節點複製的,層次關係是每個NRkernel放在不同的NUMA節點上,每個NRkernel下又有多個cpu,這些cpu之間共享記憶體。不同的NRkernel之間因為在不同的NUMA節點上,所以核心之間的同步用NRLog來實現(主要減少了跨核心以及跨NUMA之間的訊息互動)。

  

第二張圖是關於NROS設計,主要關注三個方面:虛擬記憶體、檔案系統、程序排程

虛擬記憶體核心問題在1.修改記憶體對映時已有其他程序訪問了改記憶體對映,這一問題使用resolve操作來解決。2.對對映的修改會造成TLB表項不一致,這一點通過修改後傳送IPI讓每個節點更新從而放棄失效的TLB來完成。

檔案系統的核心問題在處理1.檔案描述符問題(放入使用者空間,核心空間之間進行對地址的讀寫)2.大檔案與日誌大小的衝突(較大的檔案在日誌中進行索引)3.使用者空間緩衝區可能在所有核心同步之前被修改的問題(將緩衝區複製入核心)4.讀寫衝突和增加併發性(使用CNR)

程序排程主要涉及1.NR將執行緒分配給cpu核心層面只保留對應的對映表進行粗粒度操作2.需要寫的程序寫入部分記憶體分配問題(在建立程序前先分配好讀寫的記憶體,在使用時直接對映到預先分配好的記憶體即可)

這篇文章針對的主要問題是在多核狀態下,針對NUMA架構的核心設計問題。在多核併發的情況下,並且為了減少多核對匯流排資源的搶奪,便產生了NUMA架構。與此同時,不同核心之間的一致性問題以及記憶體讀寫鎖問題一直備受關注,目前針對NUMA架構設計的核心主要有兩種:一是單核心架構,核心思路是多個cpu使用同一個核心,例如linux,採用的思路是使用極其複雜的資料結構和讀寫鎖來提高整個系統的併發性,但是隨著核心的增多這使得整個系統的拓展性降低——增加cpu的代價是整個系統的冗雜和沉重的負載問題。二是多核心結構,對每個NUMA節點分配一個核心,這樣操作的結果是方便了整個系統的可拓展性但是使得不同的NUMA節點之間交流的效率變低使得整個系統的訊息傳遞變得十分複雜。為了解決可拓展性和複雜性的矛盾,文章參考了分散式系統節點複製的想法,提出了NUMA節點之間進行核心複製的方法來同時滿足可拓展性和簡潔性,並設計了NROS被證明各種效能上優於linux。

核心思路

核心架構如圖,在此前多核設計的模式中,為了方便可擴充套件性,大多避免了共享記憶體的設計,但是NROS採用了共享記憶體的設計(其可擴充套件性將在核心複製中解決),NROS的每個核心是對核心節點複製,這減少了讀取核心狀態時複雜的訊息傳遞。同時為了保持節點之間的一致性,所有NRkernel共享一個NRLog資料結構,每個節點對NRLog不採用實時更新的方式,只在必要時對其進行同步,並且對整體來說,整個系統的讀寫都採用單執行緒的資料結構,從而避免出現一致性錯誤。

整體來說,NROS的設計有以下特徵和有點:

  1. 相對多核心來說,採用了核心複製的模式,簡化了NUMA節點之間訊息傳遞的方式
  2. 在整體層面上使用單執行緒讀寫的資料結構,使得一致性模型簡單並且易於擴充套件
  3. 相對單核心來說,簡化了並行模型的複雜性,同時也提高了系統的擴充套件性

NR(Node Replication)

本文核心設計思路即為節點複製(Node Replication),即在巨集觀層面上使得在同一時刻不同NUMA節點上的核心狀態是一致的,從而減少因核心狀態讀取造成的開銷。並且在此之上使用單執行緒的讀寫資料結構來保持一致性以及用共享記憶體來減少節點之間交換資料所造成的系統開銷。
一些核心概念:

NRLog一個被各個核心節點共享的迴圈資料結構,其每一個條目為每個節點對核心的一次修改,並且NRLog保證操作的順序性。NRLog自身維護一個指標指向尾部,即最新的操作位置,每個NR節點維持一個自身指標指向NRLog的條目,表示本節點目前執行到日誌的位置。每一個節點再寫入NRLog的內容後,其寫入的內容在所有節點都執行之前無法被重複使用(為了保證一致性),所以為了防止NRLog被條目佔滿,每個節點都會有執行緒(程序)定期對自身進行更新從而保證與其他節點的一致性。

Flat combing在NrOS中,flatcombing使得每個NUMA節點的多核中可以共享同一個NR節點(replic),這樣的好處在於同一個NUMA節點中的核可以共享最後一級的核心,同時也為日誌的讀寫提供了方便。

日誌更新(寫入)過程:當一個本地副本節點需要對NRLog進行修改時,向NRLog申請combiner,這個combiner有兩個作用:一是在Flat combing中所提到的對本地的執行緒進行加鎖,保證NR節點本地的一致性,二是作為對NRLog的一個訪問鎖,在將自己對NRLog寫入的條目更新完畢後,講NRLog指標放到尾部,並且將自己的指標也更新到尾部。這種對唯一NRLog加鎖的方式即上文提到的“單執行緒資料結構”,這種方法的好處是使用一個十分簡單可控並且可拓展的方法使各個節點之間保證一致性,但同時在某些極端情況下,這種方法會成為整個系統吞吐量的瓶頸,解決這一問題文章提出了CNR(Concurrent Node Replication,一致性節點複製)講在介紹完NR後詳細介紹。

日誌更新(讀取)過程:對日誌讀取的操作不需要新添NRLog條目,為了保證更新資料的實時性,讀取過程先回對比此時本地指標和NRLog維持指標的位置,並等待combiner,當所有需要寫入的NRNode使用完combiner後,需要讀取的NRNode獲得combiner講自身副本資料進行更新,講指標更新到NRLog尾部,返還combiner。這裡特別需要注意一點,NRLog中日誌的條目必然是線性順序的,並且在所有節點更新完之前,寫入的資料是不可重複使用的,這在保持各個節點之間的一致性過程中十分重要。

具體的讀寫過程以下圖為例:

初始過程:每個核心維持自己的對NRLog的指標,並且NRLog本身維持一個自身的尾部指標作為日誌的最新狀態。

申請combiner:假設節點1的T2程序需要對核心更新,此時節點1申請到了combiner,節點1中T2執行寫入NRLog操作,其他程序阻塞。

寫入NRLog:節點1申請到combiner後,講需要寫入的內容加入NRLog中,並將自己維持的指標更新到日誌尾部。

讀入操作:當節點二讀取到日誌尾部在自己維持的指標之後時,申請combiner,並使自己核心的狀態更新到日誌的最新位置。

關於NR核心的具體資料結構、檔案系統、虛擬核心等放到下週說吧,接下來講一下CNR(Concurrent Node Replication,一致性節點複製)。

CNR(Concurrent Node Replication,一致性節點複製)

首先說明一下CNR出現的原因,因為在NR中,存在一個很大的問題:即所有節點都要共享一個NRLog,並且寫入日誌的過程中單個combiner對日誌操作減少了並行性。

為了解決這一問題,文章提出了CNR(Concurrent Node Replication,一致性節點複製)。

核心思路:

明確兩個概念:commutative & conflicting(可交換的和衝突的),我們可以將核心的操作指令分為可交換的以及衝突的,這裡可交換指可線性化的,即兩條或多條指令併發執行時和其線性執行的結果一致,相反,互相沖突的指令是指交換其執行順序後可能造成其結果不一致的兩條或多條指令。

之後進入正題,CNR和NR的最大區別在於CNR將所有的操作指令根據其可交換性分組,每組指令對應一個NRLog,而CNR中每一個核都對映一個NRLog,並且將自己對核心的更新只記錄在自身的NRLog中。這就解決了以下的一致性問題:例如對記憶體的讀寫操作,當Node1執行Get(k)而Node2執行Write(k,buff)的時候,我們無法決定Node1和Node2誰先執行指令,從而造成的結果是我們無法確定從地址k讀出的資料是不是寫入的buff,而由於每個NRLog之間的指令是可交換的,所以併發執行時不會出現這樣的問題。

當然,如果放線上程層面來看,更準確的說法應該是相互衝突的指令被放在同一個combiner中來執行(在CNR中,combiner變成了保證執行緒與自己的本地log一致性的鎖)。這是一個很聰明的做法,將本應該立即保持一致的不同節點所需要保持一致性的時間要求推遲(總之就是雖然都是順序一致性但是CNR的要求要低一點該怎麼表達我暫時也想不到)。但與此同時,當所有節點需要同步時,整個同步過程將會變得較為複雜。

有關多節點同步的問題:

首先考慮一些必須涉及到全域性的操作:例如對整個檔案系統或記憶體的掃描,抑或是檔案改名等操作,這需要對整個核心進行掃描操作,這個時候我們將面臨兩個問題:1.我們需要保證對整個核心掃描的時候沒有正在更新的combiner 2.我們需要保證我們對核心的掃描均是最新的資料。

為了解決這個問題,文章巧妙設計了一種簡單的操作方法:當掃描開始時,對每個NRNode中加入掃描指令,並且加上scan-lock,保證此時只有掃描操作執行(這樣也避免爭奪log資源造成死鎖)即每一個需要更新的執行緒都等待掃描執行緒釋放scan-lock。關於節點更新問題,類似於全域性操作,不過依舊需要說明一點的是,掃描操作有讀取和更新兩種操作,讀取操作隻影響一個副本,而更新操作需要影響多個副本,這在設定讀寫鎖的問題上比較重要。

NROS具體設計

這周算是整體看完了NrOS的論文,首先是發現上週對作者的整體行文過程沒有搞清楚,以至於上週的一些地方(如CNR部分)是完全靠自己臆想推測出來的。在閱讀全文時發現了自己的問題,這裡不再一一列舉,涉及到的問題會在下文遇到時提出來。

總體設計概要

NrOS的設計主要涉及以下幾個方面:

  1. 實體記憶體設計
  2. 虛擬記憶體設計
  3. 檔案系統設計
  4. 程序分配管理
  5. 日誌同步問題

這裡先對NUMA節點和核心複製做一點簡單的說明,傳統的多核併發的架構需要不同核心以及不同NUMA節點之間進行訊息傳遞,這使得高代價的訊息傳遞(不同核之間以及不同NUMA之間)的時間複雜度與核心的數量之間是n方的關係。針對這件事,NrOS採用了不同的方法:對於同一個NUMA節點之間的不同核心採用共享記憶體的方式進行資訊互動,並且根據NrLog進行節點之間的同步,這就減少了記憶體之間訊息交換的頻率,並且使得訊息互動的複雜度與NUMA數量相關而不是與核心數量相關。

說完了總體的記憶體設計架構之後,我們再來對上面幾點進行詳細的說明。

實體記憶體設計

實體記憶體設計主要考慮兩個方面的問題:物理框架(頁表)的設計以及動態記憶體分配的問題。

先說頁表,在NrOS中,物理頁表是獨立於節點複製的,因為如果將初始化物理頁表這一行為也下放給每個節點,會造成頁表不一致的問題。所以在核心複製之前會先生成一個整體的頁表,之後會將頁表的指標作為引數傳入核心複製的過程中,從而保證每個核心都擁有相同的頁表。

關於快取:每個NUMA將自己記憶體分為4KiB和2MiB的塊,分配給每個核(類似於slap allocation的分法,這裡不在細說)。

關於動態分配記憶體問題,與常規的多核系統將自己動態記憶體分配放在使用者空間不一樣的是,NrOS將動態記憶體分配放在核心空間,將4KiB和2MiB的塊組成連結串列自動為程式動態分配記憶體。如此設計有一個很大的問題是在自動記憶體分配的地方很難做到完美,可能會使後期出現大量的bug。為了解決這一問題,NrOS的核心部分使用Rust編寫,不同於C++,Rust的特點是安全性,即從語言設計層面避免了記憶體洩漏或溢位等問題的出現,這樣以編譯器為系統安全性做出保證,這使得NrOS將動態記憶體分配放入核心空間得以實現。

同時,動態記憶體分配仍需考慮一個問題——當記憶體不夠時,對核心的狀態複製導致不同節點不一致的問題。舉個例子,不同核心進行同步的時候,由於每個核心對log進行同步不是同時進行的,所以無法確定每個核心進行同步的時候有足夠的記憶體空間,這就會造成我們無法確定一個同步過程在每個節點中都同步成功。為了解決這一問題,我們在核心中設計了一個決定性記憶體分配器,在同步時,這個分配器會暫存每一個節點的記憶體分配結果,如果遇見分配出現錯誤的節點則向之前複製成功的節點返回錯誤。

虛擬記憶體

NrOS也使用虛擬記憶體來實現地址的獨立性與一致性,其實體地址的對映表為一個B樹,並且對於每一個節點,都擁有一個物理頁表(上面已經提過),以及一個地址對映表。在虛擬記憶體系統中,NrOS提供了四個操作:MapFrame(插入表項),Unmap(刪除表項),Adjust(調整表項許可權)和Resolve(推進副本查詢地址空間狀態)。

考慮一個衝突問題:如果一個程序在複製體A的核心X上映射了一個頁面,而複製體B的核心Y在複製體B應用更新之前就在使用者空間訪問了該對映,這個時候就會產生衝突(主要原因在於硬體頁表不像日誌那樣只涉及核心空間)。為了處理這一問題,便有了Resolve,一旦出現了頁面對映問題,便推動Resolve過程尋找衝突的對映,若找到衝突的對映則恢復程序此前的操作,若沒有找到,便認為此對映為此前程序無效的讀寫所遺留的對映。

對於Unmap和Adjust操作來說,有一個需要注意的地方,即在處理完對映表上的操作之後,不同節點之間對映表通過log來同步這一過程本身沒有問題,不過卻難以處理TLB中記憶體對映的同步。為了處理這一問題,文章通過傳送處理器內部通斷來同步所有和修改塊相關的程序,使之與日誌同步。例如,當一個程序對對映表已有的條目進行修改後,他會發送處理器內部中斷(IPI)使所有節點對日誌進行同步,接著對每個核進行遍歷獲得其需要重新整理TLB的區域,最後再使對應的TLB條目無效。

檔案系統

有關檔案系統的實現主要面臨三個問題,接下來一一說明。

首先,檔案描述符問題,通常情況下,使用者空間使用檔案描述符對檔案進行讀取的過程會對核心產生修改(例如檔案描述符的偏移),這回大幅度降低整個系統的並行性。為了解決這一問題,NrOS將檔案描述符放在了使用者空間,並且在核心狀態下,只進行鍼對地址的讀寫,這樣保證了核心不必記錄每個檔案描述符的偏移位置。

其次,大型檔案的讀寫問題,我們的日誌是一個迴圈的資料結構,當系統進行改變大型檔案的時候,如果將所改變的內容全部放在日誌條目裡,可能會產生日誌的記憶體不夠用的情況。為了解決這一問題,NrOS為大的檔案在記憶體中分配緩衝區,並且將其索引放入日誌中方便其他節點進行同步。

最後,仍需要考慮一個問題,即在使用者區的讀寫緩衝區在未被其他節點複製之前就被改變造成節點之間讀寫不一致問題。為了解決這一問題,NrOS將所有的讀寫緩衝區事先複製進入核心記憶體中,這樣便可以在同步的過程中進行固定地址的讀寫。

檔案系統讀寫一致性問題

NrOS使用CNR來保證每個節點的讀寫一致性,CNR中有多個日誌,根據衝突性對操作進行分組,把每組對映到不同的日誌中(具體前面已經說過了)。之後針對檔案改名等需要對整個檔案系統進行掃描的,對整個系統進行操作(見第二節)。

程序分配管理

NrOS在核心分配層面採用粗粒度的方式為程序分配資源——NR排程器把cpu分配給程序的方式,程序通過系統呼叫來向核心申請cpu或釋放cpu。而一個cpu被分配給一個程序之後,就會生成一個執行器物件(相當於通常意義上的核心執行緒),來負責程序的系統呼叫,以及分配使用者空間和儲存暫存器狀態,程序會將執行器儲存起來以便重複使用。

NR排程器中維持一個hash表用來把核心程序對映在對應的分配器和對應的cpu,在這個表中用來生成、刪除程序以及分配排程器。

還需要注意一個問題即對程序記憶體分配的問題,當處理一個需要寫操作分配記憶體的程序時會造成衝突,同時也需要每個程序統一的虛擬地址。為了解決這一問題,在記憶體建立前,讀取程式,預先找到其需要寫的位置,為其分配好記憶體,隨後再建立程序,當需要分配需要寫的記憶體的時候,直接對映到預先分配好的記憶體,從而解決衝突問題。

日誌同步

這個其實是從一開始就困擾的問題然後到最後文章才說明白,,,

NrOS日誌是一個迴圈的資料結構造成的一個可能出現的問題是,當一個節點因為某些原因一直沒有和日誌同步,以至於別的節點無法更新日誌。為了解決這個問題,文章提出了兩個解決辦法:首先是無法進行更新的節點對沒有更新的節點發送IPI,使對應節點對日誌進行同步,從而使自己可以更新日誌。然而這種使用IPI的方法代價十分昂貴,於是大多數情況下采用另一種辦法:即當節點處於空閒狀態時,主動對日誌進行同步,從而避免頻繁的IPI。

迴圈的資料結構還會造成一個問題,即日誌中的表項只有在被覆蓋時才會銷燬掉,而一些表現可能有外部的資料引用,倘若在表項被覆蓋時才釋放這些引用就會造成大量的記憶體浪費。解決辦法則是對每個引用根據未被同步的節點數量設定一個計數器,當計數器清零的時候則釋放對應的資源。