RFC-0085:减少 zx_status_t 空间

RFC-0085:减少 zx_status_t 空间
状态已接受
领域
  • 内核
  • 系统
说明

缩小有效 zx_status_t 值的范围,使该类型能够更轻松地嵌入到其他类型中。

问题
  • 74211
  • 74212
Gerrit 更改
  • 436685
作者
审核人
提交日期(年-月-日)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 的当前定义允许传达各种不同的错误情况,但它目前只能传达一个成功值。随着时间的推移,Fucsia 开发者发现了一些使用场景,在这些场景中,将其他非错误信息传达给调用方会很有帮助:

  • 非错误警告:有些函数想发出警告,在这种情况下,函数大部分是成功的,但可能存在问题。例如,写入缓冲区的请求已成功,但缓冲区小于可用数据量。

  • 有关对象状态的其他信息:例如,内核 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 所需的步骤如下:

  • 更新描述 zx_status_t 语义的文档(Markdown 文档和源注释),使其与本规范中提议的内容相符。

  • 更新 zx_debuglog_read 系统调用以使用自定义(非 zx_status_t)类型。

  • 更新正 zx_status_t 状态范围的当前用户,以使用能够更好地描述正在产生的其他错误的新类型。

性能

首先是此编码方案的性能机制。在大多数情况下,与其他编码此信息的方案相比,性能变化应该很小且微乎其微。对于参数开销高或稀缺的系统调用,性能可能会略有提升。

其次,系统 API 的性能变化更改为使用“包含更多信息的成功”方案。此项变更的部分动机是向用户空间传达更多信息,以便它可以做出更复杂的决策,从而提高性能。

总体而言,我们预计,通过系统调用传递额外信息的功能所解锁的性能提升将比评估状态值略为复杂的代码要大。

除了性能之外,这项变更以及将来使用此功能的更改可能会更改许多二进制文件中的代码大小,尤其是生成的 FIDL 绑定中的代码大小。

安全注意事项

zx_status_t 范围嵌入到其他类型中可能会引起混淆,从而存在引入软件 bug 的风险。执行此类嵌入的函数或协议应仔细评估好处是否超过混淆风险。

将不符合规范的 zx_status_t 和特定于应用的错误代码的函数迁移到更明确的类型应该可以降低混淆的可能性。

隐私注意事项

此方案不会以有意义的方式与用户数据交互,并且不会对隐私产生任何影响。

测试

我们将针对上述几个新函数开发单元测试。

文档

在树 Markdown 文档和代码内的注释中将会更新,以反映 zx_status_t 的新定义。

缺点、替代方案和未知情况

缺点:zx_status_t 值来自不可信来源和 FIDL 绑定

目前,我们无法阻止通过信道传输超出范围的 zx_status_t 值。如果应用从不受信任的来源收到 zx_status_t 值且要求这些值位于有效范围内,则需要手动验证这些值。从长远来看,可能需要更新 FIDL 绑定生成器,以检查并拒绝超出范围的 zx_status_t 值,但这种做法独立于此 RFC 运行。

替代方案:内核输出参数

内核的另一个设计空间是使用额外的输出参数。这种方法存在一些缺点:

更改这些系统调用的类型是一种更具侵入性的更改,需要更长时间的迁移。

所有这些系统调用都需要具有包含 out 参数的变体,或者让对额外信息不感兴趣的调用方传入 null。两者都属于人体工程学降级。

使用输出参数传达一些信息位是一种效率低下且成本高昂的资源使用方式,因为它会占用少量寄存器中的一个(尤其是在 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 具有许多不同的可能解析导致的混淆风险(并进而引发 bug)导致我们拒绝使用这种替代方法。

替代方案:划分为错误代码和成功代码

之前的方案建议将 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 年 2 月 10 日:修改了以下提案:在应用错误和系统错误之间拆分 zx_status_t 中的负值,但让应用无需进一步解释即可保留所有非负值。

  • 2020 年 10 月 9 日:初始方案将 zx_status_t 错误空间拆分为四个分区:应用错误和系统错误为负值;系统成功代码和应用成功代码为正值。0 将保持为 ZX_OK