Zircon 中的執行階段鎖定驗證

說明

Zircon 整合了執行階段鎖定驗證工具,可診斷鎖定順序不一致的情況,進而造成死結。本文說明驗證工具的整合方式、如何在建構時間啟用與調整驗證工具,以及驗證工具產生的輸出內容。

如需驗證工具本身的作業理論,請參閱設計文件

啟用鎖定驗證工具

鎖定驗證功能預設為停用。如果停用鎖定檢測作業是透明的,則做為基礎鎖定基元的零負載包裝函式

驗證工具會在編譯期間啟用,方法是將 GN 建構引數 enable_lock_dep 設為 true。此變數的編寫邏輯目前是由 zircon/kernel/BUILD.gn 處理。

您可以在 GN 叫用中設定此變數,如下所示:

fx set <your build options> --args 'enable_lock_dep = true'

啟用鎖定驗證工具後,系統就會產生一組不受鎖定的全域性資料結構,並產生免等待的資料結構,用於追蹤檢測鎖定之間的關係。系統會擴增鎖定的接收/發布作業,以便更新這些資料結構。

鎖檢測

目前執行階段鎖定驗證工具的啟動作業,必須使用包裝函式類型,手動在核心中逐一檢測每個鎖定。包裝函式類型提供驗證工具所需的背景資訊,驗證工具需要正確識別鎖定,並產生全域追蹤結構,以用於具有相同情境或角色的鎖定。

核心會在 kernel/spinlock.hkernel/mutex.h 中定義公用程式巨集以用於此目的。

會員鎖

具有鎖定成員的類型,如下所示:

#include <kernel/mutex.h>

class MyType {
public:
    // ...
private:
    mutable fbl::Mutex lock_;
    // ...
};

可能的檢測方式如下:

#include <kernel/mutex.h>

class MyType {
public:
    // ...
private:
    mutable DECLARE_MUTEX(MyType) lock_;
    // ...
};

請注意,包含的類型會傳遞至 DECLARE_MUTEX(containing_type) 巨集。這個類型提供驗證工具所需的背景資訊,以便驗證屬於 MyType 成員的鎖定與其他類型的鎖定。

巨集 DECLARE_SPINLOCK(containing_type) 針對檢測 SpinLock 成員提供類似的支援。

如果您很好奇,上述範例中的巨集會展開為以下類型運算式:::lockdep::LockDep<containing_type, fbl::Mutex, __LINE__>。這個運算式會導致 lockdep::LockDep<> 類型重複例項化,包括涵蓋不同的類型類型,以及所屬類型中包含多個互斥鎖的類型。

全域鎖定

全域鎖定是以單例模式進行檢測。核心會在 kernel/mutex.hkernel/spinlock.h 中定義用途巨集。

在 Zircon 全域鎖定中,一般是在全域/命名空間範圍中定義,或是定義為靜態成員。

example.h:

#include <kernel/mutex.h>

extern fbl::Mutex a_global_lock;

class MyType {
public:
    // ...
private:
    static fbl::Mutex all_objects_lock_;
};

example.cpp:

#include "example.h"

fbl::Mutex a_global_lock;

fbl::Mutex MyType::all_objects_lock_;

這項檢測作業會宣告可能用於任一範圍的單例模式類型,並自動處理 ODR 用途,藉此簡化宣告鎖定。

example.h:

#include <kernel/mutex.h>

DECLARE_SINGLETON_MUTEX(AGlobalLock);

class MyType {
public:
    // ...
private:
    DECLARE_SINGLETON_MUTEX(AllObjectsLock);
};

這些巨集叫用會分別宣告新的單例模式類型 AGlobalLockMyType::AllObjectsLock。這些類型具有靜態 Get() 方法,可傳回基礎全域鎖定和所有必要的檢測。請注意,您不需要另外定義鎖定空間的儲存空間,系統會自動由支援範本類型處理鎖定作業。

DECLARE_SINGLETON_SPINLOCK(name) 巨集提供類似宣告全域 SpinLock 的支援。

鎖具

使用限定範圍能力類型 GuardGuardMultiple 取得及釋出檢測鎖定。在核心中,這些類型會在 kernel/lockdep.h 中定義。

針對簡單互斥鎖的 Guard 作業與 AutoLock 類似:

#include <kernel/mutex.h>

class MyType {
public:
    // ...

    int GetData() const {
        Guard<fbl::Mutex> guard{&lock_};
        return data_;
    }

    int DoSomething() {
        Guard<fbl::Mutex> guard{&lock_};
        int data_copy = data_;
        guard.Release();

        return DoWorkUnlocked(data_copy);
    }

private:
    mutable DECLARE_MUTEX(MyType) lock_;
    int data_{0} TA_GUARDED(lock_);
};

SpinLock 類型需要為 Guard 提供額外的範本引數,才能選取下列任一可能選項,在取得鎖定時選取:IrqSaveNoIrqSaveTryLockNoIrqSave。省略其中一種類型標記會導致編譯時間錯誤。

