RFC-0085:減少 zx_status_t 空格

RFC-0085:減少 zx_status_t 空間
狀態已接受
區域
  • Kernel
  • 系統
說明

縮減有效的 zx_status_t 值範圍,方便將類型嵌入其他類型。

問題
Gerrit 變更
作者
審查人員
提交日期 (年-月-日)2020-10-09
審查日期 (年-月-日)2021-04-08

摘要

本文建議將有效 zx_status_t 值範圍從所有 32 位元帶正負號的整數縮小為 [-2^30, 0],並淘汰應用程式定義的錯誤碼。這樣一來,zx_status_t 就能輕鬆嵌入為其他型別的子範圍。

提振精神

zx_status_t 是簡單的錯誤類型,用於傳達特定動作是否成功。定義為帶正負號的 32 位元整數。值 ZX_OK (0) 表示作業成功。所有其他值都表示某種形式的錯誤,系統定義的錯誤代碼 (負值) 和應用程式定義的錯誤代碼 (正值) 都支援。

zx_status_t 型別廣泛用於整個 Fuchsia。舉例來說,Zircon 核心會使用這項機制 (內部使用,以及做為系統呼叫的回傳值);許多 FIDL 通訊協定會使用這項機制來指出錯誤;多個系統程式庫會使用這項機制來回報錯誤;純應用程式碼內部也經常使用這項機制,做為在函式之間回報錯誤的便利方法。

雖然目前的 zx_status_t 定義可傳達各種不同的錯誤情況,但目前只能傳達單一成功值。隨著時間推移,Fuchsia 開發人員發現,在某些情況下,將其他非錯誤資訊傳回呼叫端會很有用:

  • 非錯誤警告:部分函式會發出警告,表示函式大致上成功執行,但可能存在問題。舉例來說,寫入緩衝區的要求成功,但緩衝區小於可用資料量。

  • 物件狀態的額外資訊:舉例來說,核心 IPC 基本型別可能會想在讀取完成後指出有更多資料待處理,或提供更精細的閾值資訊 (例如插座)。

  • 流程控制:核心和系統程式庫的常見模式是回呼傳回錯誤、ZX_ERR_STOP 值 (表示不應再呼叫回呼),或 ZX_ERR_NEXT 值 (表示應繼續呼叫回呼)。這些特殊值雖然本身並非錯誤,但目前在系統定義的錯誤空間中會指派代碼。

  • 傳送小型酬載:函式可能會希望在呼叫成功時,傳送少量資料做為結果的一部分。舉例來說,目前 zx_debuglog_read 系統呼叫的實作方式會使用 zx_status_t 的正值,在成功時傳回讀取到緩衝區的位元組數。

雖然理論上,所有這些用途都可以使用額外的輸出參數或更複雜的複合型別解決,但在某些對效能要求嚴格的用途中,如果函式可以有簡單的整數/暫存器回傳值,就能處理上述用途,效率會更高。

雖然本文使用「函式」和「回傳代碼」等詞彙,但這些概念同樣適用於 FIDL 呼叫、系統呼叫等。同樣地,雖然這個設計提供 C++ 範例,但相同概念也適用於其他語言,包括 C、Rust 和 Go。

設計

在這項提案中,zx_status_t 仍為帶正負號的 32 位元整數,且會繼續定義為具有單一成功代碼 ZX_OK (0)。其他所有值仍應視為錯誤。

不過,我們會更新 zx_status_t 的定義,以便:

  • 有效 zx_status_t 值的範圍為 [-2^30, 0] (即 -10737418240)。這個範圍內的所有值都會是系統定義的錯誤代碼,或是單一成功代碼 ZX_OK

  • 應用程式定義的錯誤碼 (目前定義為所有正數 zx_status_t 值) 已淘汰,詳情請參閱下方的「回溯相容性」。

限制 zx_status_t 可採用的值範圍後,使用者就能在其他型別中嵌入 zx_status_t 值範圍,不必擔心錯誤代碼值會與非錯誤傳回值重疊。舉例來說,函式可能會定義 result_or_count_t 型別,其中負值對應至 zx_status_t 錯誤代碼,非負值則對應至處理的元素數量。

我們要求函式實作者定義「新」型別,而不只是在 zx_status_t 空間的未使用部分發出值。這樣一來,函式使用者就能清楚瞭解函式傳回的內容,以及應如何解讀:如果函式具有 zx_status_t 傳回類型,使用者就能確保 ZX_OK 是唯一有效的成功值。

範例

以下各節將示範如何處理上述不同用途,並假設 zx_status_t 的範圍有限。

其他狀態資訊

