在現代作業系統中超越 fork() 與 exec()

傳統的 Unix 程序建立模型,基於 fork()exec() 的組合,正隨著開發者與系統架構師認為其為現代運算中低效率的抽象層而面臨重新審視。雖然此模型曾是早期 Unix 設計的基石,但在替換為新的執行檔之前必須先複製父程序,這項要求通常是不必要的且在計算上非常昂貴。

fork() + exec() 模型的低效率

fork() + exec() 模式的核心問題在於它創造了一個冗餘的複製與捨棄的循環。在大多數使用情境中,開發者想要啟動一個完全全新的程序,而非當前程序的複製品。使用 fork() 會強迫作業系統複製程序狀態,而 exec() 隨即又會捨棄該狀態以載入新的二進位檔。

效能開銷與寫入時複製(Copy-on-Write)的迷思

雖然寫入時複製(Copy-on-Write, CoW)是一種防止立即複製所有實體記憶體的優化技術,但它並未消除 fork() 的效能成本。

"It is a weirdly common misconception that that fork() is cheap... it is O(N) on the size of the process, and it always has been. Yes, it's copy on write... but there is a linear relationship between the size of the process and the number of page table entries required to represent it."

由於核心必須仍然複製頁表(page tables)來表示程序的記憶體空間,fork() 的成本會隨著程序的大小線性增加。對於大型程序,這項開銷會成為顯著的瓶頸,特別是當程序緊接著呼叫 exec() 時。

程序複製的架構批判

除了效能之外,fork() 模型也被批評為概念上有缺陷的抽象層。它強迫開發者在主要目標僅是啟動一個執行檔時,必須使用一種專為複製而設計的機制。

「複製後修正」的反模式

由於 fork() 會建立父程序的精確複製品,開發者經常發現自己處於必須在 fork() 之後但在 exec() 之前「修正」子程序的境地。這包括關閉不必要的檔案描述符(file descriptors)或調整環境變數。這種「先複製再事後修正」的方法容易產生錯誤,且不如直接的程序建立呼叫更直觀。

與其他作業系統模型的比較

一些開發者認為其他作業系統已更整潔地實作了此功能。例如,Windows 的 CreateProcessW 介面被引用為一種更自然的方法,因為它允許開發者直接指定新程序的參數,而不需要對父程序進行初步的複製。

支持 fork() + exec() 模型的論點

儘管存在批評,仍有人主張 fork() + exec() 模型的優雅之處在於其靈活性。透過將程序建立拆分為兩個步驟,作業系統提供了一個時間窗口,讓子程序可以使用標準 API 來配置其環境(例如重新定向標準輸入/輸出)後,新程式才開始執行。

組合式呼叫的挑戰

替代方案的批評者建議,任何組合式的 spawnposix_spawn 風格的呼叫,都需要一份詳盡的配置參數清單,才能達到與 fork() 模型相同的靈活性。若缺乏此點,組合式呼叫若非限制過多,就是會隨著新需求出現而變成一個複雜且難以維護的參數叢集。

建議的替代方案與未來方向

關於取代 fork() 的討論建議了幾條前進路徑,包括將邏輯移入使用者空間或利用核心層級的掛鉤(hooks)。

使用者空間函式庫與 eBPF

有些人建議,對於重複啟動程序的開銷(例如在長時間運行的操作中重複呼叫 git),應透過將工具轉換為可直接連結的函式庫來解決,從而完全避免程序建立的需求。

其他人則提議一種更現代的核心方法,或許是利用 eBPF (extended Berkeley Packet Filter) 來允許進階使用者透過掛鉤來自定義程序建立的流程,使他們能夠在保持複雜配置所需靈活性的同時,跳過不必要的複製步驟。

Sources