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 的記憶體管理子系統執行。一般來說,記憶體管理員以外的程式碼在將位址傳遞給記憶體管理員之前,不應執行任何檢查,以判斷位址是否有效 (例如檢查位址是否為非空值)。

良好

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)?;
  ...
}

不過,這個規則最常見的例外狀況是,當系統呼叫需要在提供的地址為空值時傳回特定錯誤,例如:

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 的最佳做法,這是所有 Starnix 系統呼叫引數的預設類型。

Starnix 系統呼叫實作項目的所有引數一開始都是 SyscallArg。然後,系統會使用 into() 特徵將此類型轉換為特定的 syscall 引數類型。舉例來說,當系統調用呼叫時,系統會以以下方式呼叫:

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 中發生恐慌情況,就等同於正在執行的容器發生核心恐慌情況。也就是說,如果系統呼叫使用 unwrap()expect() 等 API,就可能會發生恐慌情形,不僅是導致錯誤的程序,整個容器都會受到影響。

良好

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

這個範例是良好做法,因為它使用 ok_or_else() 來處理錯誤案例。

不佳

let value = option.unwrap();

這個範例是不當做法,因為它可能會發生恐慌並導致整個容器關閉,而非只會導致違反不變量的程序關閉。

不過,如果錯誤確實無法復原,可以使用 unwrap()expect()。不過,使用時應包含上下文字串,說明為何只有使用核心恐慌做為唯一選項。

在 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 Proxy

本節說明為何 Starnix 與某些 Fuchsia 元件不同,在與 FIDL 通訊協定互動時,通常會使用同步代理程式。

由於 Starnix 的執行模式,因此通常會使用同步 Proxy。具體來說,在服務 Linux 系統呼叫時,Starnix 程式碼會在叫用 Linux 系統呼叫的使用者程式執行緒上執行。

由於執行緒屬於 Linux 程式,Starnix 必須執行要求的工作,然後將控制權交還給 Linux 程式。

這項限制條件表示 Starnix 執行的工作必須在傳回控制權之前同步完成。

由於工作必須在傳回前完成,因此同步 Proxy 是最簡單的解決方案。同步 Proxy 的效能也較高,因為它可避免將背景切換至另一個執行緒,然後再切換回來 (使用非同步 Proxy 需要非同步執行緒,供非同步執行緒執行緒使用)。

如要進一步瞭解 Starnix 的執行模式,請參閱: * 在 Fuchsia 中執行 Linux 系統呼叫 * RFC 0261:快速且有效率的使用者空間核心模擬