#include <kernel/spinlock.h>

class MyType {
public:
    // ...

    int GetData() const {
        Guard<SpinLock, IrqSave> guard{&lock_};
        return data_;
    }

    void DoSomethingInIrqContext() {
        Guard<SpinLock, NoIrqSave> guard{&lock_};
        // ...
    }

    bool TryToDoSomethingInIrqContext() {
        if (Guard<SpinLock, TryLockNoIrqSave> guard{&lock_}) {
            // ...
            return true;
        }
        return false;
    }

private:
    mutable DECLARE_SPINLOCK(MyType) lock_;
    int data_{0} TA_GUARDED(lock_);
};

檢測的全域鎖定可透過類似的方式運作:

#include <kernel/mutex.h>
#include <fbl/intrusive_double_list.h>

class MyType : public fbl::DoublyLinkedListable<MyType> {
public:
    // ...

    void AddToList(MyType* object) {
        Guard<fbl::Mutex> guard{AllObjectsLock::Get()};
        all_objects_list_.push_back(*object);
    }

private:
    DECLARE_SINGLETON_MUTEX(AllObjectsLock);
    fbl::DoublyLinkedList<MyType> all_objects_list_ TA_GUARDED(AllObjectsLock::Get());
};

請注意,檢測鎖定沒有手動的 Acquire()Release() 方法;使用 Guard 是直接取得鎖定的唯一方法。這麼做有兩個重要原因:

  1. 手動取得/發布作業比守則更容易出錯,必要時還可以手動發布。
  2. 啟用鎖定驗證功能時,防護工具會提供驗證工具用於考慮主動保留鎖定的儲存空間。這個方法只能於保留鎖定期間,暫時在堆疊上儲存驗證工具狀態,這與防護物件的使用模式相對應。如果不使用這個方法,追蹤資料必須儲存在每個鎖定執行個體中,因此即使未持有鎖定,或儲存在堆積分配記憶體中,也會增加記憶體用量。這兩種替代方式都不行。

在極少數情況下,您可以使用檢測鎖定的 lock() 存取子存取基礎鎖定。執行這項操作時應小心謹慎,因為直接操控基礎鎖定,可能會導致鎖定狀態與鎖定驗證工具的狀態不一致;最好這會導致缺少鎖定順序警告,嚴重時則可能導致死結。我們已發出警示!

Clang 靜態分析和檢測鎖

鎖定檢測設計為與 Clang 靜態鎖定分析互通。在一般情況下,檢測鎖定可做為「互斥鎖」功能,並透過任何靜態鎖定註解指定。

有兩種特殊情況需要特別留意:

  1. 傳回指標或功能的參照。
  2. 解鎖經由參照傳遞的守衛。

遊標和功能的參考資料

依指標或參照傳回鎖定時,可能會方便或使用統一類型。如先前所述,檢測鎖定會包裝在其類型、基礎鎖定類型和行號中,以區分屬於不同類型 (::lockdep::LockDep<Class, Locktype, Index>) 的鎖定。從統一 (虛擬) 介面 (例如核心 Dispatcher::get_lock()) 傳回鎖定時,這可能會發生問題。

幸好,每個檢測鎖定都是 ::lockdep::Lock<LockType> 的子類別 (或只有核心中的 Lock<LockType>)。這個類型僅取決於基礎 LockType,而非宣告檢測鎖定的情況,因此可做為指標或參照類型,更明確地參照檢測鎖定。此類型也可以用在類型註解中。

以下範例說明模式,與核心 Dispatcher 類型所使用的模式類似。

#include <kernel/mutex.h>


struct LockableInterface {
    virtual ~LockableInterface() {}
    virtual Lock<fbl::Mutex>* get_lock() = 0;
    virtual void DoSomethingLocked() TA_REQ(get_lock()) = 0;
};

class A : public LockableInterface {
public:
    Lock<fbl::Mutex>* get_lock() override { return &lock_; }
    void DoSomethingLocked() override {
        data_++;
    }
    void DoSomething() {
        Guard<fbl::Mutex> guard{get_lock()};
        DoSomethingLocked();
        // ...
    }
private:
    mutable DECLARE_MUTEX(A) lock_;
    int data_ TA_GUARDED(get_lock());
};

class B : public LockableInterface {
public:
    Lock<fbl::Mutex>* get_lock() override { return &lock_; }
    void DoSomethingLocked() override {
        // ...
    }
    void DoSomething() {
        Guard<fbl::Mutex> guard{get_lock()};
        DoSomethingLocked();
        // ...
    }
private:
    mutable DECLARE_MUTEX(B) lock_;
    char data_[32] TA_GUARDED(get_lock());
};

請注意,A::lock_ 的類型為 ::lockdep::LockDep<A, fbl::Mutex, __LINE__>B::lock_ 的類型為 ::lockdep::LockDep<B, fbl::Mutex, __LINE__>。不過,這兩種類型都是 Lock<fbl::Mutex> 的子類別,因此我們可以在指標和參照運算式中,統一將其視為這個類型。

