LWN:Rust 另一些對 kernel 有用的特性!

語言: CN / TW / HK

關注了就能看到更多這麼棒的文章哦~

More Rust concepts for the kernel

By Jonathan Corbet September 20, 2021 Kangrejos DeepL assisted translation https://lwn.net/Articles/869428/ 

Kangrejos(Rust for Linux)會議的第一天介紹了這個專案以及它所要實現的目標;第二天介紹了一些 Rust 的核心概念以及跟核心開發有什麼關係。第三天,也是最後一天的時候,Wedson Almeida Filho 深入探討了如何讓 Rust 在 Linux 核心中用起來,他介紹了目前為止已經學到的一些經驗,並與一些核心開發人員討論了下一步的工作。

Almeida 首先指出,他不是 Rust 語言的開發者,也不覺得這種語言是完美的。但他確實相信,Rust 可以解決核心中的一些問題。他是安卓平臺安全團隊的工程師,一直在尋找改善該平臺的方法,特別是減少攻擊面(reduce attack surface)。他認為 Rust 可以做到這一點,並且它還有助於提高正確性,提供一個超出 C 語言能提供的 expressive type system(型別系統)。

Ownership

他繼續介紹了 Rust 中的一個重要概念,那就是資料所有權(data ownership)。Rust 程式中的每個數值都僅有一個所有者。這個所有權會隨著程式的執行而轉交給別人,但絕不會共享。當一個物件的所有者不再存在時,該物件就會被釋放掉(free)。所有權是具有獨佔性的,但是當然還是要有辦法可以將資料進行搬移和轉交的。在 Rust 中,這是用引用(reference)來實現的。mutable reference (可變引用)允許其持有者來修改資料,注意 mutable reference 是獨佔的,不可以存在對該物件的其他引用。而 shared reference (共享引用)則是非排他性的(non-exclusive),並且是隻讀的。

但是上述規則有幾種例外情況。Almeida 簡要提到了內部可變性(interior mutability)的想法,但並沒有深入探討細節。Rust 也支援原始指標(raw pointer),但是隻有在 unsafe 程式碼中才支援。

Rust 的所有權規則帶來的一個結果是,在用 safe Rust 編寫的程式碼中不會出現 data race。因為要產生 data race 的話,就必須要有至少兩個 CPU 在以未同步好的方式(unsynchronized manner)訪問共享資料,並且至少有一個在進行寫入。由於 mutable reference 是獨佔式的,使得這種情況根本不可能出現。

這些規則也使得編譯器可以對那些編譯器無法直接看到的程式碼進行優化。例如下面這樣的程式碼:

*x = 32
some_function()
return *x

編譯器可以完全放心地只返回 32。變數 x 不會有別名,所以 some_function()不可能會對它的值進行改動。

那麼,這個所有權規則在核心程式碼中該如何使用呢?Almeida 提出了一個例子,就是在許多核心結構中都有的 private_data 欄位。這個欄位一般是由子系統來將自己的特有資料填入到由系統中更高一層程式碼所管理的結構中。一般情況此欄位是一個 void * 指標,當其被真正使用起來時會被轉換為相應的型別。根據 Rust 標準,這不是安全的用法(not safe usage)。下面以 struct file (在核心中用其來表示一個開啟的檔案)為例來解釋。

在 Rust 程式碼中,開發者會寫一個 open() 函式,該函式會建立某種內部狀態,通常會通過 private_data 來存放這些資料。這些內部狀態資訊將被返回給呼叫者,並且這個狀態物件的所有權也被返回回去。如果使用者空間在開啟的檔案上呼叫 ioctl(),那麼 ioctl 處理程式將得到一個對此狀態物件的共享引用(shared reference)。這個引用必須是共享的,因為這些呼叫可能是併發出現的。相反,當檔案被關閉時,release() 函式就可以獲得這個狀態資料的所有權,然後就可以釋放掉。所有這些都可以使用 Rust 的 type 規則來提供型別安全以及併發安全(type and concurrency safety)。

Device IDs, locks, and more

另一個例子是整個驅動程式子系統中的 device-ID table。這些表中每個陣列元素都包含了一個 ID 以及一個無型別的可選引數,並且必須是用 null-terminated。Almeida 用 Rust 編寫了一個 PL061(GPIO)驅動,來實現了核心中的相應 C 驅動的功能,並建立了一個新的 device-ID table 抽象來配合使用。開發者必須要確定這裡可選引數的型別,並且提供來的所有資料都必須是這個型別的。也沒有必要再將 list 用 null-terminate 了,這就消除了驅動程式作者經常犯的一個錯誤。因此,整個機制就是型別安全的(type-safe),並且也更容易使用。

