Starnix 中的常见编码模式

本页面列出了适用于 Starnix 开发的常见编码模式和最佳实践。

Starnix 本质上是在用户空间中运行的内核,可在 Fuchsia 上运行 Linux 程序。对于需要确保其代码能产生 Linux 程序预期结果的开发者,这会带来一些特定的注意事项。本页旨在阐明其中的一些模式并提供最佳实践,涵盖测试、管理用户空间地址、处理错误消息等主题。

这些主题包括:

使用 Linux 二进制文件测试 Starnix

本部分介绍了测试 Starnix 功能的最佳实践,其中涉及使用为 Linux 编译的二进制文件。

Starnix 的大部分测试覆盖率来自为 Linux 编译的用户空间二进制文件。Fuchsia 项目会在 Linux 和 Starnix 上运行这些二进制文件,以确保 Starnix 与 Linux 行为一致。

用户空间单元测试用于验证 Linux UAPI,Starnix 就是通过它实现的。在此级别验证 Starnix 行为后,我们可以放心地自由重构 Starnix 的实现。

此外,Starnix 内核单元测试对于验证系统的内部不变量非常有用。不过,如果您编写内核单元测试,请务必避免使用“更改检测器测试”。换句话说,请确保您编写的测试不会在实现发生变化时失败,即使这些更改在功能上是正确的也是如此。

在 Starnix 中表示用户空间地址

本部分介绍了在 Starnix 用户空间中表示和验证地址的最佳实践。

UserAddressUserRef 类型用于表示 Starnix 中的“Linux 用户空间”(即受限地址空间)中的地址。确定 UserAddress 指向 T 类型的对象后,将其转换为 UserRef<T>。这种方法提供了更多类型信息,从而简化了对用户空间的读写操作。

地址验证由 Fuchsia 的内存管理子系统执行。一般来说,内存管理器之外的代码不应在将地址传递给内存管理器之前执行任何检查来确定地址是否有效(例如,检查地址是否非 null)。

良好

pub fn sys_something(current_task: &CurrentTask, user_events: UserRef<epoll_event>)
    -> Result <(), Errno> {
  let events = current_task.read_object(user_events)?;
  ...
}

pub fn sys_something(current_task: &CurrentTask, user_events: UserRef<epoll_event>)
    -> Result <(), Errno> {
  if user_events.addr().is_null() {
    return error!(EFAULT);
  }

  let events = current_task.read_object(user_events)?;
  ...
}

不过,此规则最常见的例外情况是,当提供的地址为 null 时,系统调用需要返回特定错误,例如:

pub fn sys_something(current_task: &CurrentTask, user_events: UserRef<epoll_event>)
    -> Result <(), Errno> {
  if user_events.addr().is_null() {
    // The memory manager would never return ENOSYS for a read_object at a null
    // address, so an explicit check is required.
    return error!(ENOSYS);
  }

  let events = current_task.read_object(user_events)?;
  ...
}

在 Starnix 中处理系统调用参数

本部分重点介绍了与 SyscallArg 相关的最佳实践。SyscallArg 是所有 Starnix 系统调用参数的默认类型。

Starnix 系统调用实现的所有参数都从 SyscallArg 开始。然后,使用 into() trait 将此类型转换为特定的系统调用参数类型。例如,在调度系统调用时,系统会按如下方式调用该调用:

match syscall_nr {
  __NR_execve => {
    sys_execve(arg0.into(), arg1.into(), arg3.into())
  }
}

这样一来,系统调用实现便可灵活地使用 SyscallArg 可转换成的任何类型。

良好

fn sys_execve(
    user_path: UserCString,
    user_argv: UserRef<UserCString>,
    user_environ: UserRef<UserCString>,
) -> Result<(), Errno> {
  ...
}

fn sys_execve(
    user_path: SyscallArg,
    user_argv: SyscallArg,
    user_environ: SyscallArg,
) -> Result<(), Errno> {
  ...
}

Starnix 中的安全错误处理

本部分介绍了在 Starnix 中使用 unwrap()expect() 方法的风险。

