是時候淘汰對操作系統的 fork() 調用了

概述

一般觀點認爲針對線程創建Unix的fork()與exec()的組合堪稱絕配,但微軟研究院與波士頓大學聯合發表的一篇論文則提出了相反的觀點。他們認爲fork在當下早已過時,對操作系統和應用程序的設計弊大於利,並給出了一些替代fork的方案和未來的發展路線建議。

1 引言

當初人們在開發Unix的時候需要一種創建線程的機制,於是他們發明了一個特殊的系統調用:fork()。Fork會創建一個與其父進程(fork的調用者)相同的新進程,但系統調用的返回值除外。現在的開發者都習慣了Unix中用fork()加上exec()執行子進程中不同程序的用法,但相比非Unix系的操作系統來說,這種用法還是比較特立獨行的[例如,1,30,33,54]。

50年過去了,fork仍然是POSIX上默認的線程創建API:Atlidakis等[8]發現有1304個Ubuntu包(佔總數的7.2%)會調用fork,相比之下更現代化的posix_spawn()只有41個包在用。幾乎所有的Unix內核系統、主流Web和數據庫服務器(例如Apache、PostgreSQL和Oracle)、Google Chrome、Redis鍵值存儲甚至Node.js都在使用fork。大家似乎認爲fork是很好的設計。我們審查的幾本操作系統教科書[4,7,9,35,75,78]都對其持中立態度,甚至讚譽有加,而且常常會強調它相比其它方法的“簡單性”優勢。今天的專業課會教學生“fork系統調用是Unix的偉大思想之一”[46],並且“設計創建線程的API有很多條路可選,而fork()和exec()的組合是其中既簡單又強大的一條路……Unix的設計者選對了路”[7]。

如今我們就要糾正這種錯誤。Fork是一種機制:它是上個時代遺留的產物,在現代操作系統中已經過時,甚至有很多害處。我們開發社區對fork很熟悉,但也會因此無視它的問題(§4,第4部分,下同)。公認fork存在的問題包括它沒有線程安全、低效且不可擴展,且帶來了安全問題。此外,fork已經不再像當年一樣簡潔了;如今它會影響自己曾經正交過的其它所有操作系統抽象。此外,fork面臨的一項根本挑戰在於,由於它將進程與其運行的地址空間混爲一談,因此fork會阻礙操作系統功能的用戶模式實現,搞亂從緩衝IO到內核旁路網絡的所有內容。也許最大的問題在於fork不支持compose——但系統的每一層,從內核到最小的用戶模式庫都必須支持它。

我們使用在先前研究系統中獲得的經驗來說明fork對操作系統實現帶來的壞處(§5)。Fork限制了操作系統研究者和開發者的創新能力,因爲新的抽象都必須專門定做。有效支持fork和exec的系統被迫懶惰地複製每個進程狀態。這還促進了狀態的中心化,這是不用單內核構建的系統面臨的主要問題。另一方面,不支持fork的創新系統原型也無法運行大量需要fork支持的軟件。

我們最後討論了備選方案(§6)併發出了號召(§7):fork應移除出我們系統的第一類原語,並用良好的模擬方法替換,爲舊式應用程序提供兼容性。僅向操作系統添加新原語是不夠的,fork必須從內核中刪掉。

2 歷史起源:fork最初是一種取巧

一般認爲,最早實現fork操作的項目是Project Genie分時系統[61]。Ritchie和Thompson [70]聲稱Unix fork“基本上和我們在Genie中實現的是一樣的”。但是,Genie監視器的fork調用比Unix更靈活:它允許父進程爲新的子進程指定地址空間和機器上下文[49,71]。默認情況下,子進程共享其父進程的地址空間(有點像現代線程);根據需要也可以給子進程一個完全不同的內存塊的地址空間供用戶訪問;後者可能用來運行不同的程序。最重要的是,這裏沒有工具來複制地址空間,而是由Unix無條件完成的。

