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 的正值来返回成功读取到缓冲区中的字节数。

虽然理论上可以通过使用额外的 out 形参或更复杂的复合类型来解决所有这些用例,但在某些对性能敏感的用例中,如果函数可以具有可处理上述用例的简单整数/寄存器返回值,将会更高效。

虽然本文档中使用了函数返回代码这两个术语,但这些概念同样适用于 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 的新定义。

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

缺点:来自不可信来源和 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 年 3 月 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