1ec53d23eSlogin(_spinlock_doc)= 2ec53d23eSlogin 3ec53d23eSlogin:::{note} 4ec53d23eSlogin作者:龙进 <longjin@RinGoTek.cn> 5ec53d23eSlogin::: 6ec53d23eSlogin 7ec53d23eSlogin# 自旋锁 8ec53d23eSlogin 9ec53d23eSlogin## 1.简介 10ec53d23eSlogin 11ec53d23eSlogin  自旋锁是用于多线程同步的一种锁,线程反复检查锁变量是否可用。由于线程在这一过程中保持运行的状态,因此是一种忙等待。一旦获取了自旋锁,线程会一直保持该锁,直至显式释放自旋锁。 12ec53d23eSlogin 13ec53d23eSlogin  DragonOS在`kernel/src/lib/spinlock.rs`文件中,实现了自旋锁。根据功能特性的略微差异,分别提供了`RawSpinLock`和`SpinLock`两种自旋锁。 14ec53d23eSlogin 15ec53d23eSlogin(_spinlock_doc_rawspinlock)= 16ec53d23eSlogin## 2. RawSpinLock - 原始自旋锁 17ec53d23eSlogin 18ec53d23eSlogin  `RawSpinLock`是原始的自旋锁,其数据部分包含一个AtomicBool, 实现了自旋锁的基本功能。其加锁、放锁需要手动确定对应的时机,也就是说,和我们在其他语言中使用的自旋锁一样, 19ec53d23eSlogin需要先调用`lock()`方法,然后当离开临界区时,手动调用`unlock()`方法。我们并没有向编译器显式地指定该自旋锁到底保护的是哪些数据。 20ec53d23eSlogin 21ec53d23eSlogin  RawSpinLock为程序员提供了非常自由的加锁、放锁控制。但是,正是由于它过于自由,因此在使用它的时候,我们很容易出错。很容易出现“未加锁就访问临界区的数据”、“忘记放锁”、“双重释放”等问题。当使用RawSpinLock时,编译器并不能对这些情况进行检查,这些问题只能在运行时被发现。 22ec53d23eSlogin 23ec53d23eSlogin:::{warning} 24ec53d23eSlogin`RawSpinLock`与C版本的`spinlock_t`不具有二进制兼容性。如果由于暂时的兼容性的需求,要操作C版本的`spinlock_t`,请使用`spinlock.rs`中提供的C版本的spinlock_t的操作函数。 25ec53d23eSlogin 26ec53d23eSlogin但是,对于新开发的功能,请不要使用C版本的`spinlock_t`,因为随着代码重构的进行,我们将会移除它。 27ec53d23eSlogin::: 28ec53d23eSlogin 29ec53d23eSlogin(_spinlock_doc_spinlock)= 30ec53d23eSlogin## 3. SpinLock - 具备守卫的自旋锁 31ec53d23eSlogin 32ec53d23eSlogin  `SpinLock`在`RawSpinLock`的基础上,进行了封装,能够在编译期检查出“未加锁就访问临界区的数据”、“忘记放锁”、“双重释放”等问题;并且,支持数据的内部可变性。 33ec53d23eSlogin 34ec53d23eSlogin  其结构体原型如下: 35ec53d23eSlogin 36ec53d23eSlogin```rust 37ec53d23eSlogin#[derive(Debug)] 38ec53d23eSloginpub struct SpinLock<T> { 39ec53d23eSlogin lock: RawSpinlock, 40ec53d23eSlogin /// 自旋锁保护的数据 41ec53d23eSlogin data: UnsafeCell<T>, 42ec53d23eSlogin} 43ec53d23eSlogin``` 44ec53d23eSlogin 45ec53d23eSlogin### 3.1. 使用方法 46ec53d23eSlogin 47ec53d23eSlogin  您可以这样初始化一个SpinLock: 48ec53d23eSlogin 49ec53d23eSlogin```rust 50ec53d23eSloginlet x = SpinLock::new(Vec::new()); 51ec53d23eSlogin``` 52ec53d23eSlogin 53ec53d23eSlogin  在初始化这个SpinLock时,必须把要保护的数据传入SpinLock,由SpinLock进行管理。 54ec53d23eSlogin 55ec53d23eSlogin  当需要读取、修改SpinLock保护的数据时,请先使用SpinLock的`lock()`方法。该方法会返回一个`SpinLockGuard`。您可以使用被保护的数据的成员函数来进行一些操作。或者是直接读取、写入被保护的数据。(相当于您获得了被保护的数据的可变引用) 56ec53d23eSlogin 57ec53d23eSlogin  完整示例如下方代码所示: 58ec53d23eSlogin 59ec53d23eSlogin```rust 60ec53d23eSloginlet x :SpinLock<Vec<i32>>= SpinLock::new(Vec::new()); 61ec53d23eSlogin { 62ec53d23eSlogin let mut g :SpinLockGuard<Vec<i32>>= x.lock(); 63ec53d23eSlogin g.push(1); 64ec53d23eSlogin g.push(2); 65ec53d23eSlogin g.push(2); 66ec53d23eSlogin assert!(g.as_slice() == [1, 2, 2] || g.as_slice() == [2, 2, 1]); 67ec53d23eSlogin // 在此处,SpinLock是加锁的状态 68*2eab6dd7S曾俊 debug!("x={:?}", x); 69ec53d23eSlogin } 70ec53d23eSlogin // 由于上方的变量`g`,也就是SpinLock守卫的生命周期结束,自动释放了SpinLock。因此,在此处,SpinLock是放锁的状态 71*2eab6dd7S曾俊 debug!("x={:?}", x); 72ec53d23eSlogin``` 73ec53d23eSlogin 74935f40ecSlogin  对于结构体内部的变量,我们可以使用SpinLock进行细粒度的加锁,也就是使用SpinLock包裹需要细致加锁的成员变量,比如这样: 75935f40ecSlogin 76935f40ecSlogin```rust 77935f40ecSloginpub struct a { 78935f40ecSlogin pub data: SpinLock<data_struct>, 79935f40ecSlogin} 80935f40ecSlogin``` 81935f40ecSlogin 82935f40ecSlogin  当然,我们也可以对整个结构体进行加锁: 83935f40ecSlogin 84935f40ecSlogin```rust 85935f40ecSloginstruct MyStruct { 86935f40ecSlogin pub data: data_struct, 87935f40ecSlogin} 88935f40ecSlogin/// 被全局加锁的结构体 89935f40ecSloginpub struct LockedMyStruct(SpinLock<MyStruct>); 90935f40ecSlogin``` 91935f40ecSlogin 92ec53d23eSlogin### 3.2. 原理 93ec53d23eSlogin 94ec53d23eSlogin  `SpinLock`之所以能够实现编译期检查,是因为它引入了一个`SpinLockGuard`作为守卫。我们在编写代码的时候,保证只有调用`SpinLock`的`lock()`方法加锁后,才能生成一个`SpinLockGuard`。 并且,当我们想要访问受保护的数据的时候,都必须获得一个守卫。然后,我们为`SpinLockGuard`实现了`Drop` trait,当守卫的生命周期结束时,将会自动释放锁。除此以外,没有别的方法能够释放锁。因此我们能够得知,一个上下文中,只要`SpinLockGuard`的生命周期没有结束,那么它就拥有临界区数据的访问权,数据访问就是安全的。 95ec53d23eSlogin 96ec53d23eSlogin### 3.3. 存在的问题 97ec53d23eSlogin 98ec53d23eSlogin#### 3.3.1. 双重加锁 99ec53d23eSlogin 100ec53d23eSlogin  请注意,`SpinLock`支持的编译期检查并不是万能的。它目前无法在编译期检查出“双重加锁”问题。试看这样一个场景:函数A中,获得了锁。然后函数B中继续尝试加锁,那么就造成了“双重加锁”问题。这样在编译期是无法检测出来的。 101ec53d23eSlogin 102ec53d23eSlogin  针对这个问题,我们建议采用这样的编程方法: 103ec53d23eSlogin 104ec53d23eSlogin- 如果函数B需要访问临界区内的数据,那么,函数B应当接收一个类型为`&SpinLockGuard`的参数,这个守卫由函数A获得。这样一来,函数B就能访问临界区内的数据。 105