Ritchie [69]後來指出“Unix引入fork的主要原因可能是它比較容易實現,不用改變太多東西。”他接着講到了當年的PDP-7計算機如何用27行代碼第一次實現了fork,包括將當前進程複製到虛擬內存,並將子進程保留在內存中。Ritchie還指出,Unix的fork-exec組合“當其中的exec並不存在時,這個組合就會變得非常複雜;它的功能已經由shell使用顯式IO執行了。“

TENEX操作系統[18]爲Unix的路子提供了一個值得注意的反例。它也受到了Project Genie的影響,但它的發展和Unix互相獨立。它的設計者也爲進程創建引入了fork調用,但與Genie更相似的是,TENEX fork要麼共享了父進程之間的地址空間,要麼創建了具有空地址空間的子進程[19]。它沒有Unix風格的地址空間複製,可能是因爲它能用到虛擬內存硬件了。

Unix fork不是一種“必然性”[61]的產物。它只是一種權宜之計,照搬PDP-7中的實現而已;結果50年過去了,它卻已遍佈現代操作系統和應用程序了。

3 FORK API的優點

當Unix爲PDP-11計算機(其帶有內存轉換硬件,允許多個進程保留駐留)重寫時,只爲了在exec中丟棄一個進程就複製進程的全部內存就已經很沒效率了。我們懷疑在Unix的早期發展階段,fork之所以能倖存下來,主要是因爲程序和內存都很小(PDP-11上有隻8個8 KiB頁面),內存訪問速度相對於指令執行速度較快,而且它提供了一個合理的抽象。這裏有兩點很重要:

Fork很簡單。除了易於實現之外,fork還簡化了Unix API。最明顯的是fork不需要參數,因爲它爲新進程的所有狀態簡單地提供了一個默認值:從父進程繼承過來。與之形成鮮明對比的是,Windows CreateProcess()API採用顯式參數來指定子項內核狀態的所有細節——包括10個參數和許多可選flag。

更重要的是,使用fork創建進程和啓動一個新程序是正交的,且fork和exec之間的空間有自己的用途。由於fork複製了父進程,因此允許進程修改其內核狀態的系統在發起調用後,可以在exec之前在子進程中複用:shell在命令執行之前就可以打開、關閉和重新映射文​​件描述符,並且程序可以減少權限或更改子項的命名空間以在受限上下文中運行它。

Fork簡化了併發。在多線程或異步IO流行之前的年代,不用exec的fork提供了有效的併發形式。在共享庫流行之前,它帶來了一種簡單的代碼複用形式。程序可以初始化,解析其配置文件,然後fork自身的多個副本,這些副本從相同的二進制文件中運行不同的函數,或處理不同的輸入。這種設計延續到了預fork服務器中,我們會在§6中再講。

4 現代的fork

乍一看,fork現在好像還是很簡潔。我們認爲這是一個美麗的謊言,而且這種fork效應對現代應用來說弊大於利。

Fork已經不再簡潔了。Fork的語義已經影響了所有新的創建進程狀態的API設計。POSIX規範列出了25個關於如何將父狀態複製到子進程[63]的具體情況:文件鎖、定時器、異步IO操作、跟蹤等等。此外,許多系統調用flag會控制fork的行爲,如內存映射(Linux madvise()flag,MADV_DONTFORK/DOFORK/WIPEONFORK等)、文件描述符(O_CLOEXEC,FD_CLOEXEC)和多線程(pthread_atfork())。所有新式操作系統工具都必須用fork記錄其行爲,並且必須準備好用戶模式庫,以便隨時fork它們的狀態。Fork的簡潔性與正交性如今已蕩然無存。

Fork不會compose。因爲fork複製了整個地址空間,所以它不適合在用戶模式下實現的操作系統抽象。緩衝IO就是一個典型的例子:用戶必須在fork之前顯式刷新IO,以免重複輸出[73]。

