本頁列出常見的編碼模式,以及 Starnix 開發專用的最佳做法。
Starnix 本質上是在使用者空間中執行的核心,可在 Fuchsia 上執行 Linux 程式。因此,開發人員必須確保程式碼產生 Linux 程式預期的結果,本頁面旨在釐清其中一些模式,並提供最佳做法,涵蓋測試、管理使用者空間位址、處理錯誤訊息等主題。
主題如下:
- 使用 Linux 二進位檔測試 Starnix
- 在 Starnix 中表示使用者空間位址
- 在 Starnix 中處理系統呼叫引數
- Starnix 中的安全錯誤處理
- 在 Starnix 中建立錯誤
- 防止 Starnix 中的算術溢位
- 在 Starnix 中使用 FIDL Proxy
- 在 Starnix 中產生執行緒
- 瞭解詳情
使用 Linux 二進位檔測試 Starnix
本節將說明測試 Starnix 功能的最佳做法,包括使用為 Linux 編譯的二進位檔。
Starnix 的大部分測試涵蓋範圍來自為 Linux 編譯的使用者空間二進位檔。Fuchsia 專案會在 Linux 和 Starnix 上執行這些二進位檔,確保 Starnix 的行為與 Linux 一致。
使用者空間單元測試會驗證 Linux UAPI,也就是 Starnix 實作的內容。 在這個層級驗證 Starnix 行為,可讓我們放心重構 Starnix 的實作。
此外,Starnix 核心單元測試有助於驗證系統的內部不變量。不過,如果您編寫核心單元測試,請務必避免「變更偵測器測試」。換句話說,請確保您撰寫的測試不會在實作變更時失敗,即使變更在功能上正確無誤也一樣。
在 Starnix 中表示使用者空間位址
本節將說明在 Starnix 的使用者空間中,表示及驗證位址的最佳做法。
UserAddress 和 UserRef 類型用於表示 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() 特徵,將這種類型轉換為特定系統呼叫引數型別。舉例來說,分派系統呼叫時,會呼叫為:
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 會使用 Linux 錯誤碼的包裝函式型別,稱為 errno。這類錯誤很有用,因為可以擷取錯誤的來源位置,有助於偵錯。建立新錯誤時,請使用 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 協定互動時使用同步 Proxy。
Starnix 通常會使用同步 Proxy,因為這是其執行模型。具體來說,在處理 Linux 系統呼叫時,Starnix 程式碼會在叫用 Linux 系統呼叫的使用者程式執行緒上執行。
由於這個執行緒屬於 Linux 程式,因此 Starnix 必須執行要求的工作,然後將控制權還給 Linux 程式。
這項限制表示 Starnix 執行的工作必須同步完成,才能傳回控制項。
由於工作必須在傳回前完成,因此同步 Proxy 是最簡單的解決方案。同步 Proxy 的效能也較高,因為它會避免切換至其他執行緒,然後再切換回來 (使用非同步 Proxy 需要獨立執行緒,供非同步執行器使用)。
在 Starnix 中衍生執行緒
請避免 std::thread::spawn。
std::thread::spawn 會建立成為呼叫程序一部分的執行緒。執行緒的生命週期會小於或等於呼叫程序的生命週期。
可能的問題所在
假設您在 Linux 程序環境中呼叫 std::thread::spawn,新產生的執行緒現在會與該 Linux 程序連結,程序終止時,執行緒也會終止。如果執行緒在程序終止時剛好持有互斥鎖 (或類似資源),互斥鎖就永遠不會釋出,且容器中的任何工作都無法取得該互斥鎖。
替代方案是什麼?
KernelThreads 的產生方法通常是正確的選擇。這些方法會建立執行緒,成為主要 Starnix 程序 (有時稱為原型程序) 的一部分。主要的 Starnix 程序沒有私人位址空間,且會比容器中的所有 Linux 程序存活更久。
瞭解詳情
如要進一步瞭解 Starnix 的執行模型,請參閱: - 在 Fuchsia 中發出 Linux 系統呼叫 - RFC 0261:快速有效率的使用者空間核心模擬