柴艷娜
(長安大學(xué) 信息與網(wǎng)絡(luò)管理處,陜西 西安 710064)
計算機是現(xiàn)代日常生活的一種必需品,其高效可靠的運行需要依賴于一套穩(wěn)健無缺陷(bug-free)的操作系統(tǒng)。現(xiàn)代操作系統(tǒng)都會使用內(nèi)核(kernel)來對硬件進行管理,因此可以說內(nèi)核的安全穩(wěn)定決定了人們與計算機相處的體驗。內(nèi)核中的缺陷(bug)將可能使用戶的應(yīng)用程序甚至操作系統(tǒng)本身變得不可靠[1]。
內(nèi)核是用戶和應(yīng)用程序與計算機硬件之間的橋梁,內(nèi)核管理各種系統(tǒng)資源,包括內(nèi)存和硬盤空間,并且處理CPU處理程序的調(diào)度。它也提供對輸入輸出設(shè)備和網(wǎng)絡(luò)的訪問。應(yīng)用程序運行在內(nèi)核之上,通過內(nèi)核的系統(tǒng)調(diào)用從而使用到內(nèi)核的功能。
大多數(shù)成熟的操作系統(tǒng)內(nèi)核都是用C語言實現(xiàn)的。C語言因為允許高度控制內(nèi)存使用以及其他如可與匯編語言互操作等低級程序操作特性,成為最受歡迎的內(nèi)核語言[2]。這種高度的自由也會付出一些代價,比如內(nèi)存釋放兩遍的錯誤、數(shù)組越界的錯誤以及死鎖[3]。同時它也不能防止數(shù)據(jù)類型的錯誤解析,保證不了類型的安全性。
隨著計算機多處理器以及多核處理器的增加,如何高效地利用多線程是評價內(nèi)核優(yōu)秀與否的一個有力因素。C語言實現(xiàn)的內(nèi)核不能輕易地全面發(fā)揮多核的性能,因為C語言本身沒有涵蓋現(xiàn)代處理器的特性,C中的線程(thread)對內(nèi)存和CPU來說都是很昂貴的一筆開銷,而線程之間的同步機制則更復(fù)雜,所以內(nèi)核需要大費周章地實現(xiàn)一套機制來充分調(diào)動多核計算機的全部性能[4]。
如果用Java,Go等高級語言來開發(fā)內(nèi)核,則可能會規(guī)避掉很多上述問題,比如許多高級語言提供了數(shù)組越界檢查和內(nèi)存垃圾回收機制。然而,通常來說高級語言開發(fā)的程序會比C語言的慢,有時候由于代碼解釋、自動內(nèi)存管理、垃圾回收等特性,會帶來很大的系統(tǒng)開銷。同時,高級語言很難操作匯編語言,因此可能很難滿足內(nèi)核的底層任務(wù)調(diào)用。
當(dāng)下社區(qū)中有很多用高級語言實現(xiàn)內(nèi)核的嘗試,諸多原因?qū)е铝怂鼈儧]有一個被廣泛采用。
Mirage是一個Linux基金會項目,致力于將Web應(yīng)用變成一個運行在Xen虛擬機下的獨立的專屬精簡內(nèi)核(Unikernel),它包含一個用OCaml開發(fā)的內(nèi)核子系統(tǒng)的早期實現(xiàn)。因為它是為專屬內(nèi)核(單用戶單進程,大多運行于虛擬機中)開發(fā)的,所以不能滿足大多數(shù)普通用戶的需求。另外,也不能在多核上并行,因為它本來就是為單進程運行而設(shè)計的。
Pycorn是一個用Python開發(fā)的操作系統(tǒng),目前只兼容16位ARM微處理器。因為Python是一門解釋型語言,Pycorn實際運行十分慢,性能不是該項目的目標(biāo)。因此它從未被廣泛使用過。
因為實現(xiàn)一個完整的內(nèi)核是一項巨大的工程,所以該文代之以實現(xiàn)一個內(nèi)核子系統(tǒng),即網(wǎng)絡(luò)堆棧子系統(tǒng),來進行相應(yīng)的研究工作。網(wǎng)絡(luò)堆棧(network stack)是任何內(nèi)核必須有的特性,網(wǎng)絡(luò)堆棧的功能和性能可以容易地比較和測試,因此是個比較理想的可用于研究的子系統(tǒng)。
該文用Go語言實現(xiàn)內(nèi)核子系統(tǒng),用于研究用高級語言開發(fā)內(nèi)核的相對優(yōu)勢。之所以選擇Go是因為語言本身自帶的優(yōu)秀的CSP并發(fā)模型(concurrent sequential processes)[4-6]。CSP模型將復(fù)雜任務(wù)解構(gòu)成更小的、更加可管理的子任務(wù)。這些子任務(wù)都能被單個進程所處理,進程之間彼此保持通信,共同完成原始的復(fù)雜任務(wù)。
CSP模型的目標(biāo)是幫助程序員設(shè)計,實現(xiàn)和驗證復(fù)雜的計算機系統(tǒng),這是十分重要的,特別是要設(shè)計一個如內(nèi)核般復(fù)雜的軟件。Go提供了線程安全(thread-safe)方式的CSP模型,Go語言的線程即協(xié)程(go-routines),同步的通信構(gòu)造即通道(channel)[7]。Go語言運行時自動根據(jù)計算機的物理內(nèi)核數(shù)量來管理調(diào)度協(xié)程。CSP模型能讓人很容易地使用計算機所有內(nèi)核,同時改善代碼的可讀性,使得更簡單地進行調(diào)試和減少產(chǎn)生的缺陷。網(wǎng)絡(luò)堆棧很自然地可以被劃分成多個子任務(wù)去運行,可以充分利用Go協(xié)程去動態(tài)調(diào)度高效利用所有可用物理內(nèi)核[8]。
CSP模型只在垃圾回收語言里有可行性,Go提供了必要的垃圾回收。Go是一門強類型語言,能減少一大類錯誤,包括錯誤類型轉(zhuǎn)換,內(nèi)存釋放兩遍,對象釋放后再使用等。Go的延遲聲明(defer statement)允許在函數(shù)結(jié)束時更方便地清理,減少那些疏于管理的資源導(dǎo)致死鎖的可能性。
Go和CSP模型的優(yōu)勢可能伴隨著某種代價,比如垃圾回收有性能花銷并導(dǎo)致運行時的短暫暫停。另外,多核的使用,將帶來昂貴的內(nèi)核間通信。該文的目標(biāo)是評估Go帶來的收益是否能蓋過性能損失帶來的劣勢。
該文實現(xiàn)的獨立網(wǎng)絡(luò)堆棧(項目代號NStack)是建立在Tap虛擬網(wǎng)卡的基礎(chǔ)上。為了功能完整,所有基礎(chǔ)網(wǎng)絡(luò)協(xié)議,包括以太網(wǎng)(Ethernet),ARP,IPv4,ICMP,UDP和TCP,都被實現(xiàn)。為確保性能不受影響,延遲(latency)和吞吐量(through-out)會被測試,并與C語言實現(xiàn)的網(wǎng)絡(luò)堆棧進行比較。
Tap接口即一種虛擬網(wǎng)絡(luò)接口(虛擬網(wǎng)卡),用軟件來模仿實際硬件。NStack會將Tap接口當(dāng)作正常物理接口一樣讀寫[9]。Tap接口會關(guān)聯(lián)一橋接接口,就好像一個路由器作為主機的一個子網(wǎng)接入其中,這樣可以允許NStack能使用它自己的MAC地址和IP地址,連接到外部網(wǎng)絡(luò)。
NStack會實現(xiàn)數(shù)據(jù)鏈路層,網(wǎng)絡(luò)層和傳輸層的協(xié)議,每一層獨立運行自己的協(xié)議,如圖1所示。分層模型可以增加并行,在高負(fù)載下提供高效服務(wù)[10]。

