論文:https://www./system/files/osdi18-cutler.pdf
ppt: https://www./sites/default/files/conference/protected-files/osdi18_slides_cutler.pdf
github: https://github.com/mit-pdos/biscuit
1. 問題背景
最初編寫OS的語言只有匯編語言,,但隨著OS的復雜性增加,,匯編語言慢慢暴露出它的不足。丹尼斯·里奇開發(fā)C語言,,并編寫Unix操作系統(tǒng),。從此C一戰(zhàn)成名,幾乎成為OS領(lǐng)域的唯一編程語言,。
隨著計算機硬件發(fā)展,,CPU性能變強,內(nèi)存容量極大提升,,人們對性能的訴求通過硬件高速發(fā)展得到彌補,,研究人員開發(fā)反過來思考是否可以使用安全的、高級編程語言開發(fā)操作系統(tǒng),。犧牲高性能,,換取系統(tǒng)安全性和更好的開發(fā)效率。
論文作者使用Go語言設計Biscuit kernel(一個POSIX Kernel)作為案例,,對比Go和C編寫Kernel的開發(fā)效率,,安全性和性能。
2. 動機:為什么使用高級語言編寫kernel
高級語言提供較好的語法抽象,、運行抽象,,kernel開發(fā)更方便。高級語言比C更接近人類思考方式,,易于理解,。提供自動內(nèi)存管理機制,讓開發(fā)人員不用考慮復雜的內(nèi)存管理,,特別在多線程場景下對象的同步釋放工作,,也能避免釋放后再使這個安全問題。語言支持多線程機制,,讓并行編程和同步更簡單,。
高級語言能減少CVE安全漏洞。C語言憲章思想是把自由交給程序員,,相信他們知道自己在做什么事情,。然而即使是非常有經(jīng)驗的老手,,都無法避免C語言典型的緩沖區(qū)溢出,釋放后使用,,任意的類型強轉(zhuǎn)等各種各樣的問題,。CVE數(shù)據(jù)庫披露Linux Kernel在2017年有40個關(guān)于代碼執(zhí)行的安全漏洞,如果用高級語言則能徹底避免,,或者部分減輕問題的影響,。
高級語言讓并發(fā)編程更容易。高級編程語言提供垃圾回收機制,,并行編程時,,無需考慮生命周期結(jié)束后多個線程之間的同步釋放。
高級語言的缺點,。垃圾回收減輕開發(fā)人員負擔,,但它不是沒有代價,垃圾回收機制引入的開銷和時延抖動,。語言的安全性,,自然在運行時增加安全檢測,CPU運行開銷增加,。高級語言提供的runtime自身已引入一些關(guān)鍵性機制,,比如內(nèi)存管理,線程調(diào)度機制,,開發(fā)人員在使用它開發(fā)kernel時,,必須與這些機制兼容,造成kernel方案設計可選余地變少,。
3. Biscuit設計與實現(xiàn)
從分析高級編程語言對Kernel的影響來說,,不需要深入分析Biscuit設計與實現(xiàn)。所以這里只是粗略介紹Biscuit設計方案,,重點體現(xiàn)為什么會有這樣的選擇,,以及Go語言對Biscuit設計的影響。
3.1 Biscuit整體結(jié)構(gòu)
Biscuit首先是個宏內(nèi)核,,提供部分POSIX接口,,它架整與傳統(tǒng)的POSIX宏內(nèi)存沒有太多區(qū)別,它的結(jié)構(gòu)圖如下:
Biscuit是POSIX內(nèi)核,,支持多進程和線程,。Kernel核心部分是Biscuit組件,由于采用Go語言編寫,,所以它之下還有Go runtime,。而Go runtime原來是運行在Linux用戶態(tài),,為了彌補底層功能的缺失,,論文作者實現(xiàn)一個比較輕量的shim層,,滿足Go runtime的功能依賴。
下面簡單介紹Biscuit進程,、線程模型,,中斷模型,文件系統(tǒng),,網(wǎng)絡協(xié)議棧,,垃圾回收器的實現(xiàn)方案。
3.2 進程和kernel Goroutine
Biscuit進程模型與其它POSIX內(nèi)核沒有太大的差異,,支持fork, execve系統(tǒng)調(diào)用,,每進程有單獨的地址空間,通過硬件頁表隔離,,進程可以有一個或多個線程,。
Biscuit用戶態(tài)線程與內(nèi)核態(tài)線程采用1:1的模型,即每個用戶態(tài)線程都對應一個內(nèi)核Goroutine(在Go運行環(huán)境和術(shù)語中,,routine也稱為線程),。用戶態(tài)線程執(zhí)行系統(tǒng)調(diào)用,或產(chǎn)生page fault后,,都在陷入內(nèi)核,,委托對應的kernel Goroutine執(zhí)行相應的系統(tǒng)調(diào)用和異常處理邏輯。
Biscuit的線程調(diào)度完全用go routine調(diào)度器掌管,,在go runtime看來,,用戶態(tài)線程只是Goroutine跑在user mode而已。Go runtime的調(diào)度機制采用預搶占調(diào)度機制,,在編譯階段預先生成搶占點,,當Kernel Goroutine運到該搶點占時,就會發(fā)生調(diào)度,。
Go提供Goroutine和調(diào)度功能,,Biscuit無法逃脫Goroutine的約束。優(yōu)點是快速開發(fā)Kernel功能,,缺點是無法做精細化的調(diào)用策略控制和理論創(chuàng)新,。
3.3 中斷
Go runtime原先的設計是運行在用戶態(tài),沒有中斷概念一說,,所以它在運行過程中不會關(guān)中斷,。所以Biscuit中斷處理函數(shù)不能太復雜,比如加鎖則會造成死鎖,。因此,,Biscuit中斷方案是中斷線程化,設備中斷觸發(fā)后只是對中斷線程做個標記,,中斷完成后才喚醒對應的中斷線程(Goroutine),,然后在進程上下文執(zhí)行中斷例程代碼,。
3.4 文件系統(tǒng)
Biscuit實現(xiàn)主要的POSIX文件系統(tǒng)訪問接口,實現(xiàn)日志型文件系統(tǒng),,提供批處理能力,,文件可靠性和性能有一定保證。實現(xiàn)ACHI磁盤驅(qū)動,,該驅(qū)動使用DMA和MSI機制,。
3.5 協(xié)議棧
實現(xiàn)TCP/IP協(xié)議棧,一款I(lǐng)ntel PCIE網(wǎng)卡驅(qū)動,,支持DMA和MSI機制,,同時提供POSIX的socket接口。
3.6 垃圾回收
Biscuit復用Go runtime提供的垃圾回收功能,,這意味著只要將一塊內(nèi)存給Go runtime管理,,Biscuit直接向Go runtime申請各類kernel對象即可,也無須釋放,,只要無指針引用,,垃圾回收器會在適當?shù)臅r機對它做回收。借用Go語言的內(nèi)存管理機制,,大大簡化了Biscuit的實現(xiàn)和代碼量,。
Biscuit利用Go 1.0 runtime提供的多核并行“標志-清除”垃圾回收器,減少垃圾回收過程對業(yè)務暫停時間,。該時間與系統(tǒng)存活對象數(shù)量成正比,,而與回收周期成反比。
3.7 Biscuit代碼和實現(xiàn)
Biscuit kernel基本由Go語言編寫,,Go語言有27,583 行,,匯編有1546,完全沒有C代碼,,下圖展示各組件的代碼構(gòu)成,。
Biscuit實現(xiàn)58個系統(tǒng)調(diào)用,對于Linux程序nginx和redis已經(jīng)夠用,。Biscuit實現(xiàn)磁盤驅(qū)動和網(wǎng)卡驅(qū)動,,需要訪問硬件DMA地址空間,需要使用unsafe.Pointer 訪問寄存器,,網(wǎng)絡報文,,物理頁內(nèi)存,用戶態(tài)內(nèi)存,,使用atomic package控制內(nèi)存訪問順序,。
Biscuit的一個設計原則是盡管不修改Go runtime代碼,而runtime在運行過程加鎖時并沒有關(guān)閉中斷,因此為了避免死鎖,,Biscuit在中斷只是打上標記,,中斷退出后,再喚醒中斷服務線程,。
Biscuit的調(diào)度也完全由Go runtime掌管,,無法實現(xiàn)優(yōu)先級調(diào)用策略,。在Goroutine上下文切換時,,無法更換硬件頁表,只能在返回用戶前切換頁表,。線程在切換后,,返回戶用態(tài)前需要訪問用戶態(tài)內(nèi)存時,使用軟件查頁表找到物理地址,,然后軟件權(quán)限檢查,,最后才內(nèi)存訪問。
4. 性能評估
論文作者開篇就提出使用高級語言開發(fā)kernel的好處和代價,,設計Biscuit僅僅是作為實驗從各維度評估Go vs C開發(fā)Kernel的好處和代價,。評估分為以下幾個方面:
Biscuit橫向與其它項目相比,使用Go語言的特性分析,,想說明使用高級語言能給Kernel提高開發(fā)效率
安全性,,高級語言編寫kernel能有效減少安全漏洞
Go語言高級特性給開發(fā)kernel帶來怎樣的性能耗損
4.1 Go特性在Biscuit使用情況
論文作者橫向?qū)Ρ?個采用Go語言開發(fā)項目:Biscuit, Golang和Moby,分析項目中每1000行代碼使用的特性數(shù),,結(jié)果如下圖:
從圖上可以看到3個項目較多使用Go的內(nèi)存管理機制,,避去復雜的對象生命周期管理。使用Slice,,String,,Multi-return和Closure,更多像語法糖,,提升代碼生產(chǎn)率,,以及減少編碼出錯;Defer方便處理資源釋放,。Channel用于Goroutine線程同步數(shù)據(jù),。
4.2 Biscuit能減少哪些CVE漏洞
CVE數(shù)據(jù)庫披露Linux Kernel社區(qū)在2017年一共有65個公共漏洞,其中11個bug不確定在Biscuit是否能避免,,還有14個bug屬于邏輯問題,,在Biscuit也會出現(xiàn)。最后剩下的40個bug與內(nèi)存訪問相關(guān),,有釋放后使用,,重復釋放,下標溢出訪問這3類。相比之下,,Biscuit比Linux Kernel有更好的安全性,,因為Go語言能在運行防止不安全內(nèi)存訪問,即時發(fā)生運行錯誤,,避免進一步攻擊,。
4.3 Biscuit性能分析
論文作者將Linux Kernel不太相關(guān)的功能組件關(guān)閉(比如cgroup,隨機地址化,,透明大頁,,零拷貝,ftrace, kprobe),,讓Biscuit和Linux Kernel執(zhí)行路徑大致相同,。運行nginx和redis兩個服務程序進行性能對比,結(jié)果如下:
client和server采用ping-pong測試模式時,,C語言比Go性能高15%,,專門針對Page fault做性能測試時,發(fā)現(xiàn)C比Go性能高5%,。對Go語言來說,,由于垃圾回收的存在,對性能影響也不可忽略,,垃圾回收占CPU開銷的1%~3%,,致命的是它影響業(yè)務時延,造成業(yè)務單次請求的最大時延在574ms,。
5. 思考和借鑒
論文作者使用Go語言開發(fā)Biscuit僅僅做為一個實驗,,幫助OS設計者去理解、剖析高級語言編寫Kernel需要考慮哪些維度,。高級編程語言,,提供更好的抽象能力,類型安全和內(nèi)存安全,,此外像Go語言還提供很多非常強大的功能:協(xié)程,、垃圾回收、多線程同步原言,,大大簡化kernel開發(fā)工作量,。Biscuit項目引入Go語言,因其類型安全和內(nèi)存安全,,系統(tǒng)運行更安全,,性能下降15%,用安全性換取性能,,在安全優(yōu)先的場景下是值得的,。此外,,Go語言提供Goroutine機制和帶垃圾回收器的內(nèi)存管理方式,卻讓Biscuit kernel在線程調(diào)度上強依賴于Go runtime的調(diào)度機制,,無法做調(diào)度算法上的創(chuàng)新,,垃圾回收的內(nèi)存管理方式,讓Biscuit在內(nèi)核內(nèi)存耗盡情況下,,也大費力氣解決,,完全無法掌整個kernel的核心設計,這是Go語言開發(fā)kernel的不足,。
是不是所有高級語言都與Go語言大同小異的,,其實還不是,Rust語言是最近系統(tǒng)級編程語言的新星,。Rust與Go不同,,它不支持垃圾回收機制,,它的設計哲學就認為垃圾回收很難做到高性能,。Rust沒有垃圾回收功能,并不意味內(nèi)存就不安全了,,它提供所有權(quán),,借用和引用機制,讓開發(fā)人員編寫內(nèi)存安全的程序,,并支多線程并發(fā)安全訪問,。Rust的好處是提供類型安全和內(nèi)存安全,多線程內(nèi)存安全訪問機制,,但沒有了Go的runtime的約束,,讓Kernel開發(fā)人員聚焦OS架構(gòu)和方案創(chuàng)新。
采用C語言開發(fā)Kernel,,盡管可以獲得很高的性能,,但不可避免地引入很多安全問題。而采用像Go一樣,,帶垃圾回收,,協(xié)程調(diào)度等厚重的語言機制,盡管可以高效編寫kernel,,但也需要為這些機制付出代價,。而Rust這類的高級語言,提供類型安全和內(nèi)存安全機制,,并沒有runtime的束縛,,讓Kernel開發(fā)有更大創(chuàng)新空間。