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 成员进行插桩。

对于好奇心,上面的示例中的宏会扩展为这种类型 表达式:::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

锁具

插桩锁定是使用作用域 capability 类型获取和释放的 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 的额外模板参数才能选择 获取锁时,可使用以下其中一种方法:IrqSaveNoIrqSave、 和 TryLockNoIrqSave。省略其中一种类型标记会导致 编译时错误。

#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. 启用锁验证后,Guard 会提供 验证器用于说明当前持有的锁。这种方法允许 只在堆栈上临时存储验证器状态 锁,这与 guard 对象的使用模式相对应。 如果不使用这种方法,跟踪数据要么存储在 增加内存使用量(即使没有持有锁),或者 存储在堆分配的内存中。这些替代方案都不可取。

在极少数情况下,可以使用 lock() 访问底层锁 插桩锁的存取器。操作时应谨慎操作 底层锁可能会导致 lock 和 lock Validator 的状态;最多可能导致系统无法锁上屏幕 严重时可能会导致死锁您收到了警告!

Clang 静态分析和插桩锁定

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

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

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

指针和对功能的引用

通过指针或引用返回锁时,这样做可能方便或必要 使用 uniform 类型。回想之前,插桩锁已封装 它捕获的是包含类型、底层锁类型以及 行号,用于消除不同类型的锁的歧义 (::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 - 触发循环检测传递并报告发现的任何循环 写入内核日志