圖1 網(wǎng)絡(luò)協(xié)議棧
每一個協(xié)議的實現(xiàn)使用了類似的結(jié)構(gòu),包處理器(packet dealer)。IP包處理器如圖2所示。包處理器從低層級讀取數(shù)據(jù)包,并通過通道傳輸。通道以箭頭表示在圖2中。IP包處理器將數(shù)據(jù)包發(fā)給不同的IP reader協(xié)程。IP reader處理完接收到的數(shù)據(jù)包后,將處理結(jié)果轉(zhuǎn)發(fā)給下一層的包處理器。

圖2 IPv4包處理器
(1)以太協(xié)議層允許其他不同層的協(xié)議綁定到特定的以太協(xié)議。比如IPv4實現(xiàn)會綁定到以太協(xié)議2048去接收所有IPv4數(shù)據(jù)包,ARP實現(xiàn)則綁定到以太協(xié)議2054。
(2)地址解析協(xié)議(address resolution protocol,ARP)會被實現(xiàn)用于MAC地址的獲取,數(shù)據(jù)的網(wǎng)絡(luò)傳輸需要物理信息的支持。ARP能讓NStack從目標(biāo)主機的目標(biāo)協(xié)議地址中獲取MAC地址。NStack為每個ARP請求創(chuàng)建一個協(xié)程負(fù)責(zé)處理。處理時協(xié)程會被阻塞直到主ARP包處理器通知其響應(yīng)或者請求超時。
(3)IPv4的設(shè)計如圖2所示,它使用包處理器結(jié)構(gòu),包含多個IP讀取器和分片重組器。所有組件之間的通信都是通過通道進行,如箭頭所示。
當(dāng)IP包大小超過最大傳輸單元(maximum transmission unit,MTU)時,便會出現(xiàn)IP分片,IP包會被拆分成多個分片,每一個分片都包含一些信息用以重組。當(dāng)分片數(shù)據(jù)包到達(dá)目的主機,它們便會被重組成原始IP包。
NStack的分片重組器演示了CSP模型的優(yōu)點。每個分片重組器都囊括了對分片IP數(shù)據(jù)包的處理過程以及相應(yīng)的數(shù)據(jù)。與用全局?jǐn)?shù)據(jù)結(jié)構(gòu)來管理所有分片數(shù)據(jù)包重組的傳統(tǒng)方法相比,為每個報文分片分配一個專屬重組器,這種CSP模型的做法可以大幅降低代碼的復(fù)雜度。輕量級的Go協(xié)程設(shè)計讓數(shù)據(jù)隔離變得可行,垃圾回收又大幅降低內(nèi)存泄漏的可能。
(4)NStack實現(xiàn)了ping及ICMP協(xié)議。ICMP實現(xiàn)也是遵循包處理器結(jié)構(gòu)。ping實現(xiàn)也有其相應(yīng)的包處理器,ping的ICMP包會被ICMP包處理器先行處理,然后再發(fā)給ping的包處理器處理。ping包處理器會將ping請求轉(zhuǎn)發(fā)給一組特別的協(xié)程,用于回復(fù)ping請求。如果NStack已經(jīng)發(fā)送了ping請求,則ping包處理器將會把回應(yīng)轉(zhuǎn)發(fā)給對應(yīng)請求的專屬協(xié)程負(fù)責(zé)。
(5)用戶報文協(xié)議(user datagram protocol,UDP)是個無連接的協(xié)議,因為它相對簡單,NStack便用一個基礎(chǔ)的包處理器將其轉(zhuǎn)發(fā)給對應(yīng)的UDP讀取器。
(6)傳輸控制協(xié)議(transmission control protocol,TCP)是面向連接的傳輸層協(xié)議,它保證了數(shù)據(jù)傳輸?shù)挠行颉R驗門CP是面向連接的,所以它會需要服務(wù)端和客戶端來初始化連接。一旦連接建立成功,便由傳輸控制單元(transmission control block,TCB)進行管理。
NStack里TCP也是使用標(biāo)準(zhǔn)的包處理器結(jié)構(gòu)管理源端口和目的端口,每個TCB里都有2個長期運行的協(xié)程。一個處理接收到的數(shù)據(jù)包,另一個則等待和發(fā)送數(shù)據(jù),也會負(fù)責(zé)創(chuàng)建額外的協(xié)程管理數(shù)據(jù)包的重發(fā),這2個協(xié)程便代表著半雙工TCP連接。TCB內(nèi)部也會用到通道來同步和管理所有創(chuàng)建的協(xié)程。比如,處理接收數(shù)據(jù)包的協(xié)程發(fā)現(xiàn)收到一個確認(rèn)數(shù)據(jù)包時,便會用通道通知數(shù)據(jù)包重傳協(xié)程。
NStack會與Tapip進行性能比較。Tapip是一個由C語言開發(fā)的多線程網(wǎng)絡(luò)堆棧。這個比較允許評估用高級語言開發(fā)網(wǎng)絡(luò)堆棧的優(yōu)點和缺點。兩個網(wǎng)絡(luò)堆棧都實現(xiàn)了相似的協(xié)議,都在用戶空間(user space)操作,都使用tap虛擬接口。測試機器是Ubuntu 14.04/Linux 3.13.0,16 GB內(nèi)存,Intel Xeon Quad Core Dual Socket處理器。
2.3.1 延 遲
為測試延遲,將取50次ping響應(yīng)時間的平均值作比較。測試環(huán)境的一臺Linux虛擬機將運行兩個網(wǎng)絡(luò)堆棧,ping請求從該虛擬機發(fā)出。為判斷堆棧在負(fù)載增加情況下的性能,多個ping會被同時并發(fā)發(fā)送。從1個增加到1 000個并發(fā)ping“連接”來模擬網(wǎng)絡(luò)堆棧可能接受的負(fù)載。為保證對兩個網(wǎng)絡(luò)堆棧公平,其他的變量都將保持不變,包括每個ping“連接”發(fā)送的ping請求數(shù),ICMP接受緩沖區(qū)大小以及ping請求數(shù)據(jù)包大小。
2.3.2 吞吐量
第二個將要評估的性能指標(biāo)便是吞吐量。一個堆棧的吞吐量是在給定時間內(nèi),能發(fā)送或接收的數(shù)據(jù)量大小[11]。以下步驟將用以測量兩個堆棧的吞吐量:
(1)初始化一個TCP服務(wù)端。
(2)初始化一個TCP客戶端,連接會在local網(wǎng)絡(luò)(localhost)中建立,以排除tap虛擬網(wǎng)卡導(dǎo)致的開銷。
(3)客戶端發(fā)送4 KB數(shù)據(jù)給服務(wù)端。
(4)計算堆棧完成上述過程的總時間,該時間和發(fā)送的數(shù)據(jù)量將用來計算吞吐量。
為測量堆棧的相對擴展能力,將會逐步增加客戶端數(shù)來測量性能[12]。最大測試到100個并發(fā)客戶端。有許多預(yù)防措施將用于保證吞吐量的準(zhǔn)確測量,比如所有可比較的緩沖區(qū)大小都一致[13]。在Tapip中,每個客戶端和服務(wù)端連接都運行在各自線程里,NStack類似,但是用的是Go的協(xié)程而不是線程。另外,也會確保所有連接完成且連接的負(fù)載被完整傳輸之后再停止運行網(wǎng)絡(luò)堆棧[14-15]。
NStack的代碼與Tapip比較類似,但是從結(jié)果來看,性能上,包括延遲和吞吐量,NStack相比之下出色得多。
NStack和Tapip都能準(zhǔn)確地運行協(xié)議,這可以通過分別測試兩個協(xié)議棧與一臺Linux終端的連接來進行判斷。測試中發(fā)現(xiàn)Tapip有內(nèi)存泄漏的情況。這是因為Tapip會開辟緩存區(qū)存儲數(shù)據(jù)包,在某些情況下這些緩存區(qū)不會被釋放或者重復(fù)釋放。當(dāng)緩存區(qū)被重復(fù)釋放時,Tapip會崩潰或者導(dǎo)致異常行為。當(dāng)緩存區(qū)不會被釋放時,Tapip會不斷侵占內(nèi)存,直至系統(tǒng)崩潰。Go則由于有內(nèi)置的垃圾回收,可以很好地避免這種情況的發(fā)生。
雖然很難量化地評估編寫Go語言相比較C語言的優(yōu)點,但是從一些代碼片段的比較還是可以看出高級語言的某些優(yōu)勢。以下以IP報文分片重組的處理代碼舉例說明。
(1)當(dāng)新的IP分片到達(dá)時,需要初始化分片重組器。Tapip則會使用全局結(jié)構(gòu)體存儲所有待重組的數(shù)據(jù),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)當(dāng)添加分片到重組隊列時,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報文分片,因此它可以簡單地將分片轉(zhuǎn)發(fā)給對應(yīng)的協(xié)程處理,同時可以緊接著處理后續(xù)數(shù)據(jù)包。此舉會改進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
經(jīng)過對比,可以凸顯出Go語言以及CSP模型的優(yōu)勢。Tapip必需按順序處理數(shù)據(jù)包,在前一個數(shù)據(jù)包未處理完時,下一個數(shù)據(jù)包只能在緩沖區(qū)中等待。這會帶來一些問題,比如這便需要C語言的IP實現(xiàn)去跟蹤所有正在進行的分片重組的狀態(tài),這樣不可避免地會使用全局變量和結(jié)構(gòu)體來記錄共享信息,并且會讓線程同步變得困難。NStack與之相反,它會對接收到的每個分片IP包創(chuàng)建一個獨立的分片重組器協(xié)程,每個協(xié)程各自負(fù)責(zé)獨立的分片組裝成IP片段。分片重組器處理重組完數(shù)據(jù)包后,它便簡單地將重組片段發(fā)回后續(xù)的IP數(shù)據(jù)包處理過程。IP數(shù)據(jù)包這個主處理過程與分片重組器是獨立的協(xié)程,因此可以實現(xiàn)完全的并行和并發(fā),代碼也更簡潔可讀。
(4)在清理分片時,C語言的Tapip需要顯性地釋放每一個內(nèi)存緩存區(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ā)請求的增加,當(dāng)1 000個ping請求時,NStack的延遲為0.717 ms,差不多比Tapip的3.279 ms好5倍。NStack在連接數(shù)為600時,開始領(lǐng)先于Tapip。NStack延遲的增加是線性的,而Tapip是指數(shù)型的。NStack的延遲趨勢是優(yōu)于Tapip的,因為在請求數(shù)很少時,兩者之間延遲的差距很小,可以忽略不計,但是在大量并發(fā)ping時,差異就明顯變大,如圖3所示。

