天天看點

RAII技術:在Rust中實作帶有守衛的自旋鎖,支援一定程度上的編譯期并發安全檢查

摘要

本文介紹了一種使用了RAII技術的自旋鎖,配合Rust的生命周期及所有權機制,能夠在減少代碼量的同時,很好的解決自旋鎖的“忘記放鎖”、“雙重釋放”、“未加鎖就通路”的并發安全問題。并且這種自旋鎖能夠支援編譯期的檢查,任何不符合以上安全要求的代碼,将無法通過編譯。

前言

對于許多程式設計語言預設提供的鎖,加鎖、放鎖需要手動進行。手動加鎖可以了解(這不廢話嘛),但是,手動放鎖的時機,總是難以控制。比如:在臨界區内,執行過程中,如果程式出錯了,在異常處理的過程中,忘記放鎖,那麼就會造成其他程序無法獲得這個鎖。傳統的做法就是,人工尋找所有可能的異常處理路徑,添加放鎖的代碼。這樣做的話,能解決問題,但非常的繁瑣,尤其是有多個鎖的時候,更加如此。

并且,對于傳統的語言,還可能存在鎖的“雙重釋放”的問題,也就是:一個鎖被程序A釋放後,程序B對其加鎖,接着,程序A的錯誤代碼,執行了放鎖操作,導緻程序B的鎖被過早地釋放。這樣的問題,當我們發現的時候,可能已經不是第一現場了,debug很困難。

并且,對于大部分的語言,鎖與它所要保護的資料,并沒有一種機制,告訴編譯器/解釋器:“這個鎖,保護的就是這個資料對象”。是以,編譯器很難檢查出“未加鎖就通路”的bug,程式員會經常犯這種錯誤(尤其是對于新手程式員,很難處理好鎖的問題)。這樣的代碼,編譯器無法保證其并發安全。

對于Rust,借助其生命周期、所有權機制,我們能夠與RAII技術進行結合,能實作一種新的自旋鎖,進而輕松解決以上的問題。

具體的代碼連結:spinlock.rs (revision ec53d23e) - OpenGrok cross reference for /DragonOS/kernel/src/libs/spinlock.rs

什麼是RAII技術?

RAII,全稱資源擷取即初始化(英語:Resource Acquisition Is Initialization),它是在一些面向對象語言中的一種習慣用法。RAII源于C++,在許多的程式設計語言中都有應用。

RAII要求,資源的有效期與持有資源的對象的生命周期嚴格綁定,即由對象的構造函數完成資源的配置設定(擷取),同時由析構函數完成資源的釋放。在這種要求下,隻要對象能正确地析構,就不會出現資源洩漏問題。

思路

由于Rust在語言層面就實作了生命周期與所有權機制,是以,能夠很好的實作RAII,并且能夠支援編譯期檢查,不符合安全要求的代碼,将無法通過編譯。我們的思路是:把要保護的資料的所有權,交給對應的鎖來管理,不再需要程式員來手動管理“鎖——被鎖保護的資料”的關系。也就是說,這個自旋鎖,擁有要保護的資料的所有權,其他的地方需要通路被保護的資料,都需要從自旋鎖申請借用這個變量,獲得可變引用/不可變引用。這個通路的權限,不是直接給到要用到資料的函數内的局部變量的,而是由一個叫做“守衛”的對象負責持有權限。通路資料時,都要經過這個守衛(請注意,得益于Rust的“零成本抽象”,這是沒有運作時開銷的)。當守衛變量的生命周期結束,其析構函數就執行“放鎖”的動作。

自旋鎖出借自己保護的資料的通路權限時,會執行加鎖的動作,然後傳回一個守衛。請注意,守衛隻會在“自旋鎖加鎖成功”後被初始化。是以,對于一個自旋鎖,最多存在1個守衛。并且,隻要守衛的生命周期沒有結束,我們都能通過這個守衛,來通路被保護的資料。

那麼,我們來小結一下,基于RAII+所有權+生命周期機制的自旋鎖,解決以上問題的途徑:

  • 忘記放鎖/出現異常退出時,未放鎖:一旦守衛的生命周期結束,就會在析構函數中進行放鎖。
  • “雙重釋放“問題:所有放鎖操作隻能由守衛對象的析構函數進行。由于守衛對象最多同時刻隻有1個,并且,由于守衛對象隻要生命周期沒有結束,那麼鎖一定是被擷取到的。是以避免了“雙重釋放”的問題。
  • “未加鎖就通路被保護的資料“的問題:由于被保護的資料,其所有權屬于自旋鎖,并且是一個私有的字段。程序隻能通過守衛來通路被保護的資料。而要獲得守衛的方式隻有1種:成功加鎖。是以,它能解決“未加鎖就通路”的問題。任何想要“不加鎖就通路”的代碼,都無法通過編譯器的檢查。

實作

上面說了思路,那麼我們接下來就結合具體的代碼,來介紹一下它的實作:

結構體定義

下圖是SpinLock及其守衛的定義:

RAII技術:在Rust中實作帶有守衛的自旋鎖,支援一定程度上的編譯期并發安全檢查

spinlock.rs (revision ec53d23e) - OpenGrok cross reference for /DragonOS/kernel/src/libs/spinlock.rs

