最近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;
就可能被編譯器優化為以下的代碼:
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的內核當中還有很多的路要走,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/323291.html
標籤:其他
上一篇:Tomcat部署及優化
下一篇:Docker從入門到初步掌握
