简介
Zircon 集成了运行时锁定验证程序,用于诊断不一致的锁定 可能会导致死锁本文档将讨论 如何集成验证程序、如何在构建时启用和调整验证程序, 以及验证程序生成的输出内容。
有关验证器本身的运行理论,请参阅 设计文档。
启用锁定验证器
默认情况下,锁定验证处于停用状态。停用锁定插桩后 是透明的,可充当底层锁定的零开销封装容器 基元。
您可以通过设置 GN build 参数在编译时启用验证器
enable_lock_dep
设为 true。根据此变量的编写逻辑,
由 zircon/kernel/BUILD.gn 处理。
您可以在 GN 调用中设置此变量,如下所示:
fx set <your build options> --args 'enable_lock_dep = true'
启用锁验证器后,系统会提供一组全局无锁、无等待数据 并生成结构来跟踪插桩测试之间的关系 锁定。增加了锁的获取/释放操作,以更新 这些数据结构。
锁定插桩
运行时锁定验证器的当前版本需要手动 使用封装容器类型对内核中的每个锁进行插桩。封装容器类型提供 验证器正确识别锁并生成 全局跟踪结构。
内核在 kernel/spinlock.h
和
kernel/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.h
和 kernel/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);
};
这些宏调用声明了新的单例类型 AGlobalLock
和
MyType::AllObjectsLock
。这些类型具有静态 Get()
方法
该函数会返回底层全局锁
插桩。请注意,无需为
则由支持的模板类型自动处理。
宏 DECLARE_SINGLETON_SPINLOCK(name)
为以下对象提供类似的支持:
声明了全局 SpinLock
。
锁具
插桩锁定是使用作用域 capability 类型获取和释放的
Guard
和GuardMultiple
。在内核中,这些类型是在
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
的额外模板参数才能选择
获取锁时,可使用以下其中一种方法:IrqSave
、NoIrqSave
、
和 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
是直接获取锁的唯一方式。那里
有两个重要原因
- 手动获取/释放操作比后期守则更容易出错,并且 必要时手动释放。
- 启用锁验证后,Guard 会提供 验证器用于说明当前持有的锁。这种方法允许 只在堆栈上临时存储验证器状态 锁,这与 guard 对象的使用模式相对应。 如果不使用这种方法,跟踪数据要么存储在 增加内存使用量(即使没有持有锁),或者 存储在堆分配的内存中。这些替代方案都不可取。
在极少数情况下,可以使用 lock()
访问底层锁
插桩锁的存取器。操作时应谨慎操作
底层锁可能会导致
lock 和 lock Validator 的状态;最多可能导致系统无法锁上屏幕
严重时可能会导致死锁您收到了警告!
Clang 静态分析和插桩锁定
锁插桩旨在与 Clang 静态锁进行互操作 分析。一般来说,插桩锁可以用作“互斥锁” 功能,并在任意静态锁定注释中指定。
需要特别注意以下两种特殊情况:
- 返回对功能的指针或引用。
- 解锁通过引用传递的 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):此功能的完整文档。
锁定验证错误
锁验证程序可检测并报告两大类违规行为:
- 在收购时报告的成对违规行为。
- 由专用循环检测机制异步报告多锁定周期 线程。
在收购时报告的违规行为
如果在获取锁时检测到违规行为,验证器 会在内核日志中生成如下消息:
[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
- 触发循环检测传递并报告发现的任何循环 写入内核日志