前言
受到2022年“谷歌使用Rust重寫Android系統且所有Rust代碼的內存安全漏洞為零” [1] 的啟發,最近筆者懷著濃厚的興趣也順應Rust 的潮流,嘗試著將一款C語言開發的基礎軟件轉化為 Rust 語言。本文的主要目的是通過記錄此次轉化過程中遇到的比較常見且有意思的問題以及解決此問題的方法與大家一起做相關的技術交流和討論。
問題描述
在項目轉化過程中我遇到了一個與 CAS (Compare and Swap) [2] 操作實現相關的問題,在計算機科學中CAS 是多線程/協程中用于實現同步的原子指令。該軟件針對不同的芯片平臺,通過在C語言中根據芯片平臺的類別進行宏定義并嵌入相應的匯編代碼來實現CAS操作。我知道不同芯片平臺對應的 CAS 操作的匯編代碼是不一樣的 [3],例如:
x86-64 (Intel/AMD) 需要類似如下匯編代碼塊:
lockcmpxchgq[destination],rdx
ARM 需要類似如下匯編代碼塊:
ldrexr1,[destination] cmpr1,r2 strexeqr1,r2,[destination]
PowerPC 需要類似如下匯編代碼塊:
lwarxr0,0,destination cmpwr0,r1 bneretry;branchifnotequal stwcx.r2,0,destination bneretry;branchifstorefailed
然而如下面的代碼片段所示,即使該軟件使用相同的Intel x86芯片平臺,但是在不同的操作系統平臺上其實現的匯編指令也有可能是不一樣的。
C頭文件中 cas_operation.h 的部分代碼如下:
#ifdefined(__i386)||defined(__x86_64__)||defined(__sparcv9)||defined(__sparcv8plus) typedefunsignedintslock_t; #else typedefunsignedcharslock_t; #endif externslock_tmy_atomic_cas(volatileslock_t*lock,slock_twith,slock_tcmp); #defineTAS(a)(my_atomic_cas((a),1,0)!=0) #endif
對應實現的x86匯編文件 cas_operation.s 的部分代碼如下:
my_atomic_cas: #ifdefined(__amd64) movl%edx,%eax lock cmpxchgl%esi,(%rdi) #else movl4(%esp),%edx movl8(%esp),%ecx movl12(%esp),%eax lock cmpxchgl%ecx,(%edx) #endif Ret
眾所周知雖然Rust也有宏定義的包 Macros,但是目前也與C語言的有不小的差別。因此,在做轉化的過程中如何做到芯片平臺和操作系統級別的代碼兼容則是我遇到的最大挑戰。
解決方案
想到兩個解決方案:
使用asm! 宏去處理不同芯片平臺的匯編代碼
使用 Rust代碼對特定的操作進行針對性的實現
第一種方案比較簡單,只需要在代碼中使用std::asm 包,然后使用 asm! 宏(類似 println! 宏)去包裹不同平臺的匯編代碼即可,這也是最直接最容易想到的解決方案,而且無需考慮具體的匯編操作實現的指令和代碼。但是這方法雜糅了很多的不同平臺的匯編代碼,同時需要Rust做很多額外的平臺相關的邏輯控制,對這些控制邏輯部分代碼的維護也是一個持久且復雜的工作。比如對新的平臺指令 RSIC-V 的支持也要納入其中。
第二種方案則需要考慮具體的操作邏輯,然后通過Rust代碼去實現與匯編指令相同的邏輯,雖然有較大的工作量,但是這種方案可以消除由于芯片和系統平臺不同帶來的各種匯編代碼實現的差異。關于第一種方案的實現讀者可以參照文檔 Inline assembly [4] 中去做。針對 CAS 操作的第二種方案的實現則是本文主要提出的一種解決方案,而本文以類似Rust u32類型的 CAS 操作為例子實現其代碼,在 my_compare_and_swap.rs 中會有如下代碼段實現:
usestd::{AtomicU32,Ordering}; pubtypeuint32=libc::c_uint; pubstructmy_atomic_uint32{ pubvalue:uint32, } implmy_atomic_uint32{ #[inline] pubfncompare_and_swap(&self,expected:uint32,newval:uint32)->bool{ letatomic_ptr=selfas*constmy_atomic_uint32as*constAtomicU32; letatomic=unsafe{&*(atomic_ptr)}; atomic.compare_and_swap(expected,newval,Ordering::SeqCst)==expected } } pubfnmy_compare_and_swap_u32_impl( mutptr:*mutpg_atomic_uint32, mutexpected:*mutuint32, mutnewval:uint32, )->bool{ letatomic=&*ptr; atomic.compare_and_swap(*expected,newval) }
下面我來解釋一下上面的代碼。由于是從 C 轉到 Rust,因此我使用了 Rust 的 libc 包來自定義 uint32類型。然后通過自定義struct my_atomic_uint32 來對uint32進行CAS原子操作的包裹,同時對于此 struct實現其 inline 的compare_and_swap 操作函數。在該函數的實現中最關鍵的是將my_atomic_uint32的實體轉化為一個AtomicU32的常量(注意需要在 Rust 代碼文件開頭使用 std::{AtomicU32, Ordering} [5]),然后通過調用 AtomicU32 的compare_and_swap 來最終實現 uint32 的 CAS 操作。另外對于Ordering::SeqCst內存順序 [6] 的選擇也是比較考究的一個話題,這里我使用 SeqCst實際上是一個在保證正確的情況下不太考慮效率優化問題的選項。代碼的最后my_compare_and_swap_u32_impl 則是對外使用的 u32 的 CAS 操作(事實上該軟件主要也是需要實現 uint32 的 CAS 操作)。
結論
在本例中由于剛好有對應AtomicU32的CAS 實現,而且軟件中整個原子同步的代碼部分都是使用uint32進行的比較交換操作,因此我選擇第二種方案則是最佳選擇。由此可知上述的兩種解決方案其實是各有利弊的,我必須結合實際的應用場景才能去做決定。那么這里有一個問題,如果需要對許多數據類型(比如uint32, int32, uint64, int64, float, float32, float64……)進行比較交換操作,又該做何種選擇呢?這也許是仁者見仁智者見智的。
關于作者
張懷龍曾就職于阿爾卡特朗訊,百度,IBM等企業從事云計算研發相關的工作。目前就職于 Intel 中國,擔任云原生開發工程師并致力于云原生、服務網格等技術領域研究實踐,也是Istio 的maintainer的開發者。曾多次在 KubeCon、ServiceMeshCon、IstioCon、GOTC 和 InfoQ/QCon 等大會上發表演講。
審核編輯:黃飛
-
ARM
+關注
關注
134文章
9168瀏覽量
369217 -
Android
+關注
關注
12文章
3945瀏覽量
127939 -
C語言
+關注
關注
180文章
7614瀏覽量
137720 -
匯編代碼
+關注
關注
0文章
24瀏覽量
7567 -
Rust
+關注
關注
1文章
230瀏覽量
6665
原文標題:一次Rust重寫基礎軟件的實踐(一)
文章出處:【微信號:Rust語言中文社區,微信公眾號:Rust語言中文社區】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
相關推薦
評論