圖3 并發(fā)延遲性測試結(jié)果
基于圖3的結(jié)果,可以得出Tapip能非常快地處理小量級的數(shù)據(jù)包,而對于大量的數(shù)據(jù)包涌入時,則顯得處理乏力,性能極差。相對應(yīng)的,NStack會用相對較長的時間來處理每個數(shù)據(jù)包,但是因為其在每個協(xié)議實現(xiàn)中良好的并發(fā)控制,在負(fù)載大量增加的情況下,幾乎不影響其處理性能。表現(xiàn)出來便是結(jié)果中,Tapip雖然開始性能優(yōu)秀,但延遲卻隨著并發(fā)量的增長,迅速增大上升,而NStack則小幅平緩的增加。Tapip陡峭的增長趨勢凸顯了其底層架構(gòu)的問題,即在所有的協(xié)議層處理完一個數(shù)據(jù)包后,再處理下一個數(shù)據(jù)包,這種做法不是一個高效的方法,因為這會導(dǎo)致擴展或并發(fā)難以實現(xiàn)。
測試結(jié)果如圖4所示,1個并發(fā)連接時,NStack的吞吐量達(dá)到7.3 Mbit/s,而Tapip的只有4.6 Mbit/s。當(dāng)100個并發(fā)連接時,NStack達(dá)到了284.9 Mbit/s,而Tapip則只有195 Mbit/s。并且,NStack的吞吐量增加速度比Tapip快得多。這表明NStack可以繼續(xù)在更大量的并發(fā)情況下擴展吞吐量而Tapip則很可能處理不了這種負(fù)載。