目前,zx_channel_read 的使用者只能透過對管道執行失敗的讀取作業,判斷是否有訊息等待處理。使用這項提案,zx_channel_read 可以導入新的回傳型別,將「有更多訊息等待處理」狀態做為回傳值的一部分,避免額外的系統呼叫:

/// Keep reading messages until none remain on the channel.
do {
  // Read from the channel.
  zx_channel_read_result_t result =
      zx_channel_read(channel, buffer, /*options=*/ZX_GET_CHANNEL_STATE);

  // `zx_channel_read_result_t` defines negative values to correspond
  // to `zx_status_t` error codes.
  if (result < 0) {
    return static_cast<zx_status_t>(result);
  }

  // Otherwise, the result is defined to be a bitmap indicating
  // the state of the channel.
} while ((result & ZX_CHANNEL_MORE_MESSAGES_WAITING) != 0);

流量控制

與其依賴錯誤代碼 ZX_ERR_NEXTZX_ERR_STOP (並依賴說明文件告知呼叫端需要處理哪些代碼),不如導入新類型,使用非負空間來表示流量控制:

// Negative values are zx_status_t error codes, while non-negative
// values must be one of the constants below.
using zx_iteration_status_t = int32_t;
constexpr int32_t ZX_ITERATION_CONTINUE = 0;
constexpr int32_t ZX_ITERATION_DONE = 1;

// ...

while (true) {
  zx_iteration_status_t result = Next(thing);
  if (result < 0) {
    return result;  // error
  }
  if (result == ZX_ITERATION_DONE) {
    break;
  }
  // ...
}

將酬載混入回應

zx_debuglog_read 已使用非負空間傳回小型酬載 (讀取的位元組數),這目前違反了 zx_status_t 的嚴格定義。這項提案可讓 zx_debuglog_read 定義新類型,明確指出如何解讀回傳值:

// Read the debug log. Returns a negative value on error, otherwise
// the number of bytes read from the debug log.
zx_debuglog_read_result_t result = zx_debuglog_read(buffer);
if (result < 0) {
  return result;  // error
}
print_log(/*buffer=*/buffer, /*size=*/result);

應用程式定義的錯誤代碼

如果應用程式想定義自己的錯誤代碼,可以繼續這麼做,但應定義類型,清楚說明值的解讀方式:

enum ApplicationError {
  INVALID_AUTHORIZATION = 1,
  TOO_MANY_OUTSTANDING_REQUESTS = 2,
  // ...
}

// Zero indicates success. Negative values map to `zx_status_t` error
// codes. Positive values map to `ApplicationError` error codes.
using app_status_t = int32_t;

由於 zx_status_t 只會佔用 [-2^30, 0] 範圍,因此應用程式可以視需要使用 [-2^31, -2^30) 範圍做為應用程式定義的錯誤代碼,並視需要將正數空間空下來做為其他回傳代碼。

回溯相容性

這項提案會將 zx_status_t 的有效範圍更新為 [-2^30, 0] 值,這些值都是系統定義的。不過,目前有一小部分應用程式會使用正空間,儲存應用程式專屬的程式碼。

根據這項 RFC 的實作內容,我們會將樹狀結構內的正向狀態碼使用者遷移至新的 (非 zx_status_t) 型別。

實作

如要實作這項 RFC,請按照下列步驟操作:

  • 更新說明文件 (包括 Markdown 文件和來源註解),說明 zx_status_t 的語意,以符合本規格的提案。

  • 更新 zx_debuglog_read 系統呼叫,以使用自訂 (非 zx_status_t) 型別。

  • 請更新目前使用正向 zx_status_t 狀態範圍的使用者,改用可更清楚說明產生哪些其他錯誤的新型別。

效能

首先是這項編碼機制本身的效能機制。在大多數情況下,相較於其他資訊編碼方式,效能變化應該不大且微幅。對於具有昂貴或稀少參數的系統呼叫,效能可能會略有提升。

第二項是系統 API 的效能變化,這些 API 會改用「成功但有更多資訊」的架構。這項變更的部分動機是向使用者空間傳達更多資訊,以便做出更精密的決策,進而提升效能。

整體而言,我們預期系統呼叫能力傳達額外資訊所帶來的成效提升,將會超過評估狀態值時稍微複雜的程式碼。

除了效能之外,這項變更和日後使用這項能力進行的變更,可能會改變許多二進位檔中的程式碼大小,尤其是產生的 FIDL 繫結。

安全性考量

zx_status_t 範圍嵌入其他型別可能會造成混淆,因此有引入軟體錯誤的風險。執行這類嵌入作業的函式或通訊協定應仔細評估,確認好處是否大於造成混淆的風險。

將函式從規格外的 zx_status_t 用途和應用程式專屬錯誤代碼遷移至更明確的型別,應可減少混淆情況。

隱私權注意事項

