Zircon 中的运行时锁定验证

简介

Zircon 集成了运行时锁验证器,用于诊断可能导致死锁的不一致的锁顺序。本文档介绍了如何集成该验证工具、如何在构建时启用和调整该验证工具,以及该验证工具生成的输出。

您可以在设计文档中找到验证器本身的操作理论。

启用锁验证器

锁定验证默认处于停用状态。停用后,锁定插桩是透明的,充当底层锁定基元的零开销封装容器

该验证器在编译时通过将 GN build 参数 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 成员进行插桩 (instrument) 提供了类似的支持。

如有兴趣了解,上例中的宏会扩展为以下类型表达式:::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. 手动获取/释放操作比 guard 更容易出错,必要时再进行手动释放。
  2. 启用锁验证后,Guard 会提供验证程序的存储空间,供验证器用来考虑主动持有的锁。此方法仅允许在锁定持有期间在堆栈上临时存储验证器状态,这与保护对象的使用模式相对应。如果不使用此方法,跟踪数据必须随每个锁实例一起存储,这样即使未持有锁时也会增加内存使用量,或存储在堆分配的内存中。这两种替代方案都不可取。

在极少数情况下,可以使用插桩锁的 lock() 访问器访问底层锁定。您应谨慎执行此操作,因为直接操控底层锁可能会导致锁的状态与锁验证器的状态不一致;这种情况最好导致缺少锁顺序警告,最严重的可能会导致死锁。你收到了警告!

Clang 静态分析和插桩锁定

锁定插桩旨在与 Clang 静态锁定分析进行互操作。在一般用法中,插桩锁定可用作“互斥”功能,并在任何静态锁定注解中指定。

您需要特别注意以下两种特殊情况:

  1. 返回对功能的指针或引用。
  2. 解锁通过引用传递的 Guard。

对功能的指针和引用

通过指针或引用返回锁时,使用统一类型可能很方便或有必要。如前所述,插桩锁定封装在一个类型中,用于捕获包含类型、底层锁定类型和行号,以区分属于不同类型的锁 (::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 实例非常有用。

待办事项(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 - 触发循环检测通过,并将发现的所有循环报告给内核日志。