Fork是非線程安全的。今天的Unix進程支持多線程,但fork創建的子進程只有一個線程(調用線程的副本)。除非父進程對其他線程也逐個fork,否則子地址空間最後可能會與父進程不一致。一個簡單但常見的情況是一個線程進行內存分配並持有堆鎖,而另一個線程fork。任何在子進程中分配內存的嘗試(從而獲得相同的鎖)都將立即死鎖,等待永遠不會發生的解鎖操作。

編程指南建議不要在多線程進程中使用fork,或者fork之後立即調用exec [64,76,77]。POSIX僅保證在fork和exec之間可以使用一小部分“異步信號安全”函數,特別是排除malloc()以及標準庫中其它可能分配內存或獲取鎖的標準庫中的內容。真正的多線程程序如果fork,可能會在實踐中出現各種錯誤併爲之困擾[24-26,66]。

很難想象有哪位理智的內核維護者會在內核中加入一個有這麼多限制屬性的系統調用。

Fork是不安全的。默認情況下,fork出的子進程從其父進程繼承所有內容,並且程序員要負責顯式刪除子進程不需要的狀態:他要關閉文件描述符(或將其標記爲close-on-exec)、從內存中清除機密 、使用unshare()[52]等隔離命名空間。從安全角度來看,fork的默認繼承行爲違反了最小特權原則。此外,fork但不執行的程序使地址空間佈局隨機化無效,因爲每個進程都具有相同的內存佈局[17]。

Fork很慢。自Thompson首次應用fork以來的幾十年中,內存大小和相對訪問成本不斷增長。即使在1979年(當時第三個BSD Unix引入了vfork()[15]),fork已經有了性能問題,多虧了寫入時複製技術[3,72]才讓它的性能表現可以被接受。今天,就連建立寫時複製映射的時間也成了一個問題:Chrome在fork [28]中的延遲長達100毫秒,並且在exec之前fork時,Node.js應用會被阻塞幾秒鐘[56]之久。

image

Fork現在太拖累性能了,所以C語言庫特意避免在posix_spawn()[34,38]中使用它,而Solaris將spawn用作了原生系統調用[32]。但是,只要應用程序還是直接調用fork,它們就會付出高昂的代價。圖1對比了在3.6 GHz的Intel i7-6850K CPU上,Ubuntu 16.04.3下不同大小的進程fork和exec的時間。髒線顯示使用髒頁fork進程的開銷,必須將其降級爲只讀來做寫入時複製映射。在碎片化的情況下,父對象只會污染它的堆棧,但會通過交替分配只讀和讀寫頁面來模擬複雜應用的內存佈局,後者的複雜性體現在共享庫、隨機化地址空間和實時編譯等。相比之下,無論父進程的大小或內存佈局如何,posix_spawn()需要相同的時間都一樣(大約0.5 ms)。

Fork無法擴展。在Linux中,設置fork的寫時複製映射所需的內存管理操作會損害可擴展性[22,82],但真正的問題在更深的層次:正如Clements等人[29]所觀察到的,fork API的規範本身就引入了一個瓶頸,因爲(與spawn不同)它無法與進程上的其他操作通信。其他因素進一步阻礙了fork的可擴展實現。直觀地說,擴展系統規模就要避免不必要的共享。Fork進程啓動時就與其父進程共享所有內容。由於fork複製了進程操作系統狀態的所有方面,因此它鼓勵將該狀態集中在單體內核中,這樣複製和/或引用計數開銷較少。這樣就難以實現諸如用於安全性或可靠性的內核劃分了。