鎖(locking)是核心中很多複雜問題和混亂的來源之一。在 C 語言程式碼中,鎖通常被宣告為某個結構中的一個欄位,而且往往不清楚某個特定的鎖到底是在保護什麼資料。而在 Rust 中,資料是直接跟保護它的鎖關聯起來的,這樣一來,如果不先獲得鎖就沒法編寫訪問該資料的程式碼。至少得滿足這個條件之後編譯器才會幫開發者完成編譯。所有的檢查工作都可以在編譯時完成。

Almeida 簡要地提到了代號 CVE-2021-26708 的漏洞,這是在 mainline 核心中一個可以被惡意攻擊利用的 race condition,就是在獲得保護資料的鎖之前訪問資料而造成的問題。他認為 Rust 就可以防止這個漏洞的發生。鎖在定義的時候就會確保在沒有獲得該鎖的情況下不能觸及相關資料。但是,如果開發者沒有意識到首先需要一個鎖,會發生什麼?在這種情況下,Rust 的所有權規則會起到效果,因為試圖修改未受保護的資料會因為使用了錯誤的引用型別而失敗。

Laurent Pinchart 跳出來說,他喜歡這種把資料和保護它的鎖捆綁在一起的想法。但他比較擔心開發者明知道不需要鎖的那些情況,比如初始化部分的程式碼就屬於這樣的情況。看起來如果編譯器知道對有關物件只能有一個引用,那麼鎖就沒有必要了,例如,當物件剛剛被建立時就會是這種情況。Almeida 說,如果所有其他辦法都沒法用的話,開發者總是可以使用 "unsafe escape hatch" (意思是寫 unsafe 程式碼來繞過編譯器的限制)。

另一個需要注意的抽象概念是檔案描述符(file descriptors),這些描述符在建立時首先需要獲得對底層相應的檔案結構的引用,然後才能分配描述符編號。如果分配失敗的話,程式碼必須要記著放棄(drop)對檔案的引用。Rust 的生命週期管理功能可以讓錯誤處理自動就完成,減少了很多八股文一樣的程式碼。核心程式碼中經常出現的那種 "goto out; " 的寫法在 Rust 中是沒有必要的。

Almeida 指出 CVE-2019-15971 就是由於未能增加檔案的引用計數而產生的 use-after-free 漏洞。在 Rust 中如果犯了這種錯誤的話,一定會收到編譯器的友好提示資訊的。

繼續談論到錯誤路徑(error path),Almeida 重複強調說,他認為在核心程式碼中看到的大多數複雜和容易出錯的錯誤處理,在使用 Rust 時都可以消失了。在大多數情況下,物件會在超出 scope (生效範圍)的時候就會直接被清理掉,根本不需要顯式去進行處理。對於開發者需要更多的控制權的那些情況,可以使用 scopeguard 物件。這個物件在初始化時會使用相應的 error-handling 資訊,如果一切順利的話,不需要呼叫這些 error-handling 程式碼,那麼它的 dismiss() 方法會被呼叫到。否則,如果該物件超出了生效範圍之後的時候會直接執行相應的 error handling 錯誤處理。

此外會議上還討論了其他一些核心抽象概念,包括 task 結構、紅黑樹,以及對 memory-mapped I/O 區域的訪問。不過,Almeida 想講的觀點在此時應該已經很清楚了。Rust 語言能夠處理核心層面的程式設計工作,這比用 C 語言完成同樣的任務要安全得多。

Discussion

Julia Lawall 首先詢問了在 Rust 中有哪些缺點?有什麼突出的缺點?Almeida 回答說,在 Rust 中,所有的物件在記憶體中都是可移動的(movable),這對那些會自己引用自己(self-referential)的資料結構來說可能是個問題。解決辦法是釘住(pinning),但這就需要編寫 unsafe 的程式碼了。他說,現有的 Rust 驅動中的很多 unsafe 程式碼都源於這個問題。另一個問題是 Rust 要求所有資料都要被初始化,這對 mutex 來說尤其是一個問題。要解決這個問題的話主要是要找到並使用正確的抽象概念。