對于SpinLock,其内部包含兩個私有的成員變量:

  • lock:這是一個RawSpinlock,具體功能與其他語言的自旋鎖一緻,需要手動加鎖、放鎖,具有自旋鎖的最基本功能。不具備編譯期的并發安全檢查的特性。
  • data:這個字段是自旋鎖保護的資料。在自旋鎖被初始化時,要被保護的資料,會被放到這個UnsafeCell中。請注意,UnsafeCell支援内部可變性,也就是說,被保護的資料的值可以被修改。

對于SpinLockGuard這個守衛,它隻有1個成員變量,也就是SpinLock的不可變引用。并且,SpinLockGuard沒有構造器,它隻能通過SpinLock的lock()方法,在加鎖後産生。

SpinLock實作

SpinLock隻具有兩個成員方法:new()和lock()。如下圖所示:

RAII技術:在Rust中實作帶有守衛的自旋鎖,支援一定程度上的編譯期并發安全檢查

spinlock.rs (revision ec53d23e) - OpenGrok cross reference for /DragonOS/kernel/src/libs/spinlock.rs

  • new()方法:初始化lock字段,并且将資料放入data字段。請注意,由于傳入的value不是引用,是以,value的所有權,在new()函數結束後,被移動到了data字段中。程式的其他部分,不再擁有這個value的所有權。在外部的其他函數中,任何嘗試通路value的行為,都會被編譯器阻止。
  • lock()方法:本方法先對自旋鎖進行加鎖,然後傳回一個守衛。請注意,lock()函數是唯一的獲得守衛的途徑。

同時,我們為SpinLock實作Sync這個Trait,這樣,編譯器就知道,SpinLock是線程安全的,它能在幾個線程之間共享。(當然,我們要求T是實作了Send Trait的,因為隻有這樣,才意味着它能從一個程序發送給另一個程序)

RAII技術:在Rust中實作帶有守衛的自旋鎖,支援一定程度上的編譯期并發安全檢查

spinlock.rs (revision ec53d23e) - OpenGrok cross reference for /DragonOS/kernel/src/libs/spinlock.rs

SpinLockGuard實作

SpinLockGuard的實作也很簡單,我們為它實作了3個trait: Deref、DerefMut、Drop。

RAII技術:在Rust中實作帶有守衛的自旋鎖,支援一定程度上的編譯期并發安全檢查

spinlock.rs (revision ec53d23e) - OpenGrok cross reference for /DragonOS/kernel/src/libs/spinlock.rs

  • Deref:當我們通路SpinLockGuard時,相當于通路被自旋鎖保護的變量(不可變引用)
  • DerefMut:當我們通路SpinLockGuard時,相當于通路被自旋鎖保護的變量(可變引用)
  • Drop:當SpinLockGuard的生命周期結束時,将會自動釋放鎖。

如何使用這樣的自旋鎖?

與傳統的SpinLock需要反複确認變量在鎖的保護之下相比,SpinLock的使用非常簡單,隻需要這樣做:

RAII技術:在Rust中實作帶有守衛的自旋鎖,支援一定程度上的編譯期并發安全檢查

在上面這個例子中,我們聲明了一個SpinLock,并且把要保護的資料:一個Vec數組,傳了進去。然後,我們在第3行,擷取了鎖。在接下來的幾行中,我們通過這個守衛,來向Vec内部插入資料。當離開内部的閉包(由“{}”包裹)之後,在最後一行,我們通過列印,能發現,鎖被自動的釋放了。

對于結構體内部的變量,我們可以使用SpinLock進行細粒度的加鎖,也就是使用SpinLock包裹需要細緻加鎖的成員變量,比如這樣:

pub struct a {
  pub data: SpinLock<data_struct>,
}
           

那麼,對data_struct類型的data字段的通路,必須先加鎖,否則是無法通路它的。

當然,我們也可以對整個結構體進行加鎖:

struct MyStruct {
  pub data: data_struct,
}
/// 被全局加鎖的結構體
pub struct LockedMyStruct(SpinLock<MyStruct>);
           

總結

本文介紹的自旋鎖,使用了RAII技術,結合Rust的生命周期及所有權機制。将鎖與被其保護的資料進行了綁定,使其能夠支援編譯期檢查。減少了BUG的産生,也減輕了程式員手動維護“鎖——被鎖保護的資料”關系的負擔。

附錄

  • DragonOS中,SpinLock的實作:spinlock.rs (revision ec53d23e) - OpenGrok cross reference for /DragonOS/kernel/src/libs/spinlock.rs
    RAII技術:在Rust中實作帶有守衛的自旋鎖,支援一定程度上的編譯期并發安全檢查
    http://opengrok.ringotek.cn/xref/DragonOS/kernel/src/libs/spinlock.rs?r=ec53d23e#137
  • DragonOS代碼倉庫:GitHub - fslongjin/DragonOS: 一個64位的作業系統。An x86_64 operating system.一個64位的作業系統。An x86_64 operating system. Contribute to fslongjin/DragonOS development by creating an account on GitHub.
    RAII技術:在Rust中實作帶有守衛的自旋鎖,支援一定程度上的編譯期并發安全檢查
    https://github.com/fslongjin/DragonOS

轉載請注明來源:RAII技術:在Rust中實作帶有守衛的自旋鎖,支援一定程度上的編譯期并發安全檢查 – 龍進的部落格

RAII技術:在Rust中實作帶有守衛的自旋鎖,支援一定程度上的編譯期并發安全檢查

https://longjin666.cn/?p=1678

歡迎關注我的公衆号“燈珑”,讓我們一起了解更多的事物~

繼續閱讀