Fork鼓勵內存過度使用。在考慮寫時複製頁面映射所使用的內存時,fork的實現者面臨着一個艱難的選擇。這樣的頁面都代表了一個潛在的分配——如果頁面的任何副本被修改,將需要一個新的物理內存頁面來解決頁面錯誤。因此,保守的實現會讓fork調用失敗,除非有足夠的後備存儲來應對所有潛在的寫時複製錯誤[55]。但是,當一個大進程執行fork和exec時,會創建許多寫時複製頁面映射但從不去修改,尤其是exec過的子進程很小時更是如此;並且因爲最壞的分配情況(進程的虛擬空間加倍)無法實現就導致fork失敗是不可理喻的。

另一種方法,也就是Linux上的默認方法是過度使用虛擬內存:建立虛擬地址映射的操作(包括fork的地址空間的寫時複製克隆)無論是否存在足夠的後備存儲,都會立即成功。後續頁面錯誤(例如,對分fork頁面的寫入)可能無法分配所需的內存,而調用基於啓發式的“內存外殺手”來終止進程並釋放內存。

需要明確的是,Unix並不需要過度使用,但我們認爲寫入時複製fork(而不是類似於spawn的工具)的廣泛應用讓這種現象氾濫了。現實應用程序並沒有準備好處理fork [27,37,57]中明顯虛假的內存不足錯誤。Redis使用fork進行持久化,明確建議不要禁用內存過量提交[67];否則,Redis必須限制在總虛擬內存的一半用量,以避免在內存不足的情況下被殺死的風險。

總結。今天的Fork是適合單線程進程的API,具有較小的內存佔用和簡單的內存佈局,需要對其子進程的執行環境進行細粒度控制,但不需要與它們完全隔離。換句話說,它是一個shell。毫不奇怪,Unix shell是第一個fork [69]的程序,fork的支持者也會拿shell舉例作爲其優雅的證明[4,7]。但是,大多數現代程序都不是shell。爲了方便shell而去優化操作系統 API現在還是個好主意嗎?

5 實現fork

雖然很難量化在現有系統上實現fork的成本,但有明顯證據表明支持fork限制了操作系統體系結構的變化,並限制了操作系統適應硬件演進的能力。

Fork與單個地址空間不兼容。許多現代上下文將執行限制在單個地址空間,包括picoprocess [42]、unikernels [53]和en- claves [14]。儘管有數量龐大操作系統研究者在使用並改進Unix系統,但如果研究者使用的是不基於fork的系統,那麼就更容易適應這些環境。

例如,Drawbridge libOS[65]在隔離的用戶模式地址空間內實現二進制兼容的Windows運行時環境,稱爲picoprocess。Drawbridge支持同一共享地址空間內的多個“虛擬進程”; CreateProcess()是通過在地址空間的不同部分加載新的二進制文件和庫,然後創建一個單獨的線程來開始執行子進程,同時確保跨進程系統調用是按預期運行實現的。不用說,這些進程之間沒有安全隔離——主picoprocess負責提供安全邊界。然而,該模型已被用於在SGX Enclave內支持完整的多進程Windows環境[14],使包含多進程和程序的複雜應用程序能夠部署在enclave中。

相比之下,fork在單地址空間[23]中需要複雜的編譯器和鏈接調整[81]才能實現。因此,從Unix系統派生的Unikernels不支持內部多進程環境[44,45],並且在enclave中運行多進程Linux應用程序要複雜得多。SCONE和SGX-LKL僅支持單進程應用程序[6,50]。Graphene-SGX [79]通過在新的主機進程中創建一個新的接口來實現fork,然後通過加密的RPC流複製父進程的內存;這套操作可能要花幾秒鐘的時間。

Fork與異構硬件不兼容。Fork將進程的抽象與包含它的硬件地址空間混合在一起。實際上,fork將進程的定義限制爲單個地址空間,並且(如前所述)是在某個核心上運行的單個線程。

現代硬件和在其上運行的程序並不是這樣的機制。硬件越來越異構化,並且使用諸如帶內核旁路NIC[12]的DPDK,或使用GPU的OpenCL的進程無法安全地fork,因爲操作系統無法複製NIC/GPU上的進程狀態。這種困境似乎已經困擾了GPU程序員至少十年[58-60,74]。隨着未來的片上系統包含越來越多的有狀態加速器,這種情況只會變得更糟。