在 Starnix 中 panic 相当于其正在运行的容器的内核 panic。这意味着,如果系统调用使用 unwrap()expect() 等 API,则可能会导致 panic,不仅会导致导致错误的进程 panic,整个容器也会 panic。

良好

let value = option.ok_or_else(|| error!(EINVAL))?;

此示例是一个良好做法,因为它使用 ok_or_else() 来处理错误情形。

let value = option.unwrap();

此示例是一种不好的做法,因为它可能会 panic 并导致整个容器崩溃,而不仅仅是导致违反不变量的进程。

不过,如果错误确实无法恢复,则可以使用 unwrap()expect()。不过,使用该方法时应包含一个上下文字符串,说明为什么内核 panic 是唯一的选择。

在 Starnix 中创建错误

本部分介绍了创建和翻译错误的最佳实践。

Starnix 使用名为 errno 的封装容器类型来表示 Linux 错误代码。此类型非常有用,因为它可以捕获错误的来源位置,这在调试时非常有用。创建新错误时,请使用 error!() 宏。

良好

if !name.entry.node.is_dir() {
  return error!(ENOTDIR, "Invalid path provided to sys_chroot");
}

此示例是一个好做法,因为 errno!() 宏提供了更多信息,有助于调试。

return error!(EINVAL);

此示例是一种不良做法,因为它会返回错误代码,但没有任何上下文或错误信息。

此外,在将一种错误转换为另一种错误时,将 map_err()errno!() 宏搭配使用可能很方便。

良好

let s = mm.read_c_string_to_vec(user_string, elem_limit).map_err(|e| {
  if e.code == ENAMETOOLONG {
    errno!(E2BIG)
  } else {
    e
  }
})?;

let s = match mm.read_c_string_to_vec(user_string, elem_limit) {
  Err(e) if e.code == ENAMETOOLONG => {
    errno!(E2BIG)
  },
  Err(e) => {
    e
  },
  ok => ok,
}?;

防止 Starnix 中的算术溢出

本部分强调了在处理来自用户空间的数值时使用经过检查的数学运算的重要性。

对来自用户空间的数值,请始终使用经过检查的数学运算(例如 checked_add()checked_mul())。这可防止用户空间中的错误值导致内核中的算术溢出。

良好

pub fn sys_something(user_value: u32) -> Result<(), Errno> {
  let value = user_value.get()?;
  let result = value.checked_mul(2).ok_or_else(|| error!(EOVERFLOW))?;
  ...
}

此示例是一种良好做法,因为该代码使用 checked_mul() 对从用户空间检索的值执行乘法。这样可确保,如果乘法溢出,系统会返回 EOVERFLOW 错误,而不是可能导致意外行为或崩溃。

pub fn sys_something(user_value: u32) -> Result<(), Errno> {
  let value = user_value.get()?;
  let result = value * 2; // Potential overflow here!
  ...
}

此示例是一种不良做法,因为该代码会直接将用户提供的值乘以 2,而不会检查是否存在潜在的溢出问题。如果值足够大,则乘法可能会溢出,导致结果不正确或系统崩溃。

在 Starnix 中使用 FIDL 代理

本部分介绍了与某些 Fuchsia 组件不同,Starnix 在与 FIDL 协议交互时通常使用同步代理的原因。

Starnix 通常使用同步代理,因为它的执行模型是同步的。具体而言,在处理 Linux 系统调用时,Starnix 代码会在调用了 Linux 系统调用的用户程序的线程中运行。

由于线程属于 Linux 程序,因此 Starnix 必须执行请求的工作,然后将控制权返回给 Linux 程序。

此约束条件意味着,Starnix 正在执行的工作需要同步完成,然后才能返回控制权。

由于需要先完成工作,然后才能返回,因此同步代理是最简单的解决方案。同步代理的性能也更高,因为它可以避免上下文切换到另一个线程并返回(使用异步代理需要单独的线程供异步执行器使用)。

如需详细了解 Starnix 的执行模型,请参阅: * 在 Fuchsia 中执行 Linux 系统调用 * RFC 0261:快速高效的用户空间内核模拟