這項提案不會以有意義的方式與使用者資料互動,因此應該不會影響隱私權。

測試

我們會為上述幾個新函式開發單元測試。

說明文件

樹狀結構 Markdown 說明文件和程式碼內註解會更新,以反映 zx_status_t 的新定義。

缺點、替代方案和未知事項

缺點:來自不受信任來源和 FIDL 繫結的 zx_status_t

目前我們無法防止超出範圍的 zx_status_t 值透過管道傳輸。如果應用程式從不受信任的來源收到 zx_status_t 值,且這些值必須在範圍內,則需要手動驗證。從長遠來看,或許可以更新 FIDL 繫結產生器,檢查並拒絕超出範圍的 zx_status_t 值,但這項工作與本 RFC 無關。

替代方案:核心輸出參數

核心的另一個設計空間是使用額外的 out 參數。這種做法有幾個缺點:

變更這些系統呼叫的類型是侵入性較高的變更,需要較長的遷移時間。

這些系統呼叫都必須有含 out 參數的變數,或者呼叫端不需額外資訊,因此傳遞 null。兩者在人體工學方面都較差。

使用 out 引數傳達少量資訊,會浪費稀少且昂貴的資源,因為這會耗用少數幾個暫存器之一 (特別是在 x86_64 上),或是透過指標進行相當昂貴的 user_copy

替代做法:將 zx_status_t 的非負值留空,供自由使用

這項提案建議限制有效 zx_status_t 值的範圍,以利將範圍嵌入其他型別,但不允許應用程式直接使用未使用的範圍。

先前的提案是分割 zx_status_t 空間,將負值保留給錯誤,並允許函式視需要使用非負值。允許將 zx_status_t 型別重複用於函式專屬用途,可讓開發人員輕鬆開始從函式傳回額外酬載 (無須建立額外型別),且方便核心系統呼叫開始傳回額外資料給特定呼叫端,同時不會破壞現有呼叫端的 ABI。

但缺點是光看函式型別,就無法清楚瞭解函式可能傳回的值範圍。此外,也支援下列常見的成語:

zx_status status = CallFunction();
if (status != ZX_OK) {
  return status;
}

如果 CallFunction 使用回傳代碼的正向空間,就無法再保證正確性。

zx_status_t 可能有多種不同的解讀方式,因此我們拒絕採用這個替代方案,以免造成混淆 (進而導致錯誤)。

替代方法:將代碼劃分為錯誤代碼和成功代碼

先前的提案建議將 zx_status_t 分割為錯誤代碼和成功代碼範圍,並將每個範圍進一步分割為系統定義和應用程式定義的範圍。

這種做法有幾個缺點:

  • 系統定義的成功代碼實用性不明: 錯誤代碼經常會向上傳播至呼叫堆疊,但成功代碼通常會立即處理或直接捨棄。 如果語意是全球通用的,就不太需要一組成功代碼。

  • 如果只允許正值為成功代碼,會妨礙值的其他更有效率用途,例如傳回物件目前狀態的位元欄位,或是傳回位元組數等小型酬載。

  • 如要將正空間用於成功代碼,必須遷移目前使用該空間的現有代碼,這些代碼目前用於應用程式專屬錯誤代碼。

允許每個函式使用整個非負空間,可減少遷移負擔,並為開發人員提供更多彈性。

既有技術和參考資料

  • Linux 核心會在內部使用負值範圍表示錯誤,並將正值範圍保留給函式專用。大多數系統呼叫會在系統呼叫邊界,將這個單一值分割為回傳代碼和執行緒本機 errno 變數。

  • UEFI 規格會將狀態空間劃分為負值 (錯誤)、零 (成功) 和正值 (警告)。錯誤和警告範圍都會使用第二個有效位元,進一步劃分為「EFI 保留」範圍和 OEM 範圍。

版本記錄

  • 2021-03-12:移除提案中建議的新錯誤代碼部分,該代碼表示收到超出範圍的 zx_status_t 值。而是會稽核樹狀結構內程式碼,移除這類用法,並更新 FIDL 繫結產生器,避免超出範圍的值在程序界限間傳播。

  • 2021 年 3 月 9 日:修改提案,將有效 zx_status_t 值的範圍重新定義為 [-2^30, 0]。這個縮減的範圍可讓 zx_status_t 更容易嵌入為其他型別的子範圍。

  • 2021-02-10:修改提案,將 zx_status_t 中的負值拆分為應用程式錯誤和系統錯誤,但保留所有非負值,供應用程式使用,不需進一步解讀。

  • 2020 年 10 月 9 日:初步提案,將 zx_status_t 錯誤空間分割為四個分區:應用程式錯誤和系統錯誤為負值;系統成功代碼和應用程式成功代碼為正值。0 會維持 ZX_OK