Fork會感染整個系統。僅支持fork對系統的設計和運行時環境造成了很大的限制。任何層的高效fork都需要在其下的所有層上都有基於fork的實現。例如,Cygwin是Windows的一個POSIX兼容環境;它實現了fork以運行Linux應用程序。由於Win32 API缺少fork,Cygwin在CreateProcess()[31,47]之上模擬它:它創建一個新的進程,在恢復子進程之前運行與父進程相同的程序並複製所有可寫頁面(數據部分、堆、堆棧等)。這既不快也不可靠,並且可能由於多種原因而失敗,最常見的失敗是當父和子進程中的存儲器地址因地址空間佈局隨機化而不同時出現的。

諷刺的是,NT內核本身支持fork;只有Cygwin所依賴的Win32 API纔沒有(用戶模式庫和系統服務不支持fork,因此fork的Win32進程會崩潰)。作爲一個抽象,fork無法compose:除非每個層都支持fork,否則無法使用它。

在研究用操作系統中fork:K42的經驗

許多研究用操作系統都面臨着是否(以及如何)實現fork的困境,本文作者就親身經歷了6個這種案例[13,36,41,48,51,80]。這種選擇至關重要。實現fork打開了支持大量Unix派生應用程序的大門,其中最先用到的是shell和構建工具,它們可以簡化整個系統的創建過程。然而fork也讓研究者束手束腳:但凡一個系統實現了fork,尤其是想要高效實現fork或者在開發初期就引入fork的系統都會無可救藥地變成類Unix的設計。

K42 [48]是基於我們在Tornado [36]的經驗上開發的系統,展示了對多處理器友好的面向對象方法、基於各個應用程序的可定製對象和微內核架構[5]的好處,以實現普遍的局部性和併發優化。我們的目標是構建一個成熟的通用操作系統,在(可能)非常大規模的多處理器平臺上支持使用多操作系統特性的大量應用程序。最後,K42與POSIX兼容並且兼容Linux ABI,但是爲Linux特性執行fork操作的設計導致fork語義顛覆了整個操作系統設計,對其它特性都帶來了負面影響。

我們開始以爲我們能夠像Cygwin一樣實現fork:作爲用戶級庫函數,通過適當地構造必要的新對象實例來創建現有進程的子副本。這本身並不是個問題。相比之下,爲了允許任何進程在任何時候都可fork,並且在追求高性能表現的同時有效地做到這一點的努力最終失敗了——隨之而來的複雜性讓我們放棄了幾乎所有特性,只剩下對Unix的支持和對我們原生特性的支持了。

尤其嚴重的是,以下問題幾乎滲透到了系統的每個方面:

反模塊化:只要是可能支持正在運行進程的對象實現就需要在進程fork時定義其行爲。這讓實現專用組件變得非常複雜,這些組件的目的可能僅僅是爲長期運行的並行科學計算或服務器引入局部優化而已,根本用不着fork。

內在的懶惰需求:鑑於每個核心的資源,從內存區域和文件到特定特性的抽象,諸如文件描述符和信號處理器都需要fork支持,我們只能實現懶惰寫時複製行爲來緩解性能壓力。這不僅增加了單個對象中的複雜度,還需要對象交互來維護fork創建的層次關係。這與我們限制共享和同步的目標背道而馳,結果損害了局部性。

中心化:操作系統的可擴展性是通過避免中心化的策略和避免確切全局知識的機制來實現的[11]。因此,跨對象實例和服務器的分解狀態和功能成爲了我們的核心理念。但是,儘管fork是在庫代碼中協調的,它還是需要與進程可能連接的所有服務器和對象通信。

