最近Rust For Linux的項目,随着Rust的火爆也開始逐漸升溫,但是谷歌的強烈支援以及rCore OS、Redox等各種Rust作業系統項目的經驗積累,Rust想進入到Linux的真正核心,也還是有很長的路要走,之前筆者已經撰文對于Rust在彙編支援、panic和alloc等系統操作等方面的問題進行過簡要說明了。這裡再對于Rust進入到Linux核心的最大攔路虎-也就是記憶體模型方面的問題,做一下介紹。
記憶體模型對于作業系統為何如此重要
我們這裡所說的記憶體模型并不是作業系統管理和配置設定記憶體的機制,而是對于程式指令執行順序及可打斷性的執行政策,記憶體模型在單核單線程的時代幾乎沒有意義,直到2004年,Java率先引入了适用于多線程環境的記憶體模型:JSR-133,,自此多核時代下作業系統中記憶體模型的正式登場。
簡單的講當下最新的編譯器、作業系統及處理器等等底層技術棧,都會進行某種程度上對于代碼進行重排,以擷取執行效率的提升,比如以下代碼
x=getStatus()
if (x>0)
y = x;
else
y = 0;
![](https://img.laitimes.com/img/__Qf2AjLwojIjJCLyojI0JCLiYWan5CNxUGOyEjNhJDZ4MWM2EjY4U2M0EmZ5MWM2YjMzQWOl9CX5d2bs92Yl1iclB3bsVmdlR2LcNWaw9CXt92Yu4GZjlGbh5yYjV3Lc9CX6MHc0RHaiojIsJye.gif)
就可能被編譯器優化為以下的代碼:
y=0
x=getStatus()
if (x>0)
y = x;
當然這樣的執行順序重排都有一項重要的原則,就是不會影響單線程環境下程式的執行結果,但是在多線程并發的情況下,y在x之前先被指派,這對于程式邏輯是否會有潛在影響,這就是記憶體模型要面對的問題。
簡單來講,可以認為記憶體模型是一種程式性能與程式複雜性之間的平衡政策。一般來講記憶體模型主要包含了下面三個部分:
原子操作:原子類操作一旦執行就不會被打斷,是一種不存在中間狀态的操作,它要麼是執行完成,要麼執行失敗,外界無法觀測到執行過程中的狀态。
指令的執行順序:定義哪些指令執行的順序不能被打亂。
操作的可見性:定義哪些操作是需要被其它線程所看到。
記憶體模型與記憶體屏障指令對應,無論是寫屏障(writebarrier)、讀屏障(readbarrier)、還是通用屏障(genericbarrier)其實都是對于這幾方面的行為進行明确定義的操作指令。
當然這裡并不是要詳細介紹記憶體模型,隻是要說明當Rust隻進行應用程式的開發時,這門語言大可以不用在意記憶體模型,因為編譯器隻負責生成可執行的位元組碼,至于如何執行那是底層的作業系統和CPU的問題,但是當Rust編寫“無限接近計算機底層”的操作核心時,記憶體模型就會變得很重要。記憶體模型是多線程環境能夠可靠工作的基礎,因為記憶體模型需要對多線程環境的運作細節進行完備的定義。
效率和鎖的沖突
加鎖實際上就是限制了多線程計算機體系的運作效率,因為在同一時刻即使你有多個CPU也隻能有一個CPU程序在被鎖保護的區域工作,是以盡量少用鎖甚至不用鎖才是最終的目标,但無鎖程式設計是一巨大的挑戰。它的難度不僅僅是因為無鎖程式設計本身的複雜度,更在于多線程體系下無鎖系統的設計,可能很難被非技術出身的上司所了解,這其中的複雜度積累是非線性的,這裡先推薦一下an-introduction-to-lock-free-programming(
http://preshing.com/20120612/an-introduction-to-lock-free-programming。)
以最經典的無鎖隊列為例:
void LockFreeQueue::push(Node* newHead)
{
for (;;)
{
//複制共享變量(m_Head)到oldHead
Node* oldHead = m_Head;
//做一些不能被其他線程感覺的工作
newHead->next = oldHead;
// 然後嘗試将改動發送到共享變量中
// 如果共享記憶體沒有改變,則CAS成功,傳回
if (_InterlockedCompareExchange(&m_Head, newHead, oldHead) == oldHead)
return;
}
}
這裡InterlockedCompareExchange的實作簡要說明如下:
int compare_and_swap (int* reg, int newval, int oldval)
{
int old_reg_val = *reg;
if (old_reg_val == oldval) {
*reg = newval;
}
return old_reg_val;
}
可以看到這裡無鎖的概念其實就是在測試與共享變量reg是否有變化,如果沒有變化則操作成功,如果有變化則無需要再操作,因為肯定有其它線程修改了隊列。那麼這其中最關鍵的一點就是要對于記憶體模型中的可見性進行定義了。記憶體模型必須要保證對于reg的操作如:*reg = newval;對于其它線程是可見的,否則所謂的無鎖隊列也就不成立了。
Rust中的與衆不同的鎖
上月底谷歌釋出了一個RUST版本GPIO驅動,詳見:
https://github.com/wedsonaf,其中令人印象最深刻的是RUST和C語言在鎖方面的不同
C語言中鎖的典型用法如下:
raw_spin_lock_irqsave(&pl061->lock, flags);
gpiodir = readb(pl061->base + GPIODIR);
gpiodir &= ~(BIT(offset));
writeb(gpiodir, pl061->base + GPIODIR);
raw_spin_unlock_irqrestore(&pl061->lock, flags);
而Rust中鎖的用法如下:
let _guard = data.lock();
let pl061 = data.resources().ok_or(Error::ENXIO)?;
可以看到Rust中的lock鎖是與具體要保護的資料是有強綁定關系的,開發者要調用data.lock()将鎖進行鎖定,隻有這樣才能受鎖保護的資料才能被通路,是以程式員在使用鎖時犯錯誤,不可能出現鎖的張冠李戴,但這也會造成其它的問題,由于Rust的變量都是有嚴格的生命周期及借用機制的,是以鎖也很可能要在記憶體中移動,記憶體中對象的移動、所有權借用等等除了造成移動鎖之外還會有移動構造函數等等問題。
但是移動鎖、還移動構造函數這些概念在之前的Linux中幾乎是聞所未聞的,還是那句話,這樣的問題在Rust隻開發上層應用時都不是問題,但一旦深入到作業系統核心,這些就都成了問題,是以說Rust想真正深入到Linux的核心當中還有很多的路要走。