圖4 并發(fā)吞吐量測試結(jié)果
結(jié)果有力地驗證了NStack的架構(gòu)。在Tapip里,所有的傳輸控制塊(transmission control block,TCB)都是由單個線程管理的;相應(yīng)的,在NStack中,每個TCB由兩個線程進行管理,分別負(fù)責(zé)一半的上下行連接,因而NStack可以更高效地在有限的CPU核數(shù)上多路復(fù)用大量的連接,可以達(dá)到更大的吞吐量。在小量并發(fā)連接時,NStack也工作地更高效,因為它把TCB的處理工作拆分為兩個Go協(xié)程,而Tapip則自始至終都是一個線程執(zhí)行處理任務(wù)。
操作系統(tǒng)內(nèi)核對于管理計算機系統(tǒng)資源而言是十分重要的核心組件,如何在兼顧性能的前提下,引入高級語言進行開發(fā),降低低級語言開發(fā)內(nèi)核帶來的復(fù)雜性和安全隱患是該文的初衷。該文以內(nèi)核的網(wǎng)絡(luò)堆棧子系統(tǒng)為出發(fā)點,用Go語言實現(xiàn)NStack,研究高級語言開發(fā)內(nèi)核的可行性和便利性。NStack和對比實驗的C語言開發(fā)的Tapip都是基于tap接口,都實現(xiàn)了相類似的協(xié)議,比如IPv4,ARP,UDP和TCP。在延遲性和吞吐量的對比實驗中,可以發(fā)現(xiàn)NStack有優(yōu)秀的性能表現(xiàn),在延遲性測試中,當(dāng)并發(fā)數(shù)大于600時,NStack取得更低的延時;在吞吐量的測試中,NStack的并行化讓其在所有的測試場景中都取得了優(yōu)于Tapip的吞吐量。
實驗表明,Go語言帶來的簡潔和模塊化可以提供優(yōu)于C語言的幫助,用Go開發(fā)內(nèi)核子系統(tǒng)可以改善代碼的可讀性和可靠性,結(jié)構(gòu)模塊清晰,良好的并發(fā)能力和穩(wěn)定性,同時又對內(nèi)核整體性能沒有產(chǎn)生重大不良影響。結(jié)果表明,對于內(nèi)核開發(fā)來說,Go語言可以是一個重要的C語言替代者。