可擴展性較差:除了違反我們的核心可擴展性原則之外,在NUMA系統中fork必須要麼訪問父進程位置的存儲器,要麼將子進程安排在系統的受控部分中;這些都是我們花費大量精力去解決的固有問題。

事後看來,我們犯了一個錯誤,沒有仔細評估fork的實際用例。如果我們將K42的fork侷限到單線程進程(例如shell)上,我們就可以避免讓它的複雜性影響到核心對象了。

6 取代fork

既然fork有這麼多問題,那麼該用什麼來取代它? 創建新進程會往往會引出混亂的API設計問題,因爲任何選項都必須隱式或顯式地指定屬於新進程的所有操作系統資源的初始狀態。Fork的應對很簡單:複製一切,結果如我們所見最後成爲了fork的軟肋。爲了取代fork,我們提出了一個上層spawn API和一個底層微內核API的組合,以便在執行之前設置一個新進程。然後我們討論了無需exec的fork的替代方案。

上層:Spawn。在我們看來,fork和exec的大多數功能最好改由spawn API提供。這種改動所需的重構工作可能會很棘手,尤其是當fork和exec在代碼中的位置並不好找的時候;但正如我們在§4中所示,這種方案的性能和可靠性有着顯著優勢,更不用說可移植性了。值得注意的是,使用fork的主流應用程序(例如,Apache,Chrome,PostgreSQL)的Windows端口並不支持fork,因此fork顯然不是必需的。

posix_spawn()API可以簡化這種重構。spawn屬性不要求在單個調用站點上提供影響新進程的所有參數(如CreateProcess()的情況),而是由可擴展定義的輔助函數設置。例如,fork之前的close()可以用預生成的調用替換,該調用記錄了在子進程中發生的“關閉動作”。不幸的是,這意味着API被指定爲由fork和exec實現,儘管這實際上沒必要[32]。

posix_spawn()的主要缺點在於,它不是fork和exec的完整替代。它尚不支持一些不太常見的操作,例如設置終端屬性或切換到隔離的命名空間;它還缺少有效的錯誤報告機制:在子進程開始執行之前發生的故障(例如無效的文件描述符參數)是異步報告的,並且與子進程的終止無法區分。這些缺點可以而且應該得到糾正。

替代方案:vfork()。這種流行的fork變體由BSD引入作爲優化措施[15];它創建了一個直到子進程調用exec前共享父地址空間的新進程,更像是原始的Genie fork [71]。爲了讓子進程能使用父進程的堆棧,它會阻止父進程執行,直到exec爲止。這種編程風格類似於fork,其中新進程在exec之前調整其內核狀態。但由於地址空間共享,vfork()很難安全使用[34]。雖然vfork()避免了克隆地址空間的成本,並且在難以重構代碼使用spawn的場合可以用來替換fork,但在大多數情況下最好別用它。

底層:跨進程操作。雖然大多數啓動新程序的實例都喜歡類似於spawn的API,但爲了完全通用,它需要一個flag、參數或者輔助函數控制過程狀態的所有可能方面。單個操作系統 API無法完全控制新進程的初始狀態。在今天的Unix中,高級用例的唯一後備仍然是在fork之後執行的代碼,但是整潔狀態設計[例如,40,43]已經演示了一種替代模型,其中修改每個進程狀態的系統調用不僅限於當前進程,而可以操縱調用者能訪問的任何進程。這就帶來了fork/exec模型所有的靈活性和正交性,但避開了後者的大多數缺點:一個新的進程從一個空的地址空間開始,一個高級用戶可能以零碎的方式操作它,填充它的地址空間和內核執行前的上下文,無需克隆父項,也不需要在子項的上下文中運行代碼。ExOS[43]使用這種原語的頂層用戶模式實現了fork。將跨進程API納入Unix看起來很有挑戰性,但也會對未來的研究有所幫助。

