柴艷娜
(長安大學 信息與網絡管理處,陜西 西安 710064)
計算機是現(xiàn)代日常生活的一種必需品,其高效可靠的運行需要依賴于一套穩(wěn)健無缺陷(bug-free)的操作系統(tǒng)。現(xiàn)代操作系統(tǒng)都會使用內核(kernel)來對硬件進行管理,因此可以說內核的安全穩(wěn)定決定了人們與計算機相處的體驗。內核中的缺陷(bug)將可能使用戶的應用程序甚至操作系統(tǒng)本身變得不可靠[1]。
內核是用戶和應用程序與計算機硬件之間的橋梁,內核管理各種系統(tǒng)資源,包括內存和硬盤空間,并且處理CPU處理程序的調度。它也提供對輸入輸出設備和網絡的訪問。應用程序運行在內核之上,通過內核的系統(tǒng)調用從而使用到內核的功能。
大多數成熟的操作系統(tǒng)內核都是用C語言實現(xiàn)的。C語言因為允許高度控制內存使用以及其他如可與匯編語言互操作等低級程序操作特性,成為最受歡迎的內核語言[2]。這種高度的自由也會付出一些代價,比如內存釋放兩遍的錯誤、數組越界的錯誤以及死鎖[3]。同時它也不能防止數據類型的錯誤解析,保證不了類型的安全性。
隨著計算機多處理器以及多核處理器的增加,如何高效地利用多線程是評價內核優(yōu)秀與否的一個有力因素。C語言實現(xiàn)的內核不能輕易地全面發(fā)揮多核的性能,因為C語言本身沒有涵蓋現(xiàn)代處理器的特性,C中的線程(thread)對內存和CPU來說都是很昂貴的一筆開銷,而線程之間的同步機制則更復雜,所以內核需要大費周章地實現(xiàn)一套機制來充分調動多核計算機的全部性能[4]。
如果用Java,Go等高級語言來開發(fā)內核,則可能會規(guī)避掉很多上述問題,比如許多高級語言提供了數組越界檢查和內存垃圾回收機制。然而,通常來說高級語言開發(fā)的程序會比C語言的慢,有時候由于代碼解釋、自動內存管理、垃圾回收等特性,會帶來很大的系統(tǒng)開銷。同時,高級語言很難操作匯編語言,因此可能很難滿足內核的底層任務調用。
當下社區(qū)中有很多用高級語言實現(xiàn)內核的嘗試,諸多原因導致了它們沒有一個被廣泛采用。
Mirage是一個Linux基金會項目,致力于將Web應用變成一個運行在Xen虛擬機下的獨立的專屬精簡內核(Unikernel),它包含一個用OCaml開發(fā)的內核子系統(tǒng)的早期實現(xiàn)。因為它是為專屬內核(單用戶單進程,大多運行于虛擬機中)開發(fā)的,所以不能滿足大多數普通用戶的需求。另外,也不能在多核上并行,因為它本來就是為單進程運行而設計的。
Pycorn是一個用Python開發(fā)的操作系統(tǒng),目前只兼容16位ARM微處理器。因為Python是一門解釋型語言,Pycorn實際運行十分慢,性能不是該項目的目標。因此它從未被廣泛使用過。
因為實現(xiàn)一個完整的內核是一項巨大的工程,所以該文代之以實現(xiàn)一個內核子系統(tǒng),即網絡堆棧子系統(tǒng),來進行相應的研究工作。網絡堆棧(network stack)是任何內核必須有的特性,網絡堆棧的功能和性能可以容易地比較和測試,因此是個比較理想的可用于研究的子系統(tǒng)。
該文用Go語言實現(xiàn)內核子系統(tǒng),用于研究用高級語言開發(fā)內核的相對優(yōu)勢。之所以選擇Go是因為語言本身自帶的優(yōu)秀的CSP并發(fā)模型(concurrent sequential processes)[4-6]。CSP模型將復雜任務解構成更小的、更加可管理的子任務。這些子任務都能被單個進程所處理,進程之間彼此保持通信,共同完成原始的復雜任務。
CSP模型的目標是幫助程序員設計,實現(xiàn)和驗證復雜的計算機系統(tǒng),這是十分重要的,特別是要設計一個如內核般復雜的軟件。Go提供了線程安全(thread-safe)方式的CSP模型,Go語言的線程即協(xié)程(go-routines),同步的通信構造即通道(channel)[7]。Go語言運行時自動根據計算機的物理內核數量來管理調度協(xié)程。CSP模型能讓人很容易地使用計算機所有內核,同時改善代碼的可讀性,使得更簡單地進行調試和減少產生的缺陷。網絡堆棧很自然地可以被劃分成多個子任務去運行,可以充分利用Go協(xié)程去動態(tài)調度高效利用所有可用物理內核[8]。
CSP模型只在垃圾回收語言里有可行性,Go提供了必要的垃圾回收。Go是一門強類型語言,能減少一大類錯誤,包括錯誤類型轉換,內存釋放兩遍,對象釋放后再使用等。Go的延遲聲明(defer statement)允許在函數結束時更方便地清理,減少那些疏于管理的資源導致死鎖的可能性。
Go和CSP模型的優(yōu)勢可能伴隨著某種代價,比如垃圾回收有性能花銷并導致運行時的短暫暫停。另外,多核的使用,將帶來昂貴的內核間通信。該文的目標是評估Go帶來的收益是否能蓋過性能損失帶來的劣勢。
該文實現(xiàn)的獨立網絡堆棧(項目代號NStack)是建立在Tap虛擬網卡的基礎上。為了功能完整,所有基礎網絡協(xié)議,包括以太網(Ethernet),ARP,IPv4,ICMP,UDP和TCP,都被實現(xiàn)。為確保性能不受影響,延遲(latency)和吞吐量(through-out)會被測試,并與C語言實現(xiàn)的網絡堆棧進行比較。
Tap接口即一種虛擬網絡接口(虛擬網卡),用軟件來模仿實際硬件。NStack會將Tap接口當作正常物理接口一樣讀寫[9]。Tap接口會關聯(lián)一橋接接口,就好像一個路由器作為主機的一個子網接入其中,這樣可以允許NStack能使用它自己的MAC地址和IP地址,連接到外部網絡。
NStack會實現(xiàn)數據鏈路層,網絡層和傳輸層的協(xié)議,每一層獨立運行自己的協(xié)議,如圖1所示。分層模型可以增加并行,在高負載下提供高效服務[10]。
圖1 網絡協(xié)議棧
每一個協(xié)議的實現(xiàn)使用了類似的結構,包處理器(packet dealer)。IP包處理器如圖2所示。包處理器從低層級讀取數據包,并通過通道傳輸。通道以箭頭表示在圖2中。IP包處理器將數據包發(fā)給不同的IP reader協(xié)程。IP reader處理完接收到的數據包后,將處理結果轉發(fā)給下一層的包處理器。
圖2 IPv4包處理器
(1)以太協(xié)議層允許其他不同層的協(xié)議綁定到特定的以太協(xié)議。比如IPv4實現(xiàn)會綁定到以太協(xié)議2048去接收所有IPv4數據包,ARP實現(xiàn)則綁定到以太協(xié)議2054。
(2)地址解析協(xié)議(address resolution protocol,ARP)會被實現(xiàn)用于MAC地址的獲取,數據的網絡傳輸需要物理信息的支持。ARP能讓NStack從目標主機的目標協(xié)議地址中獲取MAC地址。NStack為每個ARP請求創(chuàng)建一個協(xié)程負責處理。處理時協(xié)程會被阻塞直到主ARP包處理器通知其響應或者請求超時。
(3)IPv4的設計如圖2所示,它使用包處理器結構,包含多個IP讀取器和分片重組器。所有組件之間的通信都是通過通道進行,如箭頭所示。
當IP包大小超過最大傳輸單元(maximum transmission unit,MTU)時,便會出現(xiàn)IP分片,IP包會被拆分成多個分片,每一個分片都包含一些信息用以重組。當分片數據包到達目的主機,它們便會被重組成原始IP包。
NStack的分片重組器演示了CSP模型的優(yōu)點。每個分片重組器都囊括了對分片IP數據包的處理過程以及相應的數據。與用全局數據結構來管理所有分片數據包重組的傳統(tǒng)方法相比,為每個報文分片分配一個專屬重組器,這種CSP模型的做法可以大幅降低代碼的復雜度。輕量級的Go協(xié)程設計讓數據隔離變得可行,垃圾回收又大幅降低內存泄漏的可能。
(4)NStack實現(xiàn)了ping及ICMP協(xié)議。ICMP實現(xiàn)也是遵循包處理器結構。ping實現(xiàn)也有其相應的包處理器,ping的ICMP包會被ICMP包處理器先行處理,然后再發(fā)給ping的包處理器處理。ping包處理器會將ping請求轉發(fā)給一組特別的協(xié)程,用于回復ping請求。如果NStack已經發(fā)送了ping請求,則ping包處理器將會把回應轉發(fā)給對應請求的專屬協(xié)程負責。
(5)用戶報文協(xié)議(user datagram protocol,UDP)是個無連接的協(xié)議,因為它相對簡單,NStack便用一個基礎的包處理器將其轉發(fā)給對應的UDP讀取器。
(6)傳輸控制協(xié)議(transmission control protocol,TCP)是面向連接的傳輸層協(xié)議,它保證了數據傳輸的有序。因為TCP是面向連接的,所以它會需要服務端和客戶端來初始化連接。一旦連接建立成功,便由傳輸控制單元(transmission control block,TCB)進行管理。
NStack里TCP也是使用標準的包處理器結構管理源端口和目的端口,每個TCB里都有2個長期運行的協(xié)程。一個處理接收到的數據包,另一個則等待和發(fā)送數據,也會負責創(chuàng)建額外的協(xié)程管理數據包的重發(fā),這2個協(xié)程便代表著半雙工TCP連接。TCB內部也會用到通道來同步和管理所有創(chuàng)建的協(xié)程。比如,處理接收數據包的協(xié)程發(fā)現(xiàn)收到一個確認數據包時,便會用通道通知數據包重傳協(xié)程。
NStack會與Tapip進行性能比較。Tapip是一個由C語言開發(fā)的多線程網絡堆棧。這個比較允許評估用高級語言開發(fā)網絡堆棧的優(yōu)點和缺點。兩個網絡堆棧都實現(xiàn)了相似的協(xié)議,都在用戶空間(user space)操作,都使用tap虛擬接口。測試機器是Ubuntu 14.04/Linux 3.13.0,16 GB內存,Intel Xeon Quad Core Dual Socket處理器。
2.3.1 延 遲
為測試延遲,將取50次ping響應時間的平均值作比較。測試環(huán)境的一臺Linux虛擬機將運行兩個網絡堆棧,ping請求從該虛擬機發(fā)出。為判斷堆棧在負載增加情況下的性能,多個ping會被同時并發(fā)發(fā)送。從1個增加到1 000個并發(fā)ping“連接”來模擬網絡堆??赡芙邮艿呢撦d。為保證對兩個網絡堆棧公平,其他的變量都將保持不變,包括每個ping“連接”發(fā)送的ping請求數,ICMP接受緩沖區(qū)大小以及ping請求數據包大小。
2.3.2 吞吐量
第二個將要評估的性能指標便是吞吐量。一個堆棧的吞吐量是在給定時間內,能發(fā)送或接收的數據量大小[11]。以下步驟將用以測量兩個堆棧的吞吐量:
(1)初始化一個TCP服務端。
(2)初始化一個TCP客戶端,連接會在local網絡(localhost)中建立,以排除tap虛擬網卡導致的開銷。
(3)客戶端發(fā)送4 KB數據給服務端。
(4)計算堆棧完成上述過程的總時間,該時間和發(fā)送的數據量將用來計算吞吐量。
為測量堆棧的相對擴展能力,將會逐步增加客戶端數來測量性能[12]。最大測試到100個并發(fā)客戶端。有許多預防措施將用于保證吞吐量的準確測量,比如所有可比較的緩沖區(qū)大小都一致[13]。在Tapip中,每個客戶端和服務端連接都運行在各自線程里,NStack類似,但是用的是Go的協(xié)程而不是線程。另外,也會確保所有連接完成且連接的負載被完整傳輸之后再停止運行網絡堆棧[14-15]。
NStack的代碼與Tapip比較類似,但是從結果來看,性能上,包括延遲和吞吐量,NStack相比之下出色得多。
NStack和Tapip都能準確地運行協(xié)議,這可以通過分別測試兩個協(xié)議棧與一臺Linux終端的連接來進行判斷。測試中發(fā)現(xiàn)Tapip有內存泄漏的情況。這是因為Tapip會開辟緩存區(qū)存儲數據包,在某些情況下這些緩存區(qū)不會被釋放或者重復釋放。當緩存區(qū)被重復釋放時,Tapip會崩潰或者導致異常行為。當緩存區(qū)不會被釋放時,Tapip會不斷侵占內存,直至系統(tǒng)崩潰。Go則由于有內置的垃圾回收,可以很好地避免這種情況的發(fā)生。
雖然很難量化地評估編寫Go語言相比較C語言的優(yōu)點,但是從一些代碼片段的比較還是可以看出高級語言的某些優(yōu)勢。以下以IP報文分片重組的處理代碼舉例說明。
(1)當新的IP分片到達時,需要初始化分片重組器。Tapip則會使用全局結構體存儲所有待重組的數據,C代碼如下所示:
struct fragment *frag;
frag=xmalloc(sizeof(*frag));
list_add(& frag->frag_list, & frag_head);
list_init(& frag->frag_pkb);
return frag;
NStack會給每個待重組的包新建一個Go協(xié)程,Go語言代碼如下:
ipr.fragBuf[bufID]=make(chan []byte, FRAG_ASSEM_BUF_SZ)
quit:=make(chan bool, 1)
done:=make(chan bool, 1)
didQuit:=make(chan bool, 1)
go ipr.fragAssembler( /* ... */ )
go ipr.killFragAssembler( /* ... */ )
(2)當添加分片到重組隊列時,Tapip的C語言代碼如下:
int insert_frag(/* ... */) {
/*一些額外的分片處理 */
list_add(& pkb->pk_list, pos);
return 0;
frag_drop: free_pkb(pkb); return -1;
}
Go語言代碼則如下:
ipr.fragBuf[bufID] <- b
Go可以用協(xié)程處理IP報文分片,因此它可以簡單地將分片轉發(fā)給對應的協(xié)程處理,同時可以緊接著處理后續(xù)數據包。此舉會改進NStack代碼的模塊性、可讀性和并發(fā)性。
(3)分片處理完成時的C語言代碼片段如下:
if (complete_frag(frag))
pkb=reass_frag(frag);
else pkb=NULL;
return pkb;
struct pkbuf *reass_frag(
struct fragment *frag) {
/* more processing */
delete_frag(frag);
return pkb;
}
Go語言代碼片段如下:
ipr.incomingPackets <- append(
fullPacketHdr ,payload ...)
done <- true
經過對比,可以凸顯出Go語言以及CSP模型的優(yōu)勢。Tapip必需按順序處理數據包,在前一個數據包未處理完時,下一個數據包只能在緩沖區(qū)中等待。這會帶來一些問題,比如這便需要C語言的IP實現(xiàn)去跟蹤所有正在進行的分片重組的狀態(tài),這樣不可避免地會使用全局變量和結構體來記錄共享信息,并且會讓線程同步變得困難。NStack與之相反,它會對接收到的每個分片IP包創(chuàng)建一個獨立的分片重組器協(xié)程,每個協(xié)程各自負責獨立的分片組裝成IP片段。分片重組器處理重組完數據包后,它便簡單地將重組片段發(fā)回后續(xù)的IP數據包處理過程。IP數據包這個主處理過程與分片重組器是獨立的協(xié)程,因此可以實現(xiàn)完全的并行和并發(fā),代碼也更簡潔可讀。
(4)在清理分片時,C語言的Tapip需要顯性地釋放每一個內存緩存區(qū),代碼如下:
struct pkbuf *pkb;
list_del(& frag->frag_list);
while (!list_empty(& frag->frag_pkb)) {
pkb=frag_head_pkb(frag);
list_del(& pkb->pk_list);
free_pkb(pkb);
}
free(frag);
而Go語言只需跟蹤通道即可:
delete(ipr.fragBuf, bufID)
Go語言的簡潔友好可讀由此可見一斑。
1個ping請求時,Tapip的0.074 ms優(yōu)于NStack的0.234 ms,但是隨著并發(fā)請求的增加,當1 000個ping請求時,NStack的延遲為0.717 ms,差不多比Tapip的3.279 ms好5倍。NStack在連接數為600時,開始領先于Tapip。NStack延遲的增加是線性的,而Tapip是指數型的。NStack的延遲趨勢是優(yōu)于Tapip的,因為在請求數很少時,兩者之間延遲的差距很小,可以忽略不計,但是在大量并發(fā)ping時,差異就明顯變大,如圖3所示。
圖3 并發(fā)延遲性測試結果
基于圖3的結果,可以得出Tapip能非??斓靥幚硇×考壍臄祿?,而對于大量的數據包涌入時,則顯得處理乏力,性能極差。相對應的,NStack會用相對較長的時間來處理每個數據包,但是因為其在每個協(xié)議實現(xiàn)中良好的并發(fā)控制,在負載大量增加的情況下,幾乎不影響其處理性能。表現(xiàn)出來便是結果中,Tapip雖然開始性能優(yōu)秀,但延遲卻隨著并發(fā)量的增長,迅速增大上升,而NStack則小幅平緩的增加。Tapip陡峭的增長趨勢凸顯了其底層架構的問題,即在所有的協(xié)議層處理完一個數據包后,再處理下一個數據包,這種做法不是一個高效的方法,因為這會導致擴展或并發(fā)難以實現(xiàn)。
測試結果如圖4所示,1個并發(fā)連接時,NStack的吞吐量達到7.3 Mbit/s,而Tapip的只有4.6 Mbit/s。當100個并發(fā)連接時,NStack達到了284.9 Mbit/s,而Tapip則只有195 Mbit/s。并且,NStack的吞吐量增加速度比Tapip快得多。這表明NStack可以繼續(xù)在更大量的并發(fā)情況下擴展吞吐量而Tapip則很可能處理不了這種負載。
圖4 并發(fā)吞吐量測試結果
結果有力地驗證了NStack的架構。在Tapip里,所有的傳輸控制塊(transmission control block,TCB)都是由單個線程管理的;相應的,在NStack中,每個TCB由兩個線程進行管理,分別負責一半的上下行連接,因而NStack可以更高效地在有限的CPU核數上多路復用大量的連接,可以達到更大的吞吐量。在小量并發(fā)連接時,NStack也工作地更高效,因為它把TCB的處理工作拆分為兩個Go協(xié)程,而Tapip則自始至終都是一個線程執(zhí)行處理任務。
操作系統(tǒng)內核對于管理計算機系統(tǒng)資源而言是十分重要的核心組件,如何在兼顧性能的前提下,引入高級語言進行開發(fā),降低低級語言開發(fā)內核帶來的復雜性和安全隱患是該文的初衷。該文以內核的網絡堆棧子系統(tǒng)為出發(fā)點,用Go語言實現(xiàn)NStack,研究高級語言開發(fā)內核的可行性和便利性。NStack和對比實驗的C語言開發(fā)的Tapip都是基于tap接口,都實現(xiàn)了相類似的協(xié)議,比如IPv4,ARP,UDP和TCP。在延遲性和吞吐量的對比實驗中,可以發(fā)現(xiàn)NStack有優(yōu)秀的性能表現(xiàn),在延遲性測試中,當并發(fā)數大于600時,NStack取得更低的延時;在吞吐量的測試中,NStack的并行化讓其在所有的測試場景中都取得了優(yōu)于Tapip的吞吐量。
實驗表明,Go語言帶來的簡潔和模塊化可以提供優(yōu)于C語言的幫助,用Go開發(fā)內核子系統(tǒng)可以改善代碼的可讀性和可靠性,結構模塊清晰,良好的并發(fā)能力和穩(wěn)定性,同時又對內核整體性能沒有產生重大不良影響。結果表明,對于內核開發(fā)來說,Go語言可以是一個重要的C語言替代者。