| 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 確實有初步投資,但很快就進入維護模式,與 FIDL 相比,多年來幾乎沒有進步,而 FIDL 在類似的時間內蓬勃發展。Banjo 已無法滿足目前的需求,因此必須重新構思驅動程式庫執行階段。
這項設計是為瞭解決多個不同的問題,並希望一次解決這些問題。
穩定 ABI
驅動程式庫程式架構最重要的工作,就是啟用穩定的驅動程式庫 SDK。這是平台層級的目標,有助於 Fuchsia 支援各種硬體,並確保我們能達成目標,在更新 OS 各個部分時不會失去功能。目前的 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 所能達成的效能。如果我們無法在上述所有指標中,超越以鋯石管道為基礎的 FIDL,可能就無法取代目前的 Banjo 執行階段。我們會在後續章節中,討論為何需要優於以 Zircon 管道為基礎的 FIDL。
驅動程式作者人體工學
新驅動程式庫執行階段的另一個高層級目標,是確保驅動程式的編寫作業保持簡單。如果只追求最高效能,而忽略設計決策對驅動程式庫作者的影響,就無法獲得實用結果。提供選項讓作者選擇人體工學或零成本負擔,對於實現我們的目標至關重要。此外,能夠從簡單的實作方式演進為效能更高的實作方式,而不必重新編寫驅動程式庫,也是理想做法。
安全性與彈性
位於相同程序中的驅動程式會共用安全範圍。這是因為儘管我們盡力隔離驅動程式,但實際上無法拒絕驅動程式存取共用程序中屬於其他驅動程式的記憶體區域。這是刻意為之,因為共用位址空間是共置的主要優點之一,我們可以善用這點,進一步提升管道可能提供的效能。因此,我們必須假設任何具備合理裝備的惡意實體,只要取得驅動程式庫程式主機中某個驅動程式庫的控制權,就能存取該程序中所有驅動程式的所有功能。因此,我們不必考慮在驅動程式庫執行階段層中實作安全檢查,以提供額外的安全優勢。
不過,這並不代表我們不該繼續採用許多通常用於提供安全保障的機制,因為這些機制可能會提升個別驅動程式的韌性。舉例來說,我們仍可驗證指標是否指向緩衝區內的資料,而非盲目假設。這有助於減少錯誤造成的損害,並找出問題的根本原因。換句話說,防範錯誤仍有其價值。其餘設計應以這點為考量。
零複製選項
即使在最佳條件下,以管道為基礎的 FIDL 仍會產生至少 2 個副本。從使用者空間複製到核心 (透過 zx_channel_write),然後從核心複製到使用者空間 (透過 zx_channel_read)。通常,由於用戶端中的線性化步驟,以及選擇在伺服器中非同步處理要求,或將資料移至慣用語言的內建型別,因此會發生額外的副本,總共會產生 4 個副本。由於使用新版驅動程式庫執行階段的驅動程式會存在於相同程序 (因此也是相同位址空間),因此最佳情況可能不需要複製任何內容,而符合人體工學的情況則只需要複製 1 次。
與通道式 FIDL 類似,驅動程式之間傳遞的大多數資料都應是控制資料結構,資料平面資料則儲存在 VMO 中。這有助於我們減少最昂貴的副本。不過,控制資料結構的大小可能會加總,在特定情況下 (例如網路封包批次要求) 達到數 KB。此外,在多個驅動程式庫堆疊中,資料大多未經處理,而是通過多個層級。我們可以省略所有這些副本。事實上,在目前的 Banjo 基礎執行階段中,我們盡量達成零複製,但達成零複製的方式有許多問題,而且容易發生錯誤。這是我們無法採用 Rust 驅動程式庫開發的眾多原因之一。
值得一提的是,由於系統呼叫負擔占據主導地位,因此 FIDL 中的副本通常不會被視為效能瓶頸。也就是說,在驅動程式庫執行階段,我們會避免系統呼叫,因此預期副本會佔用相當多的時間,用於在驅動程式之間傳送訊息。
就效能而言,這些副本可能不會在系統中造成重大瓶頸。不過,這類作業可能會對 CPU 使用率造成可衡量的影響,因此提供達到零複製的能力相當實用。
推送與提取
Zircon 管道採用提取機制,使用者空間會在管道上註冊訊號,以便瞭解何時有資料可供讀取,並在收到通知後,從管道讀取資料到提供的緩衝區。FIDL 繫結通常會反轉這項機制,改為以推送為基礎。註冊回呼,並在準備好時觸發回呼。
我們有很大的彈性,可以改用完全以推送為基礎的機制,或是繼續模擬 Zircon 管道機制。對一般使用者而言,推送方式更符合人體工學,而且可能效能更高,因為提取模型需要在收到信號後重新進入執行階段。在其他選擇提高效能的系統中,例如 Windows IOCP 和 Linux io_uring,緩衝區通常會預先註冊,藉此移除核心中的額外項目和副本,進而提升效能。如果是程序內執行階段,進入執行階段的成本很低,而且實際上不需要預先註冊緩衝區即可達成零複製,因此在我們的 API 中引入與 Zircon 管道的差異不一定有益。此外,Rust 語言非常依賴提取機制,如果我們選擇在傳輸層級捨棄提取機制,很可能會與其模型發生阻抗不匹配的情況。
基本
如先前所述,新設計需要新的基本體,才能建構新的 FIDL 傳輸方式。這些基本型別本身大多是根據 zircon 核心提供的基本型別建構而成。
原始型的 API/ABI 將以 C 為基礎,類似現有的 libdriver API。我們會提供各語言的包裝函式,方便您以更慣用的方式使用。雖然可以透過 FIDL 定義 API,類似於在 FIDL 中定義 Zircon 系統呼叫介面的方式,但整體 API 集應會維持在相當小的規模,因此可能不值得投入這項工作。
競技場
第一個基本型別是競技場。為減少副本數量,我們需要確保與要求相關聯的資料生命週期至少與要求未完成的時間一樣長。提供給傳輸的資料必須以緩衝區為後盾。在以管道為基礎的 FIDL 繫結中,傳入訊息資料通常會從堆疊開始,如果要求需要非同步回覆,則可能會視需要移至堆積分配。FIDL 繫結可能會直接讀取堆積記憶體,但撰寫本文時,這種情況並未發生。如果我們改為透過執行階段提供的競技場備份資料,就可以將要求生命週期繫結至競技場。我們也可以針對競技場啟用額外分配,並保證這些分配會與要求一樣長。處理要求時,通常會將要求轉送給其他驅動程式庫,或向下游驅動程式庫提出要求;在這些情況下,可以重新利用同一個競技場,並連同新要求一併傳遞。透過這類配置,作業或許可以導覽許多驅動程式,完全不需要進行全域配置。在驅動程式庫堆疊 (例如區塊堆疊) 中,通常會在同一個驅動程式庫主機中導覽 6 個以上的驅動程式,主要是將相同要求重複轉送至較低層級的驅動程式,並對資料進行最少的修改。
執行階段會提供 API,用於建立競技場、遞減對競技場的參照、執行分配作業,以及檢查所提供的記憶體區域是否由競技場支援。最後一個 API 較為特殊,但對於 FIDL 傳輸實作的穩定性而言不可或缺。日後可以新增釋放個別分配項目的 API,但初始實作會略過此步驟。只有在競技場本身遭到破壞時,才會釋放配置, 也就是移除對競技場的所有參照時。
執行階段可視需要自由調整競技場。舉例來說,基礎實作可能會選擇將所有配置要求轉送至全域 malloc,並等待競技場遭到毀損,然後再針對每個產生的配置發出釋放要求。或者,也可以實作為單一配置緩衝區,並根據每次要求配置的最大空間調整大小。隨著時間推移,我們可能會擴充競技場介面,讓用戶提供提示,說明基礎競技場應採用的策略,因為相同的競技場策略可能不適用於所有驅動程式庫堆疊。
版本
第二個基本項目是管道基本項目。這個介面的外觀與紫晶石管道幾乎完全相同。兩者最大的差異包括:
- 寫入 API 會接收競技場物件,並遞增其參照計數。
- 寫入 API 會要求傳遞至其中的所有緩衝區 (資料 + 控制代碼表) 都由競技場支援。因此,執行階段會取得這些緩衝區 (以及所有由競技場支援的緩衝區) 的擁有權。
- 讀取 API 會提供競技場,呼叫端會取得該參照的擁有權。
- 讀取 API 會提供資料緩衝區和控制代碼資料表,而不是要求呼叫端提供要複製到其中的緩衝區。
這些差異是必要的,因為建構在頂端的 FIDL 繫結必須達成零複製。
鋯石管道的相似之處包括:
- 系統會成對建立管道。
- 管道不得重複,因此是雙向單一生產者單一消費者佇列。
調度工具
最後一個基本型別類似於鋯石通訊埠物件。這個機制不會封鎖目前的執行緒,而是透過回呼機制指出目前可讀取或已關閉的已註冊管道。此外,您也可以使用機制註冊驅動程式庫執行階段通道。
司機可以建立多個調度員,就像他們可以建立多個港口一樣。分派器是主要代理程式,可實作後續章節提及的執行緒模型。建立調度器時,必須指定調度器應使用的執行緒模式。
此外,還會有一個 API 可從驅動程式庫執行階段調度器物件接收 async_dispatcher_t*,以便等待 Zircon 物件及發布工作。針對這個調度器登錄的回呼,會遵守與執行階段通道相同的執行緒保證。
範例
雖然我們避免指定基本體的確切介面,但在評估設計時,查看可能的外觀範例或許很有幫助:
https://fuchsia-review.googlesource.com/c/fuchsia/+/549562
執行緒模型
除了共用相同的位址空間,共置驅動程式的第二個主要優點是,與核心提供的排程決策相比,共置驅動程式可以做出更完善的排程決策。我們希望驅動程式能共用相同執行緒,而不是為每個驅動程式庫提供專屬執行緒。如果多個獨立程式碼片段共用同一個執行緒,就必須遵守某種合約,確保正確性。
執行緒模型會設定規則和期望,驅動程式庫作者可運用這些規則和期望,確保正確性。如果使用得當,執行緒模型可大幅簡化開發體驗。如果驅動程式開發人員不想處理並行、同步和重入問題,應採用相關機制,讓他們忽略這些問題,同時仍能編寫實用程式碼。另一方面,執行緒模型必須提供足夠的自由度,讓驅動程式達到本文件稍早所述的效能目標。
為此,我們建議採用執行緒模型,為驅動程式提供下列兩種高階運作模式:
- 並行且同步。
- 並行且未同步。
建議您先定義所用字詞:
- 並行:多個作業可能會彼此交錯。
- 已同步:在任何時間點,不會有兩個硬體執行緒同時位於驅動程式庫內。所有回呼都會彼此尊重順序。
- 未同步:在任何時間點,多個硬體執行緒可能同時位於驅動程式庫內。我們無法保證來電順序。
同步處理並不代表工作會釘選至單一 Zircon 執行緒。只要能確保同步處理,執行階段就能將驅動程式庫遷移至管理的任何執行緒,不必支付任何費用。因此,在同步模式下使用執行緒局部儲存 (TLS) 並不安全。取而代之的是,驅動程式庫執行階段可能會提供替代 API。將同步模式視為「虛擬」執行緒或纖維,或許有助於瞭解相關概念。
非同步模式可讓驅動程式享有最大的彈性。但代價是需要額外工作,因為驅動程式庫可能會採用更精細的鎖定機制,進而提升效能。選擇此模式的驅動程式有較高的程序死鎖風險,因此我們日後可能會對選擇此模式的驅動程式設下限制。
這些模式會在執行階段定義,並與先前所述的驅動程式庫執行階段調度器物件建立關聯。由於可以有多個調度器,因此您也可以在單一驅動程式庫中混合搭配這些模式,以管理並行需求。舉例來說,您可以建立多個序列同步調度器,處理兩個不需要彼此同步的硬體。在實務上,驅動程式應會使用單一調度器,並選擇使用特定語言的建構函式,管理額外的並行和同步處理需求。這是因為 C++ 中的 fpromise::promise 或 Rust 中的 std::future 等語言專屬建構函式,可讓排程決策比驅動程式庫程式執行階段更完善,因為系統會更精細地追蹤依附元件。以 C 語言編寫的驅動程式最有可能使用驅動程式庫程式執行階段基本類型來管理並行,因為 C 語言缺少任何類型的標準並行管理基本類型或程式庫。
對驅動程式庫之間通訊的影響
由於上述原始型別未提供任何機制,讓驅動程式庫可呼叫其他驅動程式庫,因此所有呼叫都必須由執行階段調解。當驅動程式庫執行階段管道寫入時,我們可以在同一個堆疊框架中選擇呼叫另一個驅動程式庫,而不是自動將工作排入佇列。執行階段可以檢查通道另一端註冊的調度器所設的執行緒模式,判斷是否要呼叫擁有管道另一端的驅動程式庫。如果未同步,則一律會呼叫相同堆疊框架中的其他驅動程式庫。如果分派器已同步處理且並行,執行階段可以嘗試取得分派器的鎖定,如果成功,則在同一個堆疊框架中呼叫驅動程式庫。如果失敗,可以將工作排入佇列,稍後再處理。
這項最佳化看似微不足道,但初步基準化結果顯示,避免返回非同步迴圈可大幅改善延遲和 CPU 使用率。
可重入性
在上述所有模式中,駕駛人都不必處理重入問題。 執行階段會調解驅動程式之間的所有互動,因此我們能夠判斷目前呼叫堆疊中,是否已輸入要輸入的驅動程式庫。如果有的話,執行階段會將工作排入佇列,而不是直接呼叫驅動程式庫,並在返回原始執行階段迴圈後,由另一個 Zircon 執行緒或相同執行緒處理。
封鎖
在先前討論的所有模式中,都有一項隱含需求,那就是參與共用執行緒的驅動程式不得封鎖。您可能基於正當理由而想封鎖討論串。為了允許這些用途,建立調度器時,除了設定調度器將使用的執行緒模式外,驅動程式庫也可以指定是否需要能夠在呼叫時封鎖。這樣一來,執行階段就能判斷必須建立的執行緒數量下限,避免發生死結風險。舉例來說,如果驅動程式庫建立 N 個可能遭到封鎖的調度器,執行階段至少需要分配 N+1 個執行緒,才能為各種調度器提供服務。
一開始系統不會支援在也未同步處理的分派器中指定區塊,但如果出現有效的用途,我們可能會重新評估這項決定。原因在於上述簡單的 N+1 執行緒策略無法運作,因為可能有未知數量的執行緒已進入該驅動程式庫,且可能全都會遭到封鎖。
工作優先順序 / 執行緒設定檔
有些工作比其他工作更重要。雖然您可以在工作層級指派優先順序並沿用優先順序,但這並非易事。我們不會嘗試在 zircon 核心目前提供的功能之外,在這個空間進行創新,而是會將優先順序做為調度器層級的概念。建立調度器時,可以指定設定檔。雖然執行階段不一定會保證所有回呼都會在設定為使用該設定檔執行的 Zircon 執行緒上發生,但至少會為提供的每個 Zircon 執行緒設定檔產生一個 Zircon 執行緒。
就執行階段如何以最佳方式處理執行緒設定檔而言,這裡有很大的探索空間,而且可能需要與 Zircon 排程器團隊和驅動程式庫程式作者合作,才能找出符合執行緒設定檔用途需求的解決方案。
FIDL 繫結
目標為驅動程式庫程式執行階段傳輸的繫結,並非完全重新構思 FIDL,而是盡可能減少面向使用者的變更,以實現驅動程式庫執行階段的優勢。預期目前以 Zircon 管道為目標的每個語言繫結 (也支援編寫驅動程式),都會分叉以提供變體,同時以驅動程式庫執行階段傳輸為目標。在初始實作中,LLCPP 繫結會做為 C++ 使用,因為 C++ 是撰寫驅動程式的主要語言,而且在一段時間內,可能仍是支援度最高的語言。本節其餘部分會假設 LLCPP 繫結,並針對 FIDL 繫結做出具體註解。
非通訊協定類型應完整使用,不得修改。這點很重要,因為作者希望能夠在多個傳輸之間共用產生的型別。
系統會重新產生通訊協定 (又稱訊息層) 的類別和結構體。我們可能會將這些 API 設為範本,以便有條件地運用驅動程式庫執行階段 API,但這樣就必須為每個通訊協定產生管道和驅動程式庫執行階段傳輸的支援功能。另一種建議的做法是建立最小類別,為基礎傳輸作業抽象化管道讀取和寫入 API。很遺憾,這不適用於我們的設計,因為競技場在以管道為基礎的 FIDL 中會是尷尬的概念,且讀取和寫入 API 的緩衝區擁有權關係在這兩種傳輸方式之間不一致。您需要與維護各種繫結的團隊合作,才能判斷適當的程式碼重複使用和差異程度,以及繫結。
單次要求競技場
如先前章節所述,驅動程式庫執行階段會提供競技場物件,並與透過管道傳送的訊息一併傳遞。FIDL 繫結預計會利用這個競技場,實現零複製。
使用者可以選擇不使用競技場,建立傳送給其他驅動程式的結構體。沒問題,繫結可確保類型會複製到由執行階段提供的競技場分配器支援的緩衝區。不指定競技場可改善人體工學,但一定會產生副本。我們在這裡唯一可以選擇的設計,就是清楚標示正在複製內容,避免使用者在不知情的情況下複製內容。
使用者可能會偏好在堆疊上分配所有物件,希望這些物件完全在與呼叫本身相同的堆疊框架內使用,這在某些情況下可能可行,具體取決於執行緒限制。事實上,司機目前就是使用班卓琴來完成這項工作。雖然我們可能會支援這項功能,方法是在我們認為有必要將型別移至堆積 (透過類似接收器中的 ToAsync() 呼叫) 時,將型別延遲移至由競技場支援,但我們會避免支援這項功能。而是應使用相同的邏輯,將訊息複製到如上所述的競技場支援緩衝區。如果之後出現強大的使用情境,我們可能會重新考慮這個想法。
訊息驗證
目前,當駕駛人彼此通訊時,我們不會採取任何行動,驗證訊息是否符合介面指定的必要合約 (例如執行列舉驗證或鋯石控制代碼驗證)。在新執行階段中,我們可能會開始執行部分驗證,但需要根據每個驗證功能,決定要執行的驗證。舉例來說,列舉驗證的成本可能不高,且不太可能導致任何可測量的效能損失。另一方面,如果我們需要透過核心進行往返,鋯石管道驗證的成本可能相當高,因此我們會考慮如何移除。此外,我們也會避免剝奪頻道的權利,因為這同樣需要透過核心進行往返。我們將根據最終決策的基準來做出這些判斷,至於選擇執行哪些驗證,則取決於實作方式。
我們之所以能做出這些選擇,是因為同一驅動程式庫主機中的驅動程式共用相同的安全邊界。這些驗證步驟僅用於提升我們的韌性。
要求轉送
驅動程式的常見作業是接收要求、進行少量修改,然後轉送至較低層級的驅動程式庫。以低廉的價格符合人體工學地完成這項工作,是我們的目標。此外,如果驅動程式庫擁有管道的兩端,將訊息轉送至該管道可能也是實用的活動,因為驅動程式庫可能需要管理並行作業,以及將資料儲存在某處。雖然這可能會將其推送到佇列,但使用驅動程式庫執行階段管道做為佇列,可能是以較少程式碼達到相同效果的簡單方法。
取消運輸層級
如果驅動程式庫因新狀況或新規定而必須取消部分未完成的工作,無論是 banjo 或 zircon 管道,目前都沒有統一的 FIDL 方式可達成此目的。在以管道為基礎的 FIDL 中,許多繫結會允許您忽略最終會傳送的回覆。視用途而定,這可能就足夠,因為這樣可以解除配置與要求相關聯的狀態,而這可能是唯一目標。不過,有時必須傳播取消作業,讓下游驅動程式庫知道取消作業。發生這種情況時,做法是在通訊協定層建立支援,或是直接關閉管道配對的用戶端。後者僅適用於需要取消所有待處理交易,且不需要與伺服器同步處理的情況,因為您無法取得確認。
驅動程式庫執行階段管道可提供與 Zircon 管道傳輸相同的支援層級。在傳輸層中建構交易層級的取消傳播支援功能是個誘人的想法,但由於 FIDL 和傳輸之間的職責劃分,這項作業相當具有挑戰性。傳輸機制不會知道交易 ID,因為這是以管道基本類型為基礎建構的 FIDL 概念。此外,如果偏離 Zircon 通道的運作方式,可能會導致需要同時處理這兩種傳輸方式的開發人員感到困惑,因此或許不值得這麼做。此外,實作可抽象化多種傳輸方式的層也會更加困難。
實作
這項設計的實作作業大致分為三個階段:
- 實作驅動程式代管程序執行階段 API
- 實作以驅動程式代管程序執行階段 API 為基礎建構的 FIDL 繫結
- 從 banjo 遷移用戶端
Driver Host Runtime API
執行階段 API 會在新驅動程式代管程序中實作,用於以元件形式執行的驅動程式。與其授予所有駕駛人使用 API 的權限,不如在新的元件資訊清單欄位 (名為 driver_runtime) 後方,指定 colocate 欄位,限制使用權限。這項屬性可讓驅動程式庫執行元件瞭解是否應將 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 多個紫紅色驅動程式,因此這項遷移作業需要許多團隊參與。您需要詳細的遷移文件,讓大多數遷移作業都能在最少協助下完成。此外,在預期團隊執行遷移作業前,我們需要確保提供足夠的提前通知,以便他們規劃。
我們也需要判斷有助於整理所匯入驅動程式清單的條件。舉例來說,我們可能希望盡量減少產品建構中的重複驅動程式數量,同時盡可能平行處理更多移植的驅動程式。我們必須建立並積極管理程序,確保不會在過程中發生錯誤而導致回歸,並確保遷移作業及時完成。
評估主機代管
執行遷移作業的團隊也必須重新評估,驅動程式是否需要與其他驅動程式共用程序。做為另一項構件,驅動程式庫程式架構團隊會提供文件,協助驅動程式庫作者進行這項評估。團隊可能需要進行一些基準測試,以利決策過程,而建構支援功能,協助輕鬆執行這類基準測試,也可能證明很有用。
效能
在我們認真投入之前,RFC 中稍早指定的許多設計要點,都必須先經過基準化和測量。我們已完成初步基準化作業,以確定驅動程式庫執行階段的發展方向,但仍需持續嚴格把關,確保所有最佳化作業都有用。
我們會建立微基準,確保傳輸層和 FIDL 繫結層的效能都優於 Zircon 管道對等項目。
此外,我們還需要建構更多以端對端為導向的基準,確保我們不會為了提升整體層級,而犧牲任何要進行最佳化的核心指標:
- 高總處理量
- 每秒輸入/輸出作業數 (IOPS) 偏高
- 低延遲
- CPU 使用率偏低
我們可能會利用最重要的用途來判斷要移植到新執行階段的驅動程式,以便開始進行基準化結果。以下列舉幾個驅動程式庫堆疊範例:
- NVMe SSD
- 乙太網路 NIC
- 顯示觸控輸入
- USB 音訊
確保我們獲得各種驅動程式庫使用情境,有助於我們確信不會過度針對任何單一使用情境進行最佳化。
這些基準測試會在驅動程式庫介面層執行,而不是在涉及所有層級的較高層級介面執行,這些層級通常會組成相關裝置的技術堆疊。這是因為目前很難使用端對端基準來充分瞭解驅動程式庫效能,因為驅動程式通常不是效能瓶頸。我們正在努力解決平台已知的瓶頸,但必須確保驅動程式不會成為新的瓶頸。
初步基準結果
我們進行了廣泛的基準測試,以決定這項設計的發展方向。我們採用現有堆疊 (區塊和網路),並測試下列情境:
- 在所有 Banjo 呼叫中插入管道寫入和讀取呼叫 (不傳遞任何資料,也不會跳轉執行緒)。
- 延遲所有 Banjo 呼叫,在共用調度器迴圈中以非同步工作形式執行 (未插入執行緒躍點)。
- 延後所有 Banjo 呼叫,以便在每個驅動程式庫非同步調度器迴圈中,以非同步工作形式執行。
工作負載會針對這些修改後的驅動程式庫堆疊執行,並使用不同的佇列長度、作業大小、工作負載總大小,以及讀取與寫入作業。
基準測試是在使用 x64-reduced-perf-variation 板的 NUC 上執行。可預期的是,所有基準都降低了整體輸送量、增加了尾端延遲,並提高了 CPU 使用率。
- 當佇列長度為 1 和 2 時,差異尤其顯著。
- 以每個驅動程式庫執行緒為準的結果整體而言較差。
- 將管道讀取和寫入作業插入 Banjo 呼叫後,輸送量並未受到顯著影響,但尾端延遲時間受到最大影響。
- 根據所有試驗的參數,CPU 使用率相對增加了 50% 至 150%。絕對 CPU 使用率一向不容小覷 (10% 至 150%),因此相對增加也會導致絕對 CPU 使用率大幅增加。
人體工學
如需求條件一節所述,人體工學對整體設計非常重要。我們希望避免介紹 Fuchsia 程式碼編寫以外的其他概念。驅動程式作者必須瞭解 FIDL,因為他們是透過以管道為基礎的 FIDL 與非驅動程式庫元件互動。因此,減少驅動程式庫間通訊與非驅動程式庫元件互動的差異,可減少新概念。Banjo 是完全不同的技術,雖然以整體而言更複雜的技術取代 Banjo,似乎會降低人體工學,但由於 Banjo 與以管道為基礎的 FIDL 非常接近,希望這能提升整體人體工學。我們將能分享型別,這長期以來一直是開發人員的痛點,因此我們對這項期望抱持信心。
此外,導入執行緒模型後,希望可以大幅簡化正確驅動程式的撰寫難度,進而提升整體人體工學。
人體工學最令人困擾的其中一個地方,就是支援 C 驅動程式。由於 Fuchsia 平台樹狀結構中已沒有任何 C 驅動程式,我們無法充分瞭解這項提案對 C 驅動程式的影響。事實上,由於 FIDL 目前甚至連 Zircon 管道傳輸的適當 C 繫結都沒有,因此初始實作作業甚至不會支援 C 驅動程式。這個設計的許多設計選擇,都是以主要用於 C 的系統為基礎,因此只要編寫的 C FIDL 繫結符合人體工學,C 驅動程式就不會受到影響。我們在一段時間內無法取得這方面的充分意見回饋,這確實是風險。
在遷移過程中進行使用者研究,有助於瞭解預期結果是否反映實際情況。
回溯相容性
我們會在透過 Fuchsia SDK 匯出任何 banjo 介面前,實施這些變更。因此,我們不會為了確保回溯相容性而設下任何限制。不過,由於我們需要執行分段遷移,因此可能需要確保某種程度的回溯相容性,也就是在短時間內,同一個驅動程式庫同時支援 Banjo 和驅動程式庫傳輸 FIDL。詳情請參閱遷移一節。
安全性考量
本 RFC 建議的設計不應改變系統的安全架構。目前存在於相同程序中的驅動程式會繼續執行,因此安全性界線不會變動。不過,遷移至新的驅動程式庫執行階段後,預期用戶會重新評估是否適合將驅動程式與父項共置於同一程序中。我們會撰寫說明文件,協助開發人員進行這項評估。
隱私權注意事項
這項設計預計不會對隱私權造成影響。
測試
設計的不同部分會透過不同機制進行測試。系統會根據類似的 Zircon 原始項目測試,透過單元測試測試驅動程式執行階段。此外,整合測試會利用獨立的 devmgr 和 CFv2 測試架構編寫。
驅動程式傳輸 FIDL 繫結會透過 GIDL 和整合測試進行測試,確保正確性。整合測試必須以每個繫結為基礎撰寫,且最有可能的路徑是模仿該繫結的 Zircon 管道傳輸變數現有的整合測試。這些整合測試可能也需要使用獨立的 devmgr 和 CFv2 測試架構。
遷移至新版驅動程式庫執行階段的驅動程式,可能需要根據每個驅動程式庫設計測試計畫。獨立的 devmgr 測試應支援新傳輸方式,不需要任何特殊設定。如要進行單元測試,可能需要編寫測試程式庫,以提供驅動程式庫執行階段 API 的實作項目,不必在驅動程式代管程序中執行測試。您可以使用與目前在單元測試中模擬 libdriver API 類似的方法。我們會考慮在測試程式庫和 API 的驅動程式代管程序實作項目之間共用程式碼,以減少功能差異。
說明文件
您需要新指南,說明如何編寫會使用驅動程式庫程式傳輸 FIDL 的驅動程式庫程式,以及如何建立提供該 FIDL 的驅動程式庫。這些函式需要以每個繫結為基礎編寫,但由於我們最初只以 llcpp 為目標,因此只需要編寫一組函式。理想情況下,我們可以改編現有的 llcpp 指南,減少製作指南所需的整體工作量。
此外,也需要更新驅動程式的參考章節,加入執行階段 API 的相關資訊。此外,您也必須瞭解執行緒模型,以及如何編寫執行緒安全程式碼。
我們需要一份最佳做法文件,協助驅動程式庫作者判斷要使用 FIDL 的驅動程式庫傳輸或 Zircon 管道傳輸版本。
最後,如遷移作業一節所述,您需要瞭解如何從 banjo 遷移至驅動程式庫傳輸 FIDL。
缺點、替代方案和未知事項
線性化
為實現零複製,我們可以避免 FIDL 目前執行的線性化步驟。這麼做會違反現有的 FIDL 合約,就結構版面配置而言,這點至關重要。避免線性化也表示我們不會執行從解碼形式到編碼形式的轉換。
FIDL 採用這種做法的原因之一,是為了簡化解碼程序。特別是確保所有位元組都已納入考量,這項作業在一次傳遞中微不足道,而且所有參照的資料都位於訊息緩衝區內,因此可進行界線檢查。由於安全與韌性一節中討論的安全疑慮,我們認為不必擔心前者。針對後者,我們只需要確保所有資料緩衝區都指向競技場分配的記憶體。驅動程式庫執行階段會提供 API,讓我們執行這項檢查。
線性化的一項優點是能夠 memcpy 緩衝區。由於訊息已由競技場備份,因此只要轉移競技場的擁有權,即可輕鬆轉移訊息的擁有權。無法複製訊息不一定會造成明顯的負面影響。
線性化的另一項好處是簡化解碼程序。事實上,解碼訊息所需的額外複雜度,可能超過不複製資料所帶來的優勢。您必須仔細測量,確保略過線性化是明顯的優勢。
最後,線性化可改善快取效能,提升空間局部性。如果競技場實作良好 (做為凸塊分配器),空間區域性應該還是不錯,而從相同堆疊框架內呼叫其他驅動程式所帶來的時間區域性改善,應該不太可能造成問題。
雖然這項 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 系統呼叫介面中實作,因為該介面的風險較高。
雙向管道和事件
有人認為鋯石通道的雙向性質是錯誤。我們可能會選擇不讓驅動程式庫執行階段通道雙向傳輸,而是需要兩組管道 (4 個端點) 才能進行雙向通訊,類似於 Go 語言內建的管道。因此,我們不會透過驅動程式庫執行階段傳輸支援 fidl 事件。驅動程式庫執行階段傳輸支援的 FIDL 功能不會發生漂移,而是盡可能與功能集相符。除了減少使用者困惑,如果我們決定朝這個方向發展,這項做法也能讓日後實現不透明的傳輸作業更加容易。
遞迴鎖
如果我們不支援自動將工作排入佇列,導致重新進入驅動程式庫,或許可以允許驅動程式選擇加入。為正確處理這項問題,驅動程式必須執行下列其中一項操作:
- 自行將工作排入佇列,並排定佇列的服務時間。
- 使用遞迴鎖。
相較於 RFC 中呈現的設計,第一個選項似乎沒有任何優勢。 第二個選項需要使用我們最初選擇避免在平台中實作支援的鎖定。因為遞迴鎖定會導致難以確保 (甚至無法確保) 取得鎖定的順序正確性。如果以多種不同順序取得鎖定,程式碼中就會潛藏潛在的死結。與其冒著發生這個問題的風險,不如確保我們絕不會重新進入驅動程式庫。
Rust 驅動程式支援
我們不打算在這項提案中支援 Rust 驅動程式,但預計在不久的將來會啟用這項功能。Fuchsia 內部的驅動程式庫作者非常希望以 Rust 撰寫驅動程式,但 Banjo 讓驅動程式庫程式架構團隊難以全面採用 Rust 支援。確保我們能輕鬆啟用以 Rust 編寫的驅動程式是設計考量,但不一定是促成因素。
預計可達成本 RFC 先前所述的驅動程式庫傳輸 Rust FIDL 繫結。目前這類繫結的具體設計和實作方式尚未確定,日後可能會成為 RFC 的主題。
工作優先順序
未來,我們很樂意探討如何繼承工作或訊息層級的優先順序,而非在調度器層級。雖然 zircon 核心團隊有初步計畫要探索這個想法,但驅動程式庫執行階段有很大的彈性,可在此進行實驗,並在核心提供的功能之外進行創新。完成執行階段的初步實作並進行簡單的效能最佳化後,這可能就會成為重要的關注領域。
Power Managed Channels
在其他驅動程式庫架構中,當較低層級的驅動程式處於暫停狀態時,自動停止提供工作服務是很有用的作業。本 RFC 提出的設計並未提供任何功能來允許這麼做,但為了改善驅動程式庫作者的人體工學,這是一個值得探索的領域。目前,在電源管理方面,驅動程式庫架構應參與的程度仍有許多開放式問題。
既有技術和參考資料
無論是程序內執行緒模型概念,還是如何有效率且符合人體工學地處理並行作業,都不是新概念。這裡採用的方法深受下列來源啟發 (+ 更多):