Linux同步互斥4(基于Linux6.6)---spin lock
一、概述
在Linux kernel的实现中,经常会遇到这样的场景:共享数据被中断上下文和进程上下文访问,该如何保护呢?如果只有进程上下文的访问,那么可以考虑使用semaphore或者mutex的锁机制,但是现在中断上下文也参和进来,那些可以导致睡眠的lock就不能使用了,这时候,可以考虑使用spin lock。
在 Linux 内核中,Spin Lock(自旋锁)是一种常用的用于多线程/多核环境下的同步机制,旨在保护共享资源的并发访问。它是一种轻量级的锁,通常用于在短时间内保护共享数据结构的并发访问。与其他传统的锁(如互斥锁)不同,自旋锁的特性是当一个线程无法获取锁时,它不会被挂起或进入睡眠状态,而是会在一个紧密的循环中不断地检查锁是否已经释放(即“自旋”)。因此,适合用于保护临界区非常短小的情况下。
Spin Lock 的工作原理
-
自旋: 当一个线程试图获取一个已经被其他线程持有的自旋锁时,它会在同一位置持续不断地检查该锁的状态,并反复尝试获取锁。只有当锁被释放时,它才会成功获取锁。
-
锁的持有: 一旦线程成功获取了锁,它可以进入临界区并执行访问共享资源的操作。
-
释放锁: 当线程完成对共享资源的操作后,它将释放锁,允许其他等待的线程获取锁。
自旋锁的优缺点
优点:
- 低开销:如果锁被持有的时间非常短,自旋锁相比传统的互斥锁(mutex)具有较低的开销,因为它避免了线程的上下文切换。它简单且高效,适用于资源竞争非常少的场景。
- 避免睡眠和唤醒:在短时间锁定时,自旋锁不需要进行线程的睡眠和唤醒,避免了调度器的上下文切换开销。
缺点:
- 忙等待:如果锁的持有时间较长,线程将会在忙等待(自旋)中浪费 CPU 时间。这种情况下,如果锁竞争较激烈,可能会导致 CPU 资源的浪费。
- 死锁风险:如果程序员使用自旋锁时没有正确处理锁的释放或出现循环依赖,可能会导致死锁。
自旋锁的使用场景
- 短时间临界区:当多个线程并发访问共享资源时,自旋锁适合用于保护那些占用时间很短的临界区。这样,线程不会因为锁的等待而发生上下文切换。
- CPU 负载较低时:如果系统负载较低,或者系统中的线程数比 CPU 核心数少,使用自旋锁可能会比使用互斥锁更有效。
- 没有 I/O 操作的临界区:自旋锁适用于没有涉及 I/O 等长时间操作的临界区,否则会导致线程长时间处于自旋状态,浪费 CPU。
Linux 中自旋锁的实现
在 Linux 内核中,自旋锁的实现通过内核的原子操作来保证线程间的同步。内核提供了专门的接口来使用自旋锁,常见的接口包括 spin_lock、spin_unlock、spin_lock_irqsave 和 spin_unlock_irqrestore 等。
基本操作:
-
spin_lock 和 spin_unlock:
spin_lock():获取自旋锁,如果自旋锁已被持有,当前线程将会自旋(不断检查锁的状态)。spin_unlock():释放自旋锁,允许其他等待的线程获取锁。
-
spinlock_t my_lock; spin_lock(&my_lock); // 获取锁 // 临界区代码 spin_unlock(&my_lock); // 释放锁 -
spin_lock_irqsave 和 spin_unlock_irqrestore:
spin_lock_irqsave():在获取锁的同时,禁用本地中断。这样可以避免在持有锁期间被中断打断,防止死锁等问题。通常用于处理涉及中断的共享资源。spin_unlock_irqrestore():释放锁并恢复中断状态。-
spinlock_t my_lock; unsigned long flags; spin_lock_irqsave(&my_lock, flags); // 获取锁并禁用中断 // 临界区代码 spin_unlock_irqrestore(&my_lock, flags); // 释放锁并恢复中断状态自旋锁的类型:
-
普通自旋锁:最常见的自旋锁类型,使用
spinlock_t变量来表示锁状态。 -
spinlock_t my_lock = SPIN_LOCK_INIT; // 初始化自旋锁 -
读写自旋锁:如果临界区的操作较复杂,可能需要对资源进行读取和写入操作,可以使用读写自旋锁来优化并发。读写自旋锁允许多个读者并行访问,但在写者访问时会阻塞所有其他线程。
rwlock_t是 Linux 提供的读写自旋锁类型,提供了read_lock、read_unlock、write_lock和write_unlock等操作。
适用场景
- 短时间锁定:当锁的持有时间较短,且系统中存在较少的竞争时,自旋锁适合用于提高性能。
- CPU 密集型工作:自旋锁在高负载下通常不会显著影响性能,特别是当临界区代码快速执行并且不涉及 I/O 操作时。
- 避免上下文切换开销:如果每个线程执行的任务很短,自旋锁可以避免操作系统调度器的上下文切换,从而提高性能。
二、工作原理
2.1、spin lock的特点
Spin Lock(自旋锁)是一种用于多线程或多核处理器环境中的同步机制,其主要特点如下:
1. 自旋等待(Busy Waiting)
- 自旋锁的核心特点是线程在获取不到锁时,不会进入休眠状态(即不被挂起),而是会持续地循环检查锁的状态,这种行为被称为“自旋”。如果锁被其他线程持有,线程会持续自旋,直到锁被释放。
- 这种自旋的方式避免了上下文切换的开销,特别适合锁竞争较少、锁持有时间短的场景。
2. 轻量级锁机制
- 自旋锁是轻量级的同步机制,相较于其他锁(如互斥锁,mutex),它的实现非常简单,且开销小。自旋锁在没有竞争的情况下可以非常高效地工作,特别适合保护那些占用时间极短的临界区。
- 由于避免了线程的挂起和调度上下文切换,尤其适合锁竞争较少的情况。
3. 避免上下文切换
- 自旋锁的一个重要优势是它避免了线程的上下文切换。在需要锁的临界区很短时,线程可以在同一个 CPU 上自旋,而不需要等待调度器进行上下文切换,这降低了开销。
- 这种特性使得自旋锁在短时间临界区和高并发场景下特别有用。
4. 适用于短时间的临界区
- 自旋锁最适用于临界区代码短且锁持有时间非常小的情况。如果锁持有时间过长,线程可能会长时间自旋,浪费 CPU 资源。因此,如果自旋锁用于较长时间的锁定,会导致效率低下。
5. 高竞争下的性能问题
- 当锁的竞争非常激烈时,多个线程可能会在自旋过程中不断消耗 CPU 资源,这会导致性能下降。自旋锁的缺点之一就是,如果一个线程长时间无法获得锁,它将持续消耗 CPU 时间,这在高竞争场景下可能会成为瓶颈。
6. 死锁风险
- 尽管自旋锁本身并不直接导致死锁,但如果程序员没有正确管理多个自旋锁的顺序,或者错误地使用自旋锁,可能会引入死锁问题。特别是在多个线程以不同顺序获取多个自旋锁时,可能会出现死锁。
7. 不适合长时间等待
- 自旋锁通常不适合用于那些可能会长时间等待资源的场景。如果临界区需要较长时间完成任务,或者需要频繁的上下文切换,自旋锁就不再高效。此时,可以考虑使用互斥锁(mutex)或条件变量等更合适的同步机制,它们能更好地处理长时间的等待。
8. 适用于无 I/O 操作的临界区
- 自旋锁适合用于没有 I/O 操作的临界区。因为 I/O 操作通常需要等待较长时间,在这类操作中使用自旋锁会浪费大量的 CPU 资源。因此,自旋锁通常用于保护对内存或 CPU 密集型操作的访问。
9. 适用于多核环境
- 自旋锁的效率在多核处理器环境中尤其高,因为一个线程在等待时,它可以自旋在另一个核心上执行,而不会影响当前核心的其他线程。这使得自旋锁能够在多核系统上保持较高的效率。
10. 适用于临界区较小的场景
- 自旋锁通常用于临界区较小的情况,即锁的持有时间非常短,线程可以快速获取并释放锁。如果锁的持有时间较长,选择自旋锁可能会导致严重的性能问题,特别是在锁竞争激烈时。
2.2、 场景分析
Spin Lock 是一种简单的锁机制,其特点是在没有获取到锁时,线程会持续地自旋等待,而不是将自己挂起。它非常适合在一些特定的场景下使用,但并不适用于所有情况。下面通过几个具体的场景来分析 Spin Lock 的使用情况及其适用性。
1. 短时间临界区,低竞争的场景
示例:多线程计数器加法
在多线程程序中,多个线程可能需要对一个共享的计数器进行加法操作。如果每次加法操作的代码非常简单且执行时间非常短,并且线程之间的竞争不激烈,那么使用 Spin Lock 会非常高效。
场景描述:
- 线程 A、B、C 等分别对一个共享的计数器执行加法操作,每次加法的时间非常短,仅仅是执行几个简单的加法和存储操作。
- 锁的竞争较小:每个线程几乎能迅速获取锁,进行加法操作,然后释放锁。
为什么适合:
- 临界区非常短:如果临界区代码的执行时间很短(如一次简单的加法),自旋锁能快速获得并释放锁,不会浪费很多 CPU 时间。
- 低竞争:多个线程对计数器的访问冲突并不严重,因此大部分线程可以快速自旋获得锁。
- 减少上下文切换:在这种情况下,自旋锁避免了线程被挂起、唤醒和上下文切换的开销。
代码示例:
std::atomic<int> counter(0);
std::mutex lock;
void increment() {
while (true) {
// 尝试获取锁
if (lock.try_lock()) {
counter++;
lock.unlock();
break;
}
}
}
这种方式适用于短时间的自旋锁,但如果锁竞争激烈,性能会迅速下降。
2. 多核环境下的短时间同步
示例:多核 CPU 中的临界区保护
在一个多核 CPU 系统中,每个核心有独立的执行单元,多个线程可能会同时在不同的核心上运行。对于每个线程,只需要对某些共享资源进行快速的操作,并且这些操作会在很短的时间内完成,使用 Spin Lock 是合适的。
场景描述:
- 假设有多个线程在多个 CPU 核心上并行运行,每个线程需要操作一个共享的缓存(例如,更新某个计数器的值)。
- 每次操作缓存的时间非常短,例如仅仅进行一个计数器的加法。
- 由于是多核 CPU 环境,当一个线程正在自旋等待锁时,它可以充分利用其他核上的计算能力。
为什么适合:
- 多核系统:由于每个核心都能执行任务,因此一个线程可以在等待锁的过程中在其他核心继续工作,不会浪费大量 CPU 时间。
- 临界区短:操作非常快,锁持有时间极短,因此自旋等待的时间也非常短,不会浪费过多资源。
代码示例:
std::atomic<int> counter(0);
std::mutex lock;
void increment() {
while (true) {
if (lock.try_lock()) {
counter++;
lock.unlock();
break;
}
}
}
在这个场景下,锁竞争不严重,且临界区短,因此自旋锁有效。
3. 高并发环境中,锁竞争不严重
示例:高并发的日志记录
在高并发环境中,多个线程可能同时尝试写日志到共享文件或缓冲区。如果日志写入操作非常短且竞争不激烈,使用 Spin Lock 会更高效。
场景描述:
- 系统中有大量的工作线程需要记录日志,但日志的写入操作非常简单。
- 多线程环境中,每个线程记录日志时仅需在日志缓冲区加锁一次,操作非常快速。
- 锁竞争不严重:每个线程的日志写入请求相对较少,竞争的概率不高。
为什么适合:
- 低竞争:由于日志的写入操作相对简单且快速,锁的竞争并不频繁,线程通常可以迅速获得锁进行日志写入。
- 高并发环境:即使有多个线程同时执行,Spin Lock 也能减少线程挂起和唤醒的开销。
代码示例:
std::mutex log_lock;
void logMessage(const std::string& message) {
while (true) {
if (log_lock.try_lock()) {
std::cout << message << std::endl;
log_lock.unlock();
break;
}
}
}
在这种场景下,日志写入操作的快速完成,使得 Spin Lock 适合用于减少锁竞争的开销。
4. 不适合场景:高竞争或需要长时间持有锁的情况
示例:频繁访问全局队列
当多个线程频繁地访问共享的全局队列,并且每次访问队列的操作会占用较长时间时,使用 Spin Lock 就不合适了。
场景描述:
- 系统中多个线程需要访问全局队列执行出队和入队操作。
- 每次操作队列可能需要比较长时间(例如,调整队列的内部结构),线程需要长时间持有锁。
为什么不适合:
- 长时间持有锁:由于每次操作队列需要较长时间,线程会长时间持有锁,导致其他线程长时间自旋,浪费大量的 CPU 资源。
- 高竞争:多个线程对队列的访问冲突较多,竞争激烈,导致大量线程在等待锁时消耗过多的 CPU 资源。
不适用的优化:
- 互斥锁(mutex):使用互斥锁可以让线程在等待锁时挂起,而不是自旋,避免 CPU 资源浪费。
代码示例:
std::queue<int> globalQueue;
std::mutex queue_lock;
void processQueue() {
while (true) {
if (queue_lock.try_lock()) {
// 假设这里的队列处理时间比较长
int item = globalQueue.front();
globalQueue.pop();
// 模拟长时间处理
std::this_thread::sleep_for(std::chrono::milliseconds(100));
queue_lock.unlock();
break;
}
}
}
由于 std::this_thread::sleep_for 和队列操作可能导致线程在临界区中占用锁较长时间,使用 Spin Lock 将会导致大量的自旋等待,浪费 CPU 资源。
三、通用代码实现
3.1、文件整理
和体系结构无关的代码如下:
(1)include/linux/spinlock_types.h。这个头文件定义了通用spin lock的基本的数据结构(例如spinlock_t)和如何初始化的接口(DEFINE_SPINLOCK)。这里的“通用”是指不论SMP还是UP都通用的那些定义。
(2)include/linux/spinlock_types_up.h。这个头文件不应该直接include,在include/linux/spinlock_types.h文件会根据系统的配置(是否SMP)include相关的头文件,如果UP则会include该头文件。这个头文定义UP系统中和spin lock的基本的数据结构和如何初始化的接口。当然,对于non-debug版本而言,大部分struct都是empty的。
(3)include/linux/spinlock.h。这个头文件定义了通用spin lock的接口函数声明,例如spin_lock、spin_unlock等,使用spin lock模块接口API的驱动模块或者其他内核模块都需要include这个头文件。
(4)include/linux/spinlock_up.h。这个头文件不应该直接include,在include/linux/spinlock.h文件会根据系统的配置(是否SMP)include相关的头文件。这个头文件是debug版本的spin lock需要的。
(5)include/linux/spinlock_api_up.h。同上,只不过这个头文件是non-debug版本的spin lock需要的
(6)linux/spinlock_api_smp.h。SMP上的spin lock模块的接口声明
(7)kernel/locking/spinlock.c。SMP上的spin lock实现。
3.2、数据结构
首先定义一个spinlock_t的数据类型,其本质上是一个整数值(对该数值的操作需要保证原子性),该数值表示spin lock是否可用。初始化的时候被设定为1。当thread想要持有锁的时候调用spin_lock函数,该函数将spin lock那个整数值减去1,然后进行判断,如果等于0,表示可以获取spin lock,如果是负数,则说明其他thread的持有该锁,本thread需要spin。
内核中的spinlock_t的数据类型定义如下:
include/linux/spinlock_types.h
/* Non PREEMPT_RT kernels map spinlock to raw_spinlock */
typedef struct spinlock {
union {
struct raw_spinlock rlock;
#ifdef CONFIG_DEBUG_LOCK_ALLOC
# define LOCK_PADSIZE (offsetof(struct raw_spinlock, dep_map))
struct {
u8 __padding[LOCK_PADSIZE];
struct lockdep_map dep_map;
};
#endif
};
} spinlock_t;
通用(适用于各种arch)的spin lock使用spinlock_t这样的type name,各种arch定义自己的struct raw_spinlock。听起来不错的主意和命名方式,直到linux realtime tree(PREEMPT_RT)提出对spinlock的挑战。
real time linux是一个试图将linux kernel增加硬实时性能的一个分支,多年来,很多来自realtime branch的特性被merge到了mainline上,例如:高精度timer、中断线程化等等。
realtime tree希望可以对现存的spinlock进行分类:一种是在realtime kernel中可以睡眠的spinlock,另外一种就是在任何情况下都不可以睡眠的spinlock。
spin lock的命名规范定义如下:
(1)spinlock,在rt linux(配置了PREEMPT_RT)的时候可能会被抢占(实际底层可能是使用支持PI(优先级翻转)的mutext)。
(2)raw_spinlock,即便是配置了PREEMPT_RT也要顽强的spin
(3)arch_spinlock,spin lock是和architecture相关的,arch_spinlock是architecture相关的实现
3.3、spin lock接口API
在 Linux 中,spinlock 是一种用于保护共享资源的锁,通常用于临界区保护,特别是在中断上下文或不允许进程挂起的情况下。Linux 提供了多种与 spinlock 相关的 API。下面是一些常用的 spinlock API 接口。
| API 函数 | 描述 |
|---|---|
spin_lock(spinlock_t *lock) | 获取自旋锁,如果锁已经被其他线程占用,则当前线程会自旋等待。 |
spin_unlock(spinlock_t *lock) | 释放自旋锁。 |
spin_lock_irq(spinlock_t *lock) | 获取自旋锁并禁用本地中断。防止在持有锁期间发生中断。 |
spin_unlock_irq(spinlock_t *lock) | 释放自旋锁并恢复本地中断。 |
spin_lock_irqsave(spinlock_t *lock, unsigned long flags) | 获取自旋锁并保存中断标志,禁用中断。 |
spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags) | 释放自旋锁并恢复之前保存的中断标志。 |
spin_trylock(spinlock_t *lock) | 尝试获取自旋锁,如果锁已被其他线程占用,返回 false;如果成功,返回 true。 |
spin_is_locked(spinlock_t *lock) | 检查自旋锁是否已被锁定,返回 true 或 false。 |
详细描述:
-
spin_lock和spin_unlock:spin_lock用于获取自旋锁,锁被占用时线程会进行自旋等待,直到锁可用。spin_unlock用于释放自旋锁,允许其他线程获取该锁。
-
spin_lock_irq和spin_unlock_irq:spin_lock_irq和spin_unlock_irq用于在获取和释放锁的同时禁用和恢复本地中断。它们适用于需要禁用中断的情形,例如在内核中进行保护共享数据时。
-
spin_lock_irqsave和spin_unlock_irqrestore:- 这些函数类似于
spin_lock_irq和spin_unlock_irq,但它们可以保存中断的状态,以便在释放锁后恢复中断状态。这种机制可以确保中断在执行临界区代码期间不会干扰,并且在释放锁后恢复到之前的中断状态。
- 这些函数类似于
-
spin_trylock:spin_trylock尝试获取自旋锁,如果锁已经被占用,则不会进行阻塞等待,而是立即返回false。如果获取锁成功,则返回true。这通常用于非阻塞性锁定场景。
-
spin_is_locked:spin_is_locked检查自旋锁是否已被锁定。它通常用于调试或某些特定的锁检查场景。
spin_lock的代码如下:
tools/virtio/linux/spinlock.h
static inline void spin_lock(spinlock_t *lock)
{
int ret = pthread_spin_lock(lock);
assert(!ret);
}
睡眠的spin lock,也就是是raw_spin_lock,代码如下:
#define raw_spin_lock(lock) _raw_spin_lock(lock)
UP中的实现:
#define _raw_spin_lock(lock) __LOCK(lock)
#define __LOCK(lock) \
do { preempt_disable(); ___LOCK(lock); } while (0)
SMP的实现:
void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
__raw_spin_lock(lock);
}
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
preempt_disable();
spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}
四、ARM平台的细节
代码位于arch/arm/include/asm/spinlock.h和spinlock_type.h,和通用代码类似,spinlock_type.h定义ARM相关的spin lock定义以及初始化相关的宏;spinlock.h中包括了各种具体的实现。
4.1、arch_spinlock_t
ARM平台中的arch_spinlock_t定义如下(little endian):
arch/arm/include/asm/spinlock_types.h
typedef struct {
union {
u32 slock;
struct __raw_tickets {
#ifdef __ARMEB__
u16 next;
u16 owner;
#else
u16 owner;
u16 next;
#endif
} tickets;
};
} arch_spinlock_t;
详细解析说明:
1. union 类型
union {
u32 slock;
struct __raw_tickets {
u16 next;
u16 owner;
} tickets;
};
union允许多个成员共享相同的内存空间,但每次只能使用一个成员。这里,arch_spinlock_t结构体通过union提供了两种不同的自旋锁实现:u32 slock:一个 32 位的简单自旋锁,通常用于基于原子操作的锁机制。struct __raw_tickets:一个票据锁(ticket lock)结构,用于更公平的锁机制,避免线程饥饿。
2. u32 slock
slock是一个 32 位的无符号整数,表示一个基础的自旋锁。它可以通过原子操作(如cmpxchg或atomic操作)来实现对共享资源的互斥访问。- 这种锁机制简单,但可能导致某些线程饥饿,即某些线程长时间无法获取到锁。
3. __raw_tickets 结构体
struct __raw_tickets {
#ifdef __ARMEB__
u16 next;
u16 owner;
#else
u16 owner;
u16 next;
#endif
};
-
__raw_tickets是一种实现票据锁(ticket lock)的方式。票据锁通过一个票号机制确保每个请求锁的线程按照获取锁的顺序依次获取锁,从而避免了优先级反转和线程饥饿的问题。owner:表示当前持有锁的线程的编号。next:表示下一个可以获取锁的线程的编号。
-
字节序(Endianness):使用
#ifdef __ARMEB__判断当前的字节序(大端或小端):- 大端(ARMEB):
next字段在前,owner字段在后。 - 小端:
owner字段在前,next字段在后。
- 大端(ARMEB):
这种字节序的判断确保了在不同平台上数据的正确对齐和访问。
4.2、接口实现
arch_spin_lock,arch/arm/include/asm/spinlock.h
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
unsigned long tmp;
u32 newval;
arch_spinlock_t lockval;
prefetchw(&lock->slock);------------------------(1)
__asm__ __volatile__(
"1: ldrex %0, [%3]\n"-------------------------(2)
" add %1, %0, %4\n"
" strex %2, %1, [%3]\n"------------------------(3)
" teq %2, #0\n"----------------------------(4)
" bne 1b"
: "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
: "cc");
while (lockval.tickets.next != lockval.tickets.owner) {------------(5)
wfe();-------------------------------(6)
lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);------(7)
}
smp_mb();------------------------------(8)
}
(1)和preloading cache相关的操作,主要是为了性能考虑
(2)将slock的值保存在lockval这个临时变量中
(3)将spin lock中的next加一
(4)判断是否有其他的thread插入。参考:Linux同步互斥一
(5)判断当前spin lock的状态,如果是unlocked,那么直接获取到该锁
(6)如果当前spin lock的状态是locked,那么调用wfe进入等待状态。
(7)其他的CPU唤醒了本cpu的执行,说明owner发生了变化,该新的own赋给lockval,然后继续判断spin lock的状态,也就是回到step 5。
(8)memory barrier的操作。
五、ARM举例说明
ARM 架构的自旋锁实现通常依赖于 ARM 的原子操作(比如 ldrex 和 strex)来实现锁的获取和释放。以下是一个简化的 ARM 自旋锁实现的示例,展示了如何在 ARM 环境下使用自旋锁。
5.1、ARM 自旋锁的实现
1. 自旋锁结构体定义
typedef struct {
volatile unsigned int lock;
} spinlock_t;
在这个实现中,lock 是一个 volatile 类型的变量,volatile 关键字告诉编译器,不要优化这个变量,因为它的值可能在程序的其他部分或硬件中被更改。
2. 获取自旋锁(spin_lock)
void spin_lock(spinlock_t *lock)
{
unsigned long flags;
// 禁用中断,防止在获取锁时被中断打断
local_irq_save(flags);
while (__sync_lock_test_and_set(&lock->lock, 1)) {
// 如果锁已经被占用,继续自旋等待
cpu_relax(); // 提高效率,减少空转消耗
}
}
在上面的代码中,__sync_lock_test_and_set 是 GCC 提供的一个原子操作,它首先将 lock->lock 的值设置为 1(表示加锁),并返回原来的值。如果原来的值是 0,表示成功获取到锁;如果返回值是 1,表示锁已经被其他线程持有,因此需要继续自旋等待。
local_irq_save(flags)禁用了本地中断,防止在获取锁时被中断打断。cpu_relax()是一个提示,告诉处理器可以降低自旋时的空转消耗,具体实现依赖于 CPU。
3. 释放自旋锁(spin_unlock)
void spin_unlock(spinlock_t *lock)
{
// 释放锁
lock->lock = 0;
// 恢复之前的中断状态
local_irq_restore(flags);
}
在释放锁时,lock->lock = 0 将锁的状态恢复为未占用状态。然后,local_irq_restore(flags) 恢复之前保存的中断状态。
4. 使用自旋锁的示例
spinlock_t lock = { 0 };
void example_function(void)
{
spin_lock(&lock);
// 临界区:执行对共享资源的操作
spin_unlock(&lock);
}
在这个例子中,example_function 函数通过 spin_lock 和 spin_unlock 来保护一个临界区,确保只有一个线程可以在同一时间内执行临界区中的操作。
5.2、ARM 自旋锁的原子操作实现
ARM 提供了一些原子操作指令(如 ldrex 和 strex)来支持自旋锁的实现。自旋锁的实现实际上就是利用这些原子操作来保证对锁变量的操作是原子的。
在 ARM 上,__sync_lock_test_and_set 操作可以通过以下原子指令实现:
ldrex r0, [r1] // 加载锁的值到 r0
strex r2, r0, [r1] // 将 r0 的值写回到锁,如果成功,r2 为 0
ARM 版本的自旋锁实现(更底层)
void spin_lock(spinlock_t *lock)
{
unsigned long flags;
unsigned int tmp;
// 禁用本地中断
local_irq_save(flags);
do {
// 使用 ARM 原子指令进行自旋
tmp = lock->lock;
if (tmp == 0) {
// 尝试加锁,锁的状态为 0,表示没有被占用
lock->lock = 1;
} else {
// 锁已经被占用,继续自旋
cpu_relax();
}
} while (lock->lock != 1); // 继续循环直到成功加锁
}
这个版本通过检查 lock->lock 的值,并使用原子指令来保证只有一个线程能够成功获取锁。
总结:
- 自旋锁在 ARM 上的基本实现依赖于原子操作,确保对锁变量的操作是不可分割的。
spin_lock在加锁时会阻止其他线程访问临界区,直到它成功获取锁。spin_unlock释放锁后,其他等待的线程可以继续尝试获取锁。- ARM 上的自旋锁实现还可能依赖于 CPU 提供的原子操作(如
ldrex和strex)来更高效地处理锁的获取和释放。

被折叠的 条评论
为什么被折叠?



