RFC-0085:减少 zx_status_t 空间

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

缩小了有效 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 错误代码,而非负值对应于已处理的元素数量。

我们要求函数实现者定义 new 类型,而不仅仅是在 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 的性能变化,这些 API 正在改为使用“成功但有更多信息”方案。此项变更的部分动机是向用户空间传递更多信息,以便用户空间做出更复杂的决策,从而实现更出色的性能。

总而言之,我们预计,通过系统调用来传递额外信息所带来的性能提升将超过评估状态值的代码略微复杂所带来的影响。

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

安全注意事项

zx_status_t 范围嵌入到其他类型中可能会造成混淆,从而导致引入软件 bug。执行此类嵌入的函数或协议应仔细评估,以确定其带来的好处是否大于混淆风险。

将以不符合规范的方式使用 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 可能有多种不同的解释,这可能会导致混淆(进而导致 bug),因此我们拒绝了此替代方案。

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

之前的提案建议将 zx_status_t 分区为错误代码范围和成功代码范围,其中每个范围进一步拆分为系统定义范围和应用定义范围。

这种拆分方式存在一些缺点:

  • 系统定义的成功代码的实用性尚不明确:虽然错误代码经常会向上层调用堆栈传播,但成功代码往往会立即处理或直接舍弃。因此,对具有全局理解的语义的一组成功代码的需求较少。

  • 将正值限制为仅表示成功代码会妨碍该值的其他更高效的用途,例如返回有关对象当前状态的位字段,或返回少量载荷(例如字节数)。

  • 将正空间重新用于成功代码需要迁移当前使用该空间来表示应用专用错误代码的现有代码。

允许每个单独的函数使用整个非负空间可减轻迁移负担,并为开发者提供更大的灵活性。

在先技术和参考资料

  • Linux 内核在内部使用负范围表示错误,而将正范围用于特定于函数的用途。大多数系统调用会在系统调用边界将此单个值拆分为返回代码和线程本地 errno 变量。

  • UEFI 规范将其状态空间划分为负值(错误)、零(成功)和正值(警告)。错误范围和警告范围都使用次高有效位进一步拆分为“EFI 保留”范围和 OEM 范围。

版本历史记录

  • 2021-03-12:移除了提案中建议使用新错误代码来指示收到了超出范围的 zx_status_t 值的部分。相反,我们将审核树内代码以移除此类用法,并更新 FIDL 绑定生成器以避免超出范围的值在进程边界之间传播。

  • 2021-03-09:修改了提案,将有效 zx_status_t 值的范围重新定义为 [-2^30, 0]。此缩减的范围使 zx_status_t 更容易嵌入为其他类型的子范围。

  • 2021-02-10:修改了提案,将 zx_status_t 中的负值分为应用错误和系统错误,但保留所有非负值供应用使用,无需进一步解释。

  • 2020-10-09:最初提议将 zx_status_t 错误空间划分为四个分区:应用错误和系统错误为负值;而系统成功代码和应用成功代码为正值。0 会保持 ZX_OK