RFC-0126:驅動程式執行階段 | |
---|---|
狀態 | 已接受 |
區域 |
|
說明 | 驅動程式運作的新執行階段規格。 |
Gerrit 變更 | |
作者 | |
審查人員 | |
提交日期 (年-月-日) | 2021-08-04 |
審查日期 (年-月-日) | 2021-09-21 |
摘要
這項 RFC 會建立設計,讓在程序中同處的驅動程式彼此通訊。驅動程式會透過內部處理程序的執行階段進行通訊,而這個執行階段是以 Zircon 核心為範本。這個執行階段會提供類似於 zircon 管道和埠的原始元素,並在這個執行階段上方建構新的 FIDL 傳輸,讓效能超越透過 zircon 管道和埠可達到的水準。這個新的執行階段和 FIDL 傳輸工具將取代目前驅動程式使用的現有 banjo 執行階段。
在同一程序中未同時執行的驅動程式之間,將繼續使用以 zircon 管道為基礎的 FIDL。讓底層傳輸作業對驅動程式庫透明,已超出本提案的範圍,因此將在日後的提案中討論。
此 RFC 也為執行緒模型驅動程式應遵循的一組規則奠定基礎,以便有效地彼此共用執行緒。您可以同時處理來自處理中驅動程式和同一個執行緒的非處理程序驅動程式訊息。
提振精神
現有的驅動程式庫執行階段可解決建立時的問題需求。當時還沒有 FIDL,驅動程式幾乎都是以 C 語言編寫,而 Zircon 核心也尚未經過妥善最佳化。因此,用於彼此通訊的介面驅動程式一開始是函式表,其中包含關聯型別的已清除內容指標。在 C++ 驅動程式越來越常見的情況下,我們已建立 FIDL,但由於綁定實作項目的額外負擔過高,因此不認為這項技術能勝任驅動程式庫間通訊的任務。為了改善 C++ 驅動程式的人體工學,我們會手動產生 C 通訊協定定義的包裝函式。為了減少同時維護 C 通訊協定定義和 C++ 包裝函式的繁重工作,我們建立了新的 IDL,其語法大致反映當時的 FIDL,可從共同的真實來源自動產生 C 和 C++ 程式碼。後來這個名稱演變成「banjo」。雖然 banjo 確實獲得了一些初期投資,但很快就進入維護模式,而且與在類似時間內蓬勃發展的 Fidl 相比,多年以來幾乎沒有任何進展。Banjo 已無法滿足目前的需求,因此必須重新構思驅動程式庫執行階段。
這項設計的動機源自於多項不同的問題,希望能一次解決這些問題。
穩定版 ABI
驅動程式庫程式架構正在處理最重要的問題,就是啟用穩定的驅動程式庫 SDK。這是平台層級目標,可協助 Fuchsia 實現廣泛的硬體支援,並確保我們能夠實現更新作業系統的各個部分而不損失功能的目標。Banjo 是目前的驅動程式庫間通訊解決方案,在建構時並未考量 API 和 ABI 的演進。而是以簡單和低負擔為優先。因此,目前沒有任何可靠的機制可用於修改 banjo 程式庫,而不會影響所有依附於該程式庫的用戶端和伺服器。這會讓我們在達成目標的同時,難以發展平台。
執行緒安全性
目前在撰寫驅動程式庫時,如果不想在驅動程式中產生專用執行緒來處理要求,就無法實作 banjo 通訊協定。這是因為在呼叫通訊協定上的方法時,使用這些通訊協定的驅動程式並沒有任何規則需要遵循。處理來電時,您必須確保邏輯符合下列條件:
- 處理同步處理問題,因為用戶端可能會從多個執行緒 (可能會並行) 呼叫。
- 處理重入。
處理第一個問題的明顯方法是取得鎖定。不過,單純取得鎖定機制可能會輕易導致死結,因為驅動程式可能會呼叫回其他驅動程式庫,並在同一個堆疊架構中,呼叫回嘗試重新取得相同的鎖定機制。目前在驅動程式中使用的所有鎖定實作方式都不是可重入安全的,我們不希望開始使用遞迴鎖定,原因請參閱替代方案章節的詳細說明。
因此,目前唯一能正確處理此問題的方法,就是將工作推送至佇列,並稍後處理該佇列。由於目前的驅動程式庫執行階段未提供任何機制,無法排定日後要執行的工作,因此驅動程式庫必須例項化自己的執行緒,才能服務這個佇列。
這會造成問題,因為這會破壞在同一個程序中放置驅動程式的預期效能優勢。此外,驅動程式庫實作複雜度也會造成大量負擔。系統中的許多驅動程式不會選擇將工作推送至佇列,因此可能會使用新用戶端,進而導致系統發生死結。我們有許多歷史記錄,顯示這類錯誤會潛伏在驅動程式中,且只會以不穩定的方式浮現,因為重入只會發生在很少發生的錯誤情況下。
一成不變的序列化
另一個促使我們推出新執行階段的問題,是與非驅動程式通訊時缺乏一致性。驅動程式庫通常會實作 FIDL 服務,並將其公開給非驅動程式庫元件,然後透過將要求轉送至其他驅動程式庫 (通常會進行有限的轉譯) 來處理要求。這個程序充斥著固定程式碼,因為雖然類型定義 (結構體、列舉等) 和協定可能會在 zircon 管道傳輸 FIDL 和 banjo 傳輸 FIDL 之間共用,但產生的類型完全不一致。因此,驅動程式庫作者必須實作邏輯,以便將一組產生的類型序列化為另一組。
這個問題經常被視為目前撰寫驅動程式庫時,最令人頭痛的人體工學問題之一。這個問題以前更嚴重,因為不同 FIDL 傳輸工具之間的類型共用功能直到最近才推出,而 banjo 本身也只是最近才成為 FIDL 傳輸工具。先前,類型定義很容易與彼此不同步,導致細微的錯誤。如果定義變更時手動序列化邏輯未正確更新,仍可能發生這些錯誤。
相關人員
協助人員:abarth@google.com
審查者:abarth@google.com (FEC 成員)、abdulla@google.com (FDF)、yifeit@google.com (FIDL)、eieio@google.com (排程/Zircon)
諮詢對象:驅動程式團隊、網路團隊、儲存空間團隊、核心團隊、FIDL 團隊和效能團隊的成員。
社交化:本 RFC 草稿已送交 FEC 討論郵寄清單,以供意見。這項設計中所發現的早期概念已在驅動程式庫架構、FIDL 和 Zircon 團隊之間流傳。
設計
具體來說,這項設計會透過一組新的原始元素,以及利用這些原始元素的新 FIDL 傳輸機制來實現。在進一步探討設計細節之前,我們先來瞭解一些額外的要求,這些要求是我們用來完成這項設計的。
需求條件
成效
新版驅動程式庫執行階段的其中一項高層目標,就是透過使用驅動程式庫執行階段,達到更高程度的效能。驅動程式效能要求各有不同,但大致而言,我們希望能針對下列指標進行最佳化:
- 高總處理量
- 每秒輸入/輸出作業數 (IOPS) 高
- 低延遲
- 低 CPU 使用率
舉例來說,我們希望盡可能提高 NVMe SSD 的總處理量,或是確保在輸入事件發生與軟體收到事件通知之間,延遲時間降到最低。雖然在某種程度上,這些工作需要系統與驅動程式庫程式架構協同運作,才能達到理想的效能等級,但我們希望確保驅動程式庫執行階段不會成為瓶頸。
大部分的設計都著重於最佳化,我們可以運用這些最佳化技術來改善驅動程式庫間通訊的效能,超越透過 Zircon 管道和 FIDL 所能達成的效能。如果我們無法在所有上述指標上超越以 zircon 管道為基礎的 FIDL,則可能不適合取代目前的 banjo 執行階段。我們會在後續章節中討論如何超越以 zircon 管道為基礎的 FIDL。
驅動程式作者人體工學
新驅動程式庫執行階段的另一個高層目標,是確保驅動程式仍可簡單編寫。如果不考量設計決策對驅動程式庫作者的影響,就算盡量提高效能,也無法獲得有用的結果。提供作者可選擇人體工學或零成本額外負擔的選項,對於實現我們的目標至關重要。此外,能夠從簡單的實作改為效能更高的驅動程式庫,且無需重寫,是理想的做法。
安全性與彈性
位於相同程序中的驅動程式會共用安全性邊界。這是因為,儘管我們盡力隔離驅動程式,但實際上無法禁止驅動程式存取共用程序中屬於其他驅動程式的記憶體區域。這是有意為之,因為共用位址空間是併列作業的主要優點之一,我們可以利用這項功能提升成效,超越管道可能提供的效果。因此,我們必須假設任何在驅動程式庫程式主機中取得單一驅動程式庫控制權的惡意實體,都能夠存取該程序中所有驅動程式的所有功能。因此,我們不需要考慮在驅動程式庫執行階段層中實作安全性檢查,以提供額外的安全性優勢。
不過,這並不代表我們不應採用許多通常用於提供安全保證的相同機制,因為這些機制可能會提高個別驅動程式的復原能力。舉例來說,我們可以驗證指標是否確實指向緩衝區內的資料,而非盲目假設指標會指向該資料。這有助於限制錯誤造成的損害,並協助找出問題的根本原因。換句話說,防範錯誤的做法仍有助益。其餘設計應以此為考量。
零複製選項
即使在最理想的情況下,以管道為基礎的 fidl 也會產生至少 2 個副本。一個副本從使用者空間進入核心 (透過 zx_channel_write
),另一個副本則從核心進入使用者空間 (透過 zx_channel_read
)。通常,由於用戶端中的線性化步驟,以及選擇在伺服器中以非同步方式處理要求,或將資料移至慣用語言內建類型,因此會產生額外的副本,總計 4 個。由於使用新版驅動程式庫執行階段的驅動程式會存在於相同的程序,因此位址空間也相同,因此最佳情況下只需要 0 個副本,而人體工學情況下只會產生 1 個副本。
在驅動程式之間傳遞的大部分資料 (類似於以管道為基礎的 FIDL) 應為控制資料結構,而資料層資料則儲存在 VMOs 中。這可讓我們減少最昂貴的副本。不過,控制項資料結構體的大小可能會累加,在某些情況下 (例如網路封包批次要求) 會達到數千位元組。此外,在多個驅動程式庫堆疊中,資料幾乎未經處理,並會傳遞至多個層級。我們可以刪除所有這些副本。事實上,在目前以 banjo 為基礎的執行階段中,我們盡力實現零複製,但實現這一目標的方式存在許多問題,且容易發生錯誤。這也是我們無法採用 Rust 驅動程式庫開發功能的原因之一。
值得一提的是,由於系統呼叫額外負擔較大,因此 FIDL 中的副本通常不會視為效能瓶頸。不過,在驅動程式庫執行階段,我們會避免系統呼叫,因此在驅動程式之間傳送訊息時,複製作業會占用相當長的時間。
從效能角度來看,這些副本在現今系統中可能不會造成重大瓶頸。不過,這些功能可能會對 CPU 使用率造成可測量的影響,因此提供可達到零複製的功能會更實用。
推送與拉取
Zircon 管道是以 pull 為基礎的機制運作。使用者空間會註冊管道上的信號,以便瞭解何時有可讀取的資料,並在收到通知後,從管道讀取資料至提供的緩衝區。FIDL 繫結通常會將此機制倒轉為推送式。系統會註冊回呼,並在某些內容準備就緒時觸發回呼。
我們有極大的彈性,可以朝著完全以推送為基礎的機制前進,或是繼續模擬 zircon 管道機制。對於一般使用者而言,推送模式的使用體驗會更符合人體工學,且效能可能更高,因為拉取模型在收到信號後,需要重新輸入執行階段。在選擇提高效能的其他系統中,例如 Windows IOCP 和 Linux io_uring,緩衝區通常會預先註冊,藉此移除核心中的額外項目和副本,以便提升效能。對於程序內執行階段,進入執行階段的成本不高,而且實際上不需要預先註冊緩衝區,就能達到零複製,因此在 API 中引入與 zircon 管道的差異,不一定是好事。此外,Rust 是一種高度依賴 pull 式機制的語言,如果我們選擇在傳輸層級移除 pull,就可能會與其模型產生阻抗不相容的情形。
基本
如先前所述,新設計需要新的原始元素,才能建構新的 FIDL 傳輸機制。這些基元本身大多是根據 Zircon 核心提供的基元建立模型。
原始元素的 API/ABI 將以 C 為基礎,類似於現有的 libdriver API。我們會提供各語言的包裝函式,以便使用者以更符合語言習慣的方式使用。雖然您可以透過 FIDL 定義 API,類似於在 FIDL 中定義 zircon 系統呼叫介面,但由於整體 API 集應保持在最少的程度,因此這項工作可能不值得。
競技場
第一個基本元素是競技場。為了減少副本數量,我們需要確保與要求相關聯的資料生命週期至少與未處理的要求一樣長。提供給傳輸工具的資料必須由緩衝區支援。在以管道為基礎的 FIDL 繫結中,傳入訊息資料通常會從堆疊開始,如果需要非同步回覆要求,則可視需要移至堆積分配。FIDL 繫結可以直接讀取至堆積記憶體,但在撰寫本文時,並未發生這種情況。相反地,如果我們透過執行階段提供的競技場備份資料,就可以將要求的生命週期繫結至競技場。我們也可以針對競技場啟用額外的配置,並保證在要求期間內,這些配置會持續存在。處理要求時,通常會將要求轉送至其他驅動程式庫,或是向下游驅動程式庫提出要求;在這些情況下,同一個競技場可以重新用於新要求並傳遞。透過這種方案,作業可以瀏覽許多驅動程式,而無需進行全域分配。在驅動程式庫堆疊 (例如區塊堆疊) 中,通常會在同一個驅動程式庫主機中瀏覽 6 個以上的驅動程式,大多是將相同要求一再轉送至較低層級的驅動程式,對資料進行最少的修改。
執行階段會提供 API,用於建立競技場、減少對競技場的參照、執行配置,以及檢查提供的記憶體區是否由競技場支援。這個最後的 API 雖然不尋常,但對於 FIDL 傳輸實作項目的健全性而言,卻是必要的。您可以稍後新增釋出個別配置的 API,但初始實作作業會略過這項操作。只有在 Arena 本身遭到銷毀時,才會釋放分配資源,而 Arena 遭到銷毀的情況是指移除所有 Arena 參照時。
執行階段可視需要自行最佳化競技場。舉例來說,基本實作可能會選擇只將所有配置要求轉送至全域 malloc,然後等到競技場銷毀後,再為每個產生的配置發出 free。或者,您可以將其實作為單一分配升級競技場,並設定適應每次要求所需分配量的最大大小。隨著時間推移,我們可能會擴充競技場介面,讓客戶提供提示,說明基礎競技場應採用哪種策略,因為相同的競技場策略在所有驅動程式庫堆疊中可能並非最佳做法。
頻道
第二個原始元素是管道原始元素。其介面幾乎與 zircon 管道相同。兩者的主要差異包括:
- 寫入 API 會接收競技場物件,並遞增其參照計數。
- 寫入 API 會要求所有傳入的緩衝區 (資料 + 句柄資料表) 都由競技場支援。因此,執行階段會取得這些緩衝區 (以及所有由競技場支援的緩衝區) 的擁有權。
- 讀取 API 會提供一個競技場,讓呼叫端取得該參照的擁有權。
- 讀取 API 會為資料和句柄資料表提供緩衝區,而不會要求呼叫端提供要複製進去的緩衝區。
這些差異是必要的,才能讓建構在頂層的 FIDL 繫結達到零複製。
zircon 管道之間的相似之處包括:
- 管道會成對建立。
- 管道不得重複,因此是雙向的單一生產者單一消費者佇列。
調度工具
最後一個原始元素類似於 zircon 通訊埠物件。這項機制並非提供封鎖目前執行緒的機制,而是透過回呼指出目前可讀取或已關閉的註冊管道。我們也會提供註冊驅動程式庫執行階段管道的機制。
駕駛人可以建立多個調度器,就像建立多個連接埠一樣。調度器是實作後續章節所述的執行緒模型的主要代理程式。建立調度器時,您必須指定調度器應在哪種執行緒模式下運作。
此外,我們還會提供 API,用於從驅動程式庫執行階段調度器物件接收 async_dispatcher_t*
,以便等待 zircon 物件和發布工作。針對此調度器註冊的回呼會遵循與執行階段管道相同的執行緒保證。
範例
雖然我們不指定基本元素的確切介面,但在評估設計時,參考假設範例可能會很有幫助:
https://fuchsia-review.googlesource.com/c/fuchsia/+/549562
執行緒模型
除了共用相同的位址空間,共置的驅動程式還有另一項重要優點,那就是可以做出比核心更明智的排程決策。我們不想為每個驅動程式庫提供專屬執行緒,而是想讓驅動程式共用相同的執行緒。當多個獨立的程式碼共用同一個執行緒時,必須遵守某種契約,才能確保正確性。
執行緒模型會設定規則和預期值,讓驅動程式庫作者可以利用這些規則和預期值來確保正確性。如果執行得當,執行緒模型就能大幅簡化開發體驗。不想處理並行性、同步和重入問題的驅動程式開發人員,應具備可讓他們忽略這些問題,同時仍能編寫功能程式碼的機制。在另一個極端情況下,執行緒模型必須提供足夠的自由度,讓驅動程式達到本文件先前所述的成效目標。
為此,我們建議採用以下兩種高階模式的執行緒模型,讓驅動程式在其中運作:
- 並行和同步。
- 並行且未同步。
建議您先定義所使用的術語:
- 並行:多個作業可能會交錯執行。
- 同步:在任何特定時間點,驅動程式庫中不會同時有兩個硬體執行緒。所有呼叫都會依序排序。
- 未同步:在任何特定時間點,驅動程式庫中可能同時有多個硬體執行緒。我們無法保證哪些來電會發生。
同步處理並不代表工作會固定在單一 Zircon 執行緒上。只要保證同步作業,執行階段就能將驅動程式庫自由遷移至所管理的任何執行緒。因此,在同步模式下使用執行緒本機儲存 (TLS) 並不安全。驅動程式庫執行階段可能會提供其他 API。將同步模式視為「虛擬」執行緒或纖維,可能會比較容易理解。
不同步模式可讓駕駛人享有最高程度的彈性。驅動程式庫可能會使用更精細的鎖定方案,以便提高效能,但這會增加額外的工作量。選擇採用此模式的驅動程式,發生程序死結的風險較高,因此我們日後可能會對選擇採用此模式的驅動程式設下限制。
這些模式會在執行階段定義,並與先前所述的驅動程式庫執行階段調度器物件建立關聯。由於可以有多個調度器,因此您也可以在單一驅動程式庫中混合搭配這些模式,以便管理並行處理需求。舉例來說,您可以建立多個串列同步調度器,用於處理兩個硬體裝置,而這兩個裝置之間不必同步。在實際情況中,驅動程式會使用單一調度器,並選擇使用語言專屬結構來管理其他並行處理和同步處理需求。這是因為語言專屬結構 (例如 C++ 中的 fpromise::promise
或 Rust 中的 std::future
) 可讓系統做出更明智的排程決策,比起只使用多個調度器的驅動程式庫程式執行階段,可以更精細地追蹤依附元件。以 C 編寫的驅動程式,由於缺乏任何類型的標準並行管理原始碼或程式庫,因此最有可能使用驅動程式庫執行階段原始碼來管理並行作業。
對驅動程式庫間通訊的影響
由於上述原始類別未提供任何機制,讓驅動程式庫可呼叫至其他驅動程式庫,因此所有呼叫都必須由執行階段調解。當驅動程式庫執行階段管道寫入時,我們可以選擇在同一個堆疊框架中呼叫另一個驅動程式庫,而非自動將工作排入佇列。執行階段可以決定是否要呼叫擁有管道另一端的驅動程式庫,方法是檢查管道另一端註冊的調度器所設為的執行緒模式。如果未同步,則系統一律會在相同堆疊框架中呼叫其他驅動程式庫。如果調度器是同步和並行的,執行階段可以嘗試取得調度器的鎖定,如果成功,則在同一個堆疊框架中呼叫驅動程式庫。如果失敗,則可將工作排入佇列,稍後再處理。
這可能看起來像是簡單的最佳化,但初步基準測試顯示,避免需要返回非同步迴圈,可大幅改善延遲時間和 CPU 使用率。
重入
在所有說明的模式中,驅動程式都不需要處理重入問題。由於執行階段會調解驅動程式之間的所有互動,因此我們也能判斷目前呼叫堆疊中是否已輸入要輸入的驅動程式庫。如果有,則執行階段會將工作排入佇列,而非直接呼叫至驅動程式庫,並在返回原始執行階段迴圈後,由另一個 Zircon 執行緒或相同執行緒提供服務。
封鎖
在先前討論的所有模式中,參與共用執行緒的驅動程式都必須符合隱含的條件,也就是不阻斷。您可能有封鎖執行緒的正當理由。為了支援這些用途,在建立調度器時,除了設定調度器將使用的執行緒模式,驅動程式庫還可以指定調度器在呼叫時是否需要能夠阻斷。這可讓執行階段決定必須建立的執行緒最小數量,以避免發生死結的風險。舉例來說,如果驅動程式庫建立 N 個調度器,且所有調度器都可能阻斷,則執行階段就需要分配至少 N+1 個執行緒,才能為各種調度器提供服務。
我們一開始不會支援在未同步的調度器中指定區塊的功能,但如果有有效的用途,我們可以重新評估這項決定。原因是上述的簡單 N+1 執行緒策略無法運作,因為未知數量的執行緒可能已進入該驅動程式庫,且可能都會封鎖。
工作優先順序 / 執行緒設定檔
某些工作比其他工作優先順序較高。雖然您可以在工作層級指派優先順序並繼承優先順序,但這並非易事。我們不會在這個領域嘗試超越 Zircon 核心目前提供的創新功能,而是會以調度器層級概念提供優先順序。您可以在建立調度器時指定設定檔。雖然執行階段不一定會保證所有回呼都會發生在已設定為使用該設定檔執行的 zircon 執行緒上,但至少會為每個提供的 zircon 執行緒設定檔產生一個 zircon 執行緒。
就執行階段如何最佳處理執行緒設定檔而言,這裡有許多可探索的空間,而且可能需要與 zircon 排程器團隊和驅動程式庫程式作者進行一定程度的協作,才能找到符合執行緒設定檔用途的用例所需的解決方案。
FIDL 繫結
與其完全重新想像 FIDL,不如讓目標為驅動程式庫執行階段傳輸的繫結,能夠做出最少的使用者面向變更,以實現驅動程式庫執行階段提供的優點。預期目前以 zircon 管道為目標的每個語言繫結 (也支援寫入驅動程式) 都會分支,以提供同時以驅動程式庫執行階段傳輸為目標的變體。在初始實作階段,LLCPP 繫結會用於 C++,因為 C++ 是用於撰寫驅動程式的主要語言,且未來一段時間內仍會是支援度最高的語言。在本節的其餘部分中,我們會假設使用 LLCPP 繫結,並針對 FIDL 繫結提供特定說明。
非通訊協定類型應完整使用,且不得修改。這點很重要,因為能夠在多個傳輸工具之間共用產生的類型,是屬性驅動程式庫作者所需的重要功能。
為通訊協定產生的類別和結構體 (也稱為訊息傳遞層) 將會重新產生。我們可以使用範本,以條件式方式充分利用驅動程式庫執行階段 API,但這需要我們為每個通訊協定產生管道和驅動程式庫程式執行階段傳輸的支援。另一個建議的做法是建立最小類別,用於為基礎傳輸層抽象化管道讀取和寫入 API。很遺憾,這不適用於我們的設計,因為在以管道為基礎的 FIDL 中,arena 會是一個尷尬的概念,而且讀取和寫入 API 的緩衝區擁有權關係在兩種傳輸方式之間不一致。您應與負責維護各種繫結的團隊進行一定程度的協作,以便判斷適當的程式碼重複使用程度、差異和繫結。
單次要求領域
如前文所述,驅動程式庫執行階段會提供競技場物件,並與透過管道傳送的訊息一併傳遞。Fidl 繫結應可利用這個領域,實現零複製。
使用者可以選擇不使用這個競技場,以便建立傳送至其他驅動程式的結構。這沒問題,繫結可確保類型會複製至由執行階段提供的競技場指派器所支援的緩衝區。不指定競技場的改善人因工學,必然會導致複製。我們在這裡唯一可能採用的設計選擇,就是讓複製行為非常明顯,以免使用者不小心複製。
使用者可能會選擇在堆疊上配置所有物件,希望這些物件完全在與呼叫本身相同的堆疊框架內使用,這在某些情況下可能會發生,具體取決於執行緒限制。事實上,司機目前就是透過 Banjo 執行這項操作。雖然我們可以透過延遲將類型移至由競技場支援的狀態 (透過類似於接收器中的 ToAsync()
呼叫),以支援這種情況,但我們會避免支援這種情況。相反地,應使用與上述相同的邏輯,讓訊息複製到由競技場支援的緩衝區。如果有更強大的應用情境出現,我們可能會在日後重新考慮這個想法。
訊息驗證
目前,當驅動程式彼此通訊時,我們不會採取任何行動來驗證訊息是否符合所用介面指定的必要合約,也不會執行枚舉驗證或 zircon 處理程序驗證等動作。在新版執行階段中,我們可能會開始執行部分這類驗證,但需要根據各項驗證功能來決定要執行哪些驗證。舉例來說,列舉驗證可能很便宜,且不太可能導致任何可評估的效能損失。另一方面,如果需要透過核心進行往返,則 zircon 管道驗證的成本可能會相當高,因此我們會考慮如何移除這項功能。此外,我們會避免移除管道權限,因為這會再次需要透過核心進行來回傳輸。我們會在這個過程中做出這些判斷,並根據最終決定,選擇要執行哪些驗證,而這項決定則由實作端負責。
我們之所以做出這些選擇,是因為同一個驅動程式庫主機中的驅動程式共用相同的安全性邊界。這些驗證步驟只會用於改善復原能力。
要求轉接
驅動程式接收要求、進行一些必要修改,然後轉送至較低層級的驅動程式庫,這是常見的作業。我們的目標是能夠以低成本且符合人體工學的方式做到這點。此外,如果能夠將訊息轉寄至驅動程式庫擁有管道兩端的管道,可能會是項實用的活動,因為驅動程式庫可能需要管理並暫存資料。雖然可以將其推送至佇列,但使用驅動程式庫執行階段管道做為佇列,可能會是一種簡單的方式,可透過較少的程式碼達到相同的效果。
運輸層級取消
當驅動程式庫因新條件或新需求而必須取消部分未完成的工作時,目前在 banjo 和 zircon 管道中,都沒有任何統一的方式可以達成這項要求。在以管道為基礎的 FIDL 中,許多繫結都會讓您忽略最終會傳送的回覆。視用途而定,這可能就足夠了,因為它允許釋放與要求相關聯的狀態,而這可能是唯一的目標。不過,有時需要傳播取消作業,讓下游驅動程式庫得知取消作業。發生這種情況時,您可以選擇在通訊協定層級建立支援功能,或是直接關閉管道組合的用戶端端點。後者解決方案僅適用於需要取消所有未結交易的情況,且由於無法取得確認,因此不需與伺服器同步。
驅動程式庫執行階段管道可提供與 zircon 管道傳輸相同層級的支援。在傳輸層中建構交易層取消傳播的支援功能是個很誘人的想法,但由於 FIDL 和傳輸層之間的職責分工,這項工作相當具有挑戰性。傳輸程序無法辨識交易 ID,因為這是在管道原始碼上建構的 FIDL 概念。因此,如果偏離 Zircon 管道的運作方式,可能會讓需要處理這兩種傳輸方式的開發人員感到困惑,因此不建議這麼做。這也可能導致實作層級的難度提高,因為該層級會抽象化多種傳輸方式。
實作
這項設計的實作作業主要分為三個階段:
- 實作驅動程式代管程序執行階段 API
- 實作以驅動程式代管程序執行階段 API 為基礎建構的 FIDL 繫結
- 從 banjo 遷移用戶
驅動程式主機執行階段 API
執行階段 API 將在新的驅動程式代管程序中實作,用於執行為元件的驅動程式。我們並未授予所有驅動程式使用 API 的權限,而是在 colocate
欄位旁邊指定新的元件資訊清單欄位 (名為 driver_runtime
),以限制使用 API 的權限。這個屬性可讓驅動程式庫執行元件知道是否應將 API 提供給驅動程式庫。同一個 driver_host 中的所有驅動程式,此屬性值必須相同。
除了設計中先前所述的原始元素外,還需要新增支援功能,以建立機制,讓新的驅動程式庫傳輸管道可以在繫結的驅動程式之間傳輸。此外,您還需要實作描述繫結規則和節點屬性的慣用法,讓驅動程式能夠繫結至實作驅動程式庫傳輸 FIDL 服務的裝置。
我們會在隔離的 devmgr 中新增支援功能,以便產生在新執行元件中執行的驅動程式。這個環境將用於實作基本元素、編寫測試,以及執行效能基準測試。
在初始的執行階段 API 實作中,我們會著重於 MVP,以便確立最終 API,讓我們能開始著手處理 FIDL 繫結。準備就緒後,您可以同時進行 FIDL 繫結的工作,並且可改善執行階段 API 的實作方式。
FIDL 繫結
FIDL 繫結會在現有的 fidlgen_llcpp
程式碼庫中編寫。輸出驅動程式庫執行階段標頭會受到標記限制。實作策略會遵循與上述類似的策略,著重於從最少的變更開始,利用新的驅動程式庫執行階段讓繫結運作。完成後,我們會建立微基準測試,瞭解效能。接著,我們可以重複執行繫結,進行最佳化,例如移除我們認為不必要的驗證步驟。
線路類型定義最終會加入共用標頭,但在將現有 FIDL 標頭分割為專用標頭的工作完成之前,我們會直接重新發出類型。這會導致任何嘗試同時使用 zircon 管道和驅動程式庫執行階段傳輸的驅動程式發生問題,但我們希望在那個時候,我們已完成將線定義拆分為共用標頭的工作。
遷移
特別是遷移作業,儘管幾乎所有驅動程式都位於 fuchsia.git 存放區中,與其嘗試一次將整個世界從 banjo 遷移至新的執行階段,不如一次遷移單一驅動程式代管程序,這樣會簡單得多。這項策略包含下列步驟:
- 要將驅動程式庫移植至 Banjo 通訊協定,您必須複製並修改該驅動程式,以便建立以驅動程式庫傳輸工具為目標的版本。
- 正在移植的驅動程式將實作對 banjo 和驅動程式庫傳輸 FIDL 服務的支援。
- 系統會為驅動程式建立新的元件資訊清單、繫結程式和建構目標,表示驅動程式會以新執行階段為目標。
- 對於驅動程式庫所在的每個電路板,一旦驅動程式代管程序中的所有驅動程式都已移植,包含驅動程式庫的電路板 gni 檔案就會更新,以便使用新版驅動程式庫,而非針對 banjo 的舊版驅動程式。
- 您可以刪除用於 banjo 的驅動程式版本變化版本,以及驅動程式庫中使用或提供 banjo 通訊協定的任何程式碼。
- 一旦 Banjo 通訊協定不再受到任何驅動程式使用,就會遭到刪除。
如果驅動程式包含在多個驅動程式庫主機中,且並非所有驅動程式庫都已移植,則可能需要同時在主機中加入這兩個版本。繫結規則和元件執行元件欄位會確保在適當的驅動程式庫主機中載入正確的驅動程式庫版本。驅動程式也能輕鬆偵測是否已繫結至公開 banjo 服務或驅動程式庫傳輸 FIDL 服務的驅動程式庫。
由於單一團隊無法自行遷移所有 300 多個 Fuchsia 驅動程式,因此許多團隊都會參與遷移作業。您需要提供詳細的遷移文件,讓大部分的遷移作業都能在最少的協助下完成。此外,在團隊預計執行遷移作業前,我們需要確保提供足夠的提前通知,讓他們能做好準備。
我們也需要決定如何整理移植的驅動程式清單。舉例來說,我們可能會盡量減少產品版本中重複的驅動程式數量,同時盡可能平行處理最多的驅動程式。我們必須建立並積極管理相關程序,確保不會發生錯誤而導致回歸,並確保遷移作業能及時完成。
評估主機代管
進行遷移作業的團隊也必須重新評估驅動程式是否需要與其他驅動程式共存。驅動程式庫程式架構團隊也會提供另一個構件,協助驅動程式庫作者進行評估。團隊可能需要進行一些基準測試,以便引導這項決策程序,此外,建立支援機制,協助輕鬆執行這類基準測試,也可能會很有幫助。
成效
我們必須先對 RFC 中先前述明的許多設計要點進行基準測試和評估,才能認真投入實作。雖然我們已完成初步基準測試,以協助我們決定所需的驅動程式庫執行時間方向,但我們仍需持續嚴格執行,確保所有列出的最佳化調整都很實用。
我們會建立微型基準測試,確保傳輸層和 FIDL 繫結層的效能都優於 zircon 管道的效能。
此外,我們需要建立更多以 e2e 為導向的基準,以確保我們在優化任何核心指標時,不會犧牲整體層級:
- 高總處理量
- 每秒輸入/輸出作業數 (IOPS) 高
- 低延遲
- 低 CPU 使用率
我們可能會使用最重要的用途來判斷要將哪些驅動程式移植至新執行階段,以便開始基準測試結果。以下列舉幾個驅動程式庫堆疊範例:
- NVMe SSD
- 乙太網路網路介面卡
- 顯示觸控輸入
- USB 音訊
確保我們取得多種不同的驅動程式庫用途,有助於我們建立信心,確保我們不會過度針對任何單一用途進行最佳化。
這些基準測試會在驅動程式庫介面層級執行,而不是在涉及所有層級 (通常包含相關裝置的技術堆疊) 的較高層級介面執行。這是因為目前很難透過端對端基準測試,充分瞭解驅動程式庫效能,因為驅動程式通常不是效能瓶頸。我們正在努力解決已知的瓶頸問題,但我們必須確保駕駛員不會成為新的瓶頸。
初步基準測試結果
我們已進行廣泛的基準測試,以便判斷這項設計的方向。我們採用現有的堆疊 (區塊和網路),並測試下列情境:
- 在所有 banjo 呼叫中插入管道寫入和讀取呼叫 (不會傳遞任何資料,也不會跳轉執行緒)。
- 延遲所有 banjo 呼叫,以便在共用調度器迴圈中以非同步工作方式執行 (不會插入執行緒跳躍)。
- 延後所有 banjo 呼叫,以便在每個驅動程式庫非同步調度器迴圈中,以非同步工作執行。
針對這些修改過的驅動程式庫堆疊執行的工作負載,會變更佇列長度、作業大小、工作負載總大小,以及讀取與寫入。
基準測試是在使用 x64 降低效能變化的主機板的 NUC 上執行。所有基準測試都降低了整體處理量、增加了尾端延遲時間,並提高了 CPU 使用率。
- 在大小為 1 和 2 的短佇列長度中,差異特別明顯。
- 每個驅動程式庫執行緒的整體結果較差。
- 將管道讀取和寫入作業插入 banjo 呼叫中,不會對吞吐量造成太大影響,但對尾端延遲的影響最大。
- 根據所有實驗的測試結果,CPU 使用率相對增加了 50% 至 150%。絕對 CPU 使用率一向不低 (10%-150%),因此相對增加的結果也會導致絕對 CPU 使用率大幅增加。
人體工學
如前文「需求」一節所述,人體工學對整體設計非常重要。我們希望避免引入其他概念,因為這些概念已是為 Fuchsia 編寫程式碼所需的概念。驅動程式作者已需瞭解 FIDL,因為以管道為基礎的 FIDL 是他們與非驅動程式庫元件互動的途徑。因此,如果減少驅動程式庫間通訊與非驅動程式庫元件互動之間的差異,就能減少新概念。Banjo 是完全不同的技術,雖然以整體而言,將其替換為更複雜的技術可能會降低人因工程效益,但由於它與管道型 FIDL 非常相近,因此整體人因工程效益應該會有所提升。我們將能夠共用類型,這也是開發人員長期以來的痛點,也讓我們對這項預期抱持信心。
此外,引入執行緒模型後,希望能大幅簡化正確驅動程式的編寫作業,再次改善整體人因設計。
人體工學最令人頭痛的地方之一,就是支援 C 驅動程式。由於 Fuchsia 平台不再有任何樹狀結構中的 C 驅動程式,因此我們無法充分瞭解這項提案對 C 驅動程式的影響。事實上,初始實作項目甚至不支援 C 驅動程式,因為 FIDL 目前缺乏適當的 C 繫結,甚至連 zircon 管道傳輸都缺乏。這項設計採用的許多設計選項都是以主要用於 C 的系統為基礎,因此只要所編寫的 C FIDL 繫結符合人體工學,C 驅動程式就不會受到影響。我們在相當長的一段時間內都沒有收到足夠的意見回饋,這確實代表了風險。
在遷移過程中進行使用者研究,有助於瞭解預期結果是否與實際情況相符。
回溯相容性
我們會在透過 Fuchsia SDK 匯出任何 banjo 介面之前,實施這些變更。因此,我們不會有任何限制,以確保回溯相容性。不過,由於我們需要執行逐部分遷移作業,因此可能需要確保在短時間內,同一款驅動程式庫同時支援 banjo 和驅動程式庫傳輸 FIDL,以便提供某種程度的回溯相容性。詳情請參閱遷移專區。
安全性考量
本 RFC 中提出的設計不應變更系統的安全性架構。目前位於相同程序中的驅動程式會繼續執行,因此安全性界線不應改變。不過,由於遷移至新版驅動程式庫執行階段的預期結果,客戶應會根據每個介面重新評估,是否有必要讓驅動程式與其父項在同一個程序中並存。我們會撰寫說明文件,為進行評估的開發人員提供指引。
隱私權注意事項
這項設計不會對隱私權造成影響。
測試
設計的不同部分會透過不同的機制進行測試。驅動程式執行階段會透過 unittest 進行測試,這些測試與相等的 zircon 原始碼的類似測試相去蕪除。此外,整合測試將使用隔離的 devmgr 和 CFv2 測試架構編寫。
驅動程式傳輸 FIDL 繫結會透過 GIDL 和整合測試進行測試,確保正確性。整合測試必須依據每個繫結項目編寫,而模擬已針對該繫結的 zircon 管道傳輸變數所建立的整合測試,將是最有可能採用的路徑。這些整合測試可能也需要使用隔離的 devmgr 和 CFv2 測試架構。
將驅動程式遷移至新的驅動程式庫執行階段時,可能需要針對每個驅動程式庫設計測試計畫。以 devmgr 為基礎的隔離測試應支援新傳輸,且無需特別處理。針對單元測試,您可能需要編寫測試程式庫,以便提供驅動程式庫執行階段 API 的實作項目,而無須在驅動程式代管程序中執行測試。您可以使用與目前用於在單元測試中模擬 libdriver API 相同的方法。我們會考慮在測試程式庫和 API 的驅動程式代管程序實作項目之間共用程式碼,以減少功能偏差。
說明文件
我們將需要新的指南,說明如何編寫會使用驅動程式庫程式傳輸 FIDL 的驅動程式庫,以及如何建立可提供該驅動程式的驅動程式庫。這些內容必須依綁定項目編寫,但由於我們一開始只鎖定 llcpp,因此只需要編寫一組內容。理想情況下,我們可以調整現有的 llcpp 指南,減少製作指南所需的整體工作量。
驅動程式的參考資料部分也必須更新,以便納入執行階段 API 的相關資訊。您也需要瞭解執行緒模型的相關資訊,以及如何編寫執行緒安全的程式碼。
我們需要一份最佳做法文件,協助驅動程式庫作者決定是否要使用驅動程式庫傳輸或 zircon 管道傳輸版本的 FIDL。
最後,如遷移章節所述,您需要提供如何從 banjo 遷移至驅動程式庫傳輸 FIDL 的文件。
缺點、替代方案和未知事項
線性化
為了達到零複製,我們可以避免 FIDL 目前執行的線性化步驟。重要的是,這會違反現有結構體版面配置的 FIDL 合約。避免線性化也意味著我們不會將解碼格式轉換為編碼格式。
FIDL 以這種方式運作的原因之一,是因為它可讓解碼作業變得簡單許多。特別是,在單一階段中確保所有位元組都已計算,以及邊界檢查,以確保所參照的所有資料都位於訊息緩衝區內。由於「安全性和復原能力」一節中討論的安全性疑慮,我們不必擔心前述問題。針對後者,我們只需確保所有資料緩衝區都指向由競技場配置的記憶體。驅動程式庫執行階段會提供 API,讓我們執行這項檢查。
線性化的其中一個優點,就是能夠在緩衝區周圍 memcpy。由於訊息已由競技場支援,因此只要轉移競技場的擁有權,就能輕鬆轉移訊息的擁有權。無法複製訊息不一定是缺點,也不會產生任何明顯的影響。
線性化另一個好處是可簡化解碼作業。實際上,解碼訊息所需的額外複雜度,可能會超過不複製資料所帶來的優勢。這項測試需要謹慎評估,確保跳過線性化是明確的勝利策略。
最後,線性化可改善空間區域性,進而提升快取效能。如果競技場實作得宜 (做為 bump 配置器),空間區域性應仍良好,且在同一個堆疊框架內呼叫其他驅動程式所帶來的時間區域性改善,應可避免在此處發生的任何損失造成問題。
雖然這份 RFC 並未建議略過線性化,但我們會實作相關功能組合,以便評估這項功能是否值得實作,並將這項工作納入後續計畫。
外洩的控點
略過線性化後會遇到的問題之一,是當 FIDL 訊息的傳送端使用接收端不熟悉的聯合體或表格中的欄位時 (可能會使用較新版本的 FIDL 程式庫),我們可能會遺漏句柄。在 zircon 管道傳輸和建議的驅動程式庫程式執行階段傳輸中,接收器可以安全地忽略新欄位,並以其他方式瞭解訊息的其餘內容。不過,在 Zircon 管道變化版本中,帳號代碼會與資料分開儲存,也就是說,如果帳號代碼位於不明欄位,接收端仍會知道帳號代碼的存在,並可關閉未使用的帳號代碼。如果我們完全略過編碼步驟,即使瞭解完整的訊息結構,也無法得知所有句柄。
解決方法是部分編碼和解碼,而非線性化。具體來說,在編碼期間,系統會將句柄取出,並以偏移值取代,放入線性句柄表格中,然後在接收時,在解碼期間將句柄移回至物件。未使用的資料表項目可在解碼期間追蹤及關閉,類似於以 zircon 管道為基礎的方法。這種折衷方法應該能讓我們盡可能獲得略過線性化步驟的效能優勢,同時避免潛在陷阱。
電匯格式遷移
另一個問題是,在 FIDL 線路格式遷移期間,兩個同級驅動程式可能會使用不相容的版面配置物件。舉例來說,我們目前正在將 FIDL 信封的大小減半。在今天的 IPC 案例中,我們解決了這個問題,方法是在兩者之間加入轉換器,在兩種已編碼的線路格式之間進行轉換。雖然我們可以為非線性解碼格式編寫額外的轉換器,但這會增加維護負擔。
非 LLCPP 繫結
截至本文撰寫時,只有單一 FIDL 繫結實作 (LLCPP) 可以利用略過線性化步驟所提供的最佳化功能,以及處理可能從非線性化狀態解碼的功能,因為這是唯一可原生瞭解線路格式的繫結。對於其他繫結,在接收端,您可以先執行將緩衝區線性化的額外步驟,再繼續執行其他一般繫結專屬的解碼作業。此外,在傳送端,仍可執行線性化,但繫結項目必須小心避免將指標轉換為偏移。
透明傳輸
本文件所述的設計讓驅動程式庫作者可以自行決定,要使用哪個驅動程式庫程式傳輸版本的 FIDL 的 zircon 管道傳輸功能,以便處理特定通訊協定。這項設計的初始版本選擇將底層傳輸機制隱藏起來,以便改善整體驅動程式庫人因工程,減少需要學習的概念數量,並讓平台更能控制驅動程式庫是否要共置。經過多次討論後,我們決定暫時擱置這個想法,因為這會大大增加設計的複雜度,並引入許多需要解決的新問題。舉例來說,需要將訊息線性化的情況會成為動態決策,而非在編譯時就已知曉的情況。
如果我們決定嘗試讓底層傳輸作業對驅動程式庫透明,就需要撰寫後續 RFC。
Operations
如果我們在傳輸層引入概念,在堆疊中瀏覽各種驅動程式時追蹤邏輯作業,或許就能提供更完善的診斷資訊,並做出更明智的排程決策。如這項設計所述,與以 zircon 管道為基礎的 FIDL 類似,交易目前是一種通訊協定層概念。這項提案的早期版本考慮將配置器設為單一擁有權,讓我們可以將其用於追蹤驅動程式之間的作業,但以這種方式使用配置器可能會出現缺陷,因此目前的設計改為採用參照計數的配置器設計。我們日後會考慮在傳輸中重新採用第一類運算概念。
單一副本傳輸
如果我們判斷不必達成零複製,就會探索其他設計的多種做法。我們可以選擇移除競技場,改為要求預先註冊完成緩衝區或環,類似於 Windows IOCP、io_uring,甚至 NVMe 設計。要求完成後,我們會將結果寫入預先註冊緩衝區/環中,這可能會是線性化步驟的一部分。這種設計非常適合內建回壓。另一個做法是保留競技場,但一律執行線性化。如果本 RFC 的審查者認為有用,我們可以進一步評估這些方法的優缺點。
傳輸背壓
眾所皆知,Zircon 管道沒有任何回壓的預留空間。在撰寫本文時,系統設有全域管道訊息限制,如果達到這個限制,會導致擁有管道端的程序在接收端遭到終止。
由於我們正在設計新的管道基本元素,因此可以選擇在此處做得更好。考量到這項提案的範圍廣泛,我們認為應暫緩處理。不過,這可能是值得探索的領域,因為 FIDL 團隊正在積極調查這個問題。在驅動程式庫執行階段試用新方法,或許是瞭解該方法是否值得在 zircon 系統呼叫介面中實作的好方法,因為後者的風險較高。
雙向管道和事件
有人認為 zircon 管道的雙向性質是錯誤的。我們可以選擇不讓驅動程式庫程式執行階段管道雙向傳輸,而是需要兩個管道組 (4 個端點) 才能進行雙向通訊,類似 golang 內建管道。因此,我們不會支援透過驅動程式庫執行階段傳輸的 fidl 事件。與其讓驅動程式庫程式執行階段傳輸功能支援的 FIDL 功能出現偏差,不如盡可能讓功能組合相符。除了減少使用者混淆的情況,如果我們決定朝著這個方向前進,這也將有助於日後實現不透明傳輸。
遞迴鎖定
除了不支援自動排入工作 (這可能會導致重新進入驅動程式庫存取),我們也可能只允許驅動程式選擇啟用這項功能。為了正確處理這項問題,驅動程式必須執行下列任一操作:
- 將工作排入佇列,並排定稍後處理的時間。
- 使用遞迴式鎖定。
第一個選項似乎沒有比 RFC 中提出的設計更有優勢。第二個選項需要使用我們最初選擇的鎖定機制,以免在平台中實作支援功能。原因是遞迴式鎖定功能會使取得鎖定順序的正確性難以確保,甚至無法確保。當鎖定在多個不同順序中取得時,程式碼中可能會出現潛在的死結。與其冒著發生這個問題的風險,不如直接確保我們永遠不會重疊進入驅動程式庫。
Rust 驅動程式支援
我們不打算在這個提案中支援 Rust 驅動程式,但預計近期就會啟用這項功能。Fuchsia 的驅動程式庫作者非常希望能以 Rust 編寫驅動程式,而 Banjo 讓驅動程式庫程式架構團隊難以全面採用 Rust 支援。確保我們能輕鬆啟用以 Rust 編寫的驅動程式是設計考量,但不一定是動機因素。
預期可實現本 RFC 前述的驅動程式庫傳輸 Rust FIDL 繫結。目前我們尚未決定這類繫結的具體設計和實作方式,但這很可能會是日後 RFC 的議題。
工作優先順序
在未來的某個時間點,我們會探索如何在工作或訊息層級 (而非調度器層級) 繼承優先順序。雖然 Zircon 核心團隊有意在內部探索這個想法,但驅動程式庫執行階段具有極大的彈性,可在此進行實驗,並超越核心提供的創新功能。一旦完成初始的執行階段實作,並完成低難度最佳化作業,這項作業很可能會成為重點領域。
電源管理管道
在其他驅動程式庫架構中,當較低層級的驅動程式處於暫停狀態時,自動停止服務工作是一項實用的作業。本 RFC 中提出的設計並未提供任何可用性,但為了改善驅動程式庫作者的人體工學,我們仍會探索這項功能。目前有許多未解決的問題,與電源管理相關的驅動程式庫架構應如何參與。
既有技術與參考資料
無論是處理程序內執行緒模型的概念,還是如何有效且符合人體工學地處理並行作業的想法,都不是新概念。這裡採用的方法受到以下來源的大量啟發 (+ 更多):
- Windows Driver Framework 佇列
- Window Driver Framework Sync Scopes
- 通用物件模型
- Apple Grand Central Dispatch (GCD)
- Rust Streams
- 遞迴鎖定