替代方案:clone()。這個系統調用是Linux上所有進程和線程創建的基礎。就像它之前的Plan 9的rfork()一樣,它需要單獨的flag來控制子進程的內核狀態:地址空間、文件描述符表、命名空間等。這避免了fork的一個問題:它的行爲對於許多抽象是隱式的或未定義的。但是,對於每個資源都有兩個選項:在父項和子項之間共享資源,或者複製它。因此,clone遇到了fork所面臨的大多數問題。

只使用fork的用例。一些特殊情況下,fork後面並不會跟着需要複製父進程的exec。

多進程服務器。傳統上,構建併發服務器的標準方法是fork進程。然而,推動多進程服務器的動力早已不復存在:操作系統庫是線程安全的,並且困擾的多線程或事件驅動服務器的可擴展性瓶頸已經消失[10]。雖然從故障隔離的角度來看進程邊界可能還有其價值,但我們認爲使用spawn API啓動這些進程更有意義。當大多數併發由多線程處理,且現代操作系統會對內存進行重複數據刪除的情況下,fork帶來的共享初始狀態的性能優勢就沒那麼明顯了。最後,使用fork時所有進程要共享相同的地址空間佈局,並且容易受到盲目ROP攻擊[17]。

寫時複製內存。fork的現代實現使用寫時複製來減少複製很快會被丟棄的內存的開銷[72]。由此以來許多應用程序只依賴fork來獲得對寫時複製內存的訪問權限。一種常見模式是從預先初始化的進程中分離,以減少工作進程的啓動開銷和內存佔用,如Linux上[4]的Android Zygote [39,62]和Chrome站點隔離。另一種模式使用fork來捕獲正在運行的進程的地址空間的一致快照,允許父進程繼續執行;這包括Redis [68]中的持久性支持,以及一些反向調試器[21]。

POSIX將受益於一個API,它可以無需fork新進程就使用寫時複製內存功能。Bittau [16]建議使用checkpoint()和resume()調用來獲取地址空間的寫時複製快照,從而減少安全隔離的開銷。最近,Xu等人[82]觀察到fork花費的時間是影響fuzzing工具性能的主要因素,並提出了類似的snapshot()API。這些設計尚不足以涵蓋上述所有用例,但也許可以作爲新的起點。我們注意到,任何新的寫時複製內存API都必須解決§4中描述的內存過度使用問題,但是將此問題與fork解耦應該處理起來會簡單些。

7 讓我的操作系統擺脫fork!

我們已經解釋了爲什麼fork已經是舊時代的老古董了,它會損害應用程序和操作系統的設計。我們必須做三件事來糾正這種情況。

棄用fork。由於Unix的廣泛流行,未來的操作系統在很長時期內都需要支持fork;但不管怎樣,50年前的一種偏門技巧不應該決定未來操作系統的設計。因此,我們強烈建議不要在新代碼中使用fork,並嘗試將其從現有應用程序中刪除。一旦fork離開了性能關鍵路徑,它就可以從操作系統的核心中刪除,並根據需要重新實現。如果未來的系統僅在有限的情況下支持fork,例如單線程進程[2],那麼仍然可以在無需非必要的複雜性的情況下運行傳統軟件。

改進替代方案。很長一段時間裏,fork已經成爲類Unix系統上的通用進程創建機制,其他抽象層則疊在最頂層。值得慶幸的是這種情況已經開始改變[32,38],但是還有更多工作要做(§6)。

修正我們的課本。顯然,學生需要學習fork,但是目前大多數教科書(和教師)都使用fork [7,35,78]做例子來講解進程創建過程。這不僅會延長fork的生命期,而且是在灌輸過時的知識——這種API根本就不直觀。正如現代編程課程不會以goto開頭一樣,我們建議大家教授posix_spawn()或CreateProcess(),然後將fork作爲其歷史背景的特殊情況講一講就夠了(§2)。

本文的備註可在原始論文附註中查看。

查看英文原文:https://www.microsoft.com/en-us/research/publication/a-fork-in-the-road

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章