Pinchart 對 Rust 開發者所採取的漸進式方法提出質疑,這個問題在討論中也多次出現。他說,正確的做法是把核心子系統的維護者關在一個房間裡,要求他們學習 Rust,並在過渡期間為這些維護者提供大量的幫助。如果維護者被一個與 Rust 有關的問題所阻礙了,那麼他們應該能夠立即得到幫助來解決這個問題。否則的話,Rust 開發者將遇到大量的反抗。

Ojeda 質疑是不是真的會碰到反抗。他說,沒有人要剝奪使用 C 語言來編寫驅動程式的權利。Pinchart 回答說,在未來某個時刻,維護者可能不接受這些驅動程式。Ojeda 說,這可能是五年或十年後的事情了,不會那麼快。他認為有必要期望核心開發者們來學習一些 Rust 知識。safe 模式下的工作並不困難。他承認 unsafe 的 Rust 模式確實比較難,畢竟又會出現那些對未定義行為的擔憂了,而且文件也沒有那麼好。

Pinchart 又提出了另一個有很多人提到的擔心,那就是核心開發者必須要能夠使用整個原始碼 tree 來進行工作,很少有開發者是從來不去檢視自己子系統之外的程式碼的。這樣一來,我們很難在早期就將 Rust 在核心中的影響降到最低,畢竟會有無數開發者可能會讀到 Rust 部分的程式碼。Ojeda 認為現有計劃是隻在已經瞭解了 Rust 的維護者相應的子系統中引入 Rust,但 Pinchart 回答說,不應該指望其他開發者就不會受到這些 Rust 程式碼的影響了。開發人員幾乎肯定也是要用 unsafe Rust 來工作的。Mark Brown 補充說,關於 Rust 的引入,可能會有某個標誌性的一天,在那一天之後所有維護者們將不得不在某種程度上瞭解這種語言。

Almeida 問道,在這種過渡開始之前,需要有多少比例的核心開發人員要懂得 Rust?Jonathan Cameron 回答說:"a lot"。我從引進 ReStructured Text 語言文件支援的過程中得到了一些經驗,那就是這個過程要比預期的要長,這個過程中遇到了一些激烈的(和持續的)阻力,並且現在仍然沒有全部完成。如果沒有被分開發社群中相當多的人所接受的話,它就不可能會達到今天的這種成功。Rust 的引入要比這個複雜得多,因此它被接受的時間只會更長。因此是必須要努力讓大家廣泛能夠接受的。Pinchart 說,現在大多數開發者似乎都對 Rust 的價值感興趣,但其中許多人在擔心將其引入核心帶來的成本是不是過高。

Ojeda 說,Linux 的 Rust 開發者需要能說服足夠多的開發者來相信 Rust 的價值,這樣 Linus Torvalds 就會給這個改動給予祝福,否則就可以乾脆結束這些討論了。他期待著即將舉行的 Linux Plumbers Conference 和 Kernel Summit 的討論,能夠幫助把更多的開發者帶入這個過程。我對他過於依賴 Torvalds 祝福的想法表示了一些擔憂,因為這是一個必要條件,但遠遠不是充分條件。

Greg Kroah-Hartman 說,他喜歡將 Rust 引入核心開發的想法,但也認為這項工作還有很長的路要走。裝置驅動(也就是 Rust 開發者初期的目標領域)必須要與核心的許多部分互動,包括 driver model、sysfs 和其他各種子系統,而能夠實現這種互動的 Rust 功能還沒有出現。圍繞著 Rust 的引入,會有社會、政治和技術問題,而目前甚至連技術問題本身都還沒有處理好。儘管如此,他還是讚揚了 Rust 開發者到目前為止所取得的進展。

會議時間快要不夠了,Kroah-Hartman 說,至少在五年內不可能要求開發者用 Rust 編寫。Pinchart 問道,是否有人想過,社群願意在這次轉型中失去多少開發者,實際肯定會有一些開發者因此離去的。Ojeda 最後說,讓 Rust 進入核心的過程是對這一點來說最重要的一個因素,他的下一步將是在 Linux Plumbers 大會上進行討論。

全文完

LWN 文章遵循 CC BY-SA 4.0 許可協議。

歡迎分享、轉載及基於現有協議再創作~

長按下面二維碼關注,關注 LWN 深度文章以及開源社群的各種新近言論~