雖然這雖然非常方便,但 Clang 靜態分析設有一項限制,將無法理解 LockableInterface::get_lock() 相當於 A::lock_B::lock_,即使在本機環境中也一樣。因此,您必須在所有鎖定註解中使用 get_lock()

解鎖經由參照而通過的監護人

在極少數情況下,從函式呼叫端釋出函式中存放的 Guard 例項是非常實用的做法。

TODO(eieio):這項功能的完整說明文件。

鎖定驗證錯誤

鎖定驗證工具會偵測並回報兩種廣泛的違規情況:

  1. 使用者開發時回報的配對違規行為。
  2. 多鎖定週期是由專屬的迴圈偵測執行緒以非同步方式回報。

獲客回報的違規事件

如果在獲取鎖定時偵測到違規事件,驗證工具會在核心記錄中產生如下的訊息:

[00002.668] 01032:01039> ZIRCON KERNEL OOPS
[00002.668] 01032:01039> Lock validation failed for thread 0xffff000001e53598 pid 1032 tid 1039 (userboot:userboot):
[00002.668] 01032:01039> Reason: Out Of Order
[00002.668] 01032:01039> Bad lock: name=lockdep::LockClass<SoloDispatcher<ThreadDispatcher, 316111>, Mutex, 282, (lockdep::LockFlags)0> order=0
[00002.668] 01032:01039> Conflict: name=lockdep::LockClass<SoloDispatcher<ProcessDispatcher, 447439>, Mutex, 282, (lockdep::LockFlags)0> order=0
[00002.668] 01032:01039> {{{module:0:kernel:elf:0bf16acb54de1ceef7ffb6ee4449c6aafc0ab392}}}
[00002.668] 01032:01039> {{{mmap:0xffffffff10000000:0x1ae1f0:load:0:rx:0xffffffff00000000}}}
[00002.668] 01032:01039> {{{mmap:0xffffffff101af000:0x49000:load:0:r:0xffffffff001af000}}}
[00002.668] 01032:01039> {{{mmap:0xffffffff101f8000:0x1dc8:load:0:rw:0xffffffff001f8000}}}
[00002.668] 01032:01039> {{{mmap:0xffffffff10200000:0x76000:load:0:rw:0xffffffff00200000}}}
[00002.668] 01032:01039> {{{bt:0:0xffffffff10088574}}}
[00002.668] 01032:01039> {{{bt:1:0xffffffff1008f324}}}
[00002.668] 01032:01039> {{{bt:2:0xffffffff10162860}}}
[00002.668] 01032:01039> {{{bt:3:0xffffffff101711e0}}}
[00002.668] 01032:01039> {{{bt:4:0xffffffff100edae0}}}

錯誤是資訊性和非嚴重錯誤。第一行會指出違反核心鎖定的執行緒和程序。下一行會指出違規類型接下來的兩行找出哪些鎖定與先前的觀察不一致:「錯誤鎖定」是即將取得的鎖定,「衝突」則是目前情境保留的鎖定,且與即將取得鎖定的點不一致。這之後的所有行都是堆疊追蹤的一部分,導致鎖定錯誤。

多鎖定週期

系統會使用專屬的迴圈偵測執行緒,偵測三個以上鎖定之間的循環依附元件。由於這項偵測作業是在另外的情境下進行,因此我們不會提供造成循環追蹤的鎖定作業。

來自迴圈偵測執行緒的報表如下所示:

[00002.000] 00000.00000> ZIRCON KERNEL OOPS
[00002.000] 00000.00000> Circular lock dependency detected:
[00002.000] 00000.00000>   lockdep::LockClass<VmObject, fbl::Mutex, 249, (lockdep::LockFlags)0>
[00002.000] 00000.00000>   lockdep::LockClass<VmAspace, fbl::Mutex, 198, (lockdep::LockFlags)0>
[00002.000] 00000.00000>   lockdep::LockClass<SoloDispatcher<VmObjectDispatcher>, fbl::Mutex, 362, (lockdep::LockFlags)0>
[00002.000] 00000.00000>   lockdep::LockClass<SoloDispatcher<PortDispatcher>, fbl::Mutex, 362, (lockdep::LockFlags)0>

週期期間的每個鎖定目標都會回報成一組。在任何任何時間,一個執行緒通常只會取得兩個循環相依的鎖定,這會導致手動偵測困難或難以執行。然而,三個以上執行緒之間的死結可能是真實存在,應用於長期系統穩定性。

核心指令

啟用鎖定驗證工具後,即可使用下列核心指令:

  • k lockdep dump:傾印所有檢測鎖定的依附元件圖表和連線集 (迴圈)。
  • k lockdep loop:觸發迴圈偵測傳遞,並回報找到核心記錄的任何迴圈。