内核自旋锁

自旋锁用于处理器之间的互斥,适合保护很短的临界区,并且不允许在临界区睡眠。申请自旋锁的时候,如果自旋锁被其他处理器占有,本处理器自旋等待(也称为忙等待)。

进程、软中断和硬中断都可以使用自旋锁。

内核的自旋锁是排队自旋锁(queuedspinlock,也称为"FIFO ticket spinlock",票号锁),算法类似于银行柜台的排队叫号。

(1)锁拥有排队号(也叫票号,ticket)服务号,服务号是当前占有锁的进程的排队号。

(2)每个进程申请锁的时候,首先申请一个排队号,然后轮询锁的服务号是否等于自己的排队号,如果等于,表示自己占有锁,可以进入临界区,否则继续轮询。

(3)当进程释放锁时,把服务号加1,下一个进程看到服多号等于自己的排队号,退出自旋,进入临界区。


为什么中断中可以使用自旋锁?

中断中是不可以使用mutex的,因为mutex会引起进程sleep中断上下文中是不允许**sleep**。而spinlock中不会sleep,所以中断中可以使用它。

数据结构

typedef struct raw_spinlock {
	arch_spinlock_t raw_lock;
} raw_spinlock_t;

typedef struct spinlock {
	struct raw_spinlock rlock;
} spinlock_t;

spinlock_t-> raw_spinlock -> arch_spinlock_t,最后使用的实际是各处理器架构的arch_spinlock_t

操作函数

初始化

定义并且初始化静态自旋锁的方法如下:


DEFINE_SPINLOCK(x)


在运行时动态初始化自旋锁的方法如下:


spin_lock_init(x)


申请自旋锁

申请自旋锁前,为什么要先禁止硬中断或者软中断

申请自旋锁的函数如下。

(1)void spin_lock(spinlock_t *lock);

申请自旋锁,如果锁被其他处理器占有,当前处理器自旋等待。

(2)void spin_lock_bh(spinlock_t *lock);

申请自旋锁,并且禁止当前处理器的软中断。

(3)void spin_lock_irq(spinlock_t*lock);

申请自旋锁,并且禁止当前处理器的硬中断。

(4) spin_lock_irqsave(lock, flags);

申请自旋锁,保存当前处理器的硬中断状态,并且禁止当前处理器的硬中断。

(5) int spin_trylock(spinlock_t *lock);

申请自旋锁,如果申请成功,返回1;如果锁被其他处理器器占有,当前处理器不等待,立即返回0。

释放自旋锁

释放自旋锁的函数如下。

(1)void spin_unlock(spinlock_t*lock);

(2)void spin_unlock_bh(spinlock_t*lock);

释放自旋锁,并且开启当前处理器的软中断。

(3)void spin_unlock_irq(spinlock_t*lock);

释放自旋锁,并且开启当前处理器的硬中断。

(4)void spin_unlock_irqrestore(spinlock_t *lock, unsigned longflags);

释放自旋锁,并且恢复当前处理器的硬中断状态。

ARM64中spinlock实现

数据结构

typedef struct {
#ifdef __AARCH64EB__ // big endian
	u16 next;
	u16 owner;
#else                // little endian
	u16 owner;
	u16 next;
#endif
} __aligned(4) arch_spinlock_t;

<font style="color:rgb(64, 64, 64);">arch_spinlock_t</font> 是ARM64架构下的自旋锁数据结构。它包含两个16位的字段:

  • <font style="color:rgb(64, 64, 64);">owner</font>:表示当前持有锁的进程的票号。
  • <font style="color:rgb(64, 64, 64);">next</font>:表示下一个等待获取锁的进程的票号。

在ARM64架构中,<font style="color:rgb(64, 64, 64);">spinlock</font> 使用了“票号”(ticket)机制来实现公平锁。每个尝试获取锁的进程都会获得一个唯一的票号,各进程按票号的顺序依次获取锁。

函数实现

arch_spin_lock

spin_lock() -> raw_spin_lock() -> _raw_spin_lock() -> __raw_spin_lock() -> _ > do_raw_spin_lock() -> arch_spin_lock()

static inline void arch_spin_lock(arch_spinlock_t *lock)
{
	unsigned int tmp;
	arch_spinlock_t lockval, newval;

	asm volatile(
	/* Atomically increment the next ticket. */
	ARM64_LSE_ATOMIC_INSN(
	/* LL/SC */
"	prfm	pstl1strm, %3\n"
"1:	ldaxr	%w0, %3\n"
"	add	%w1, %w0, %w5\n"
"	stxr	%w2, %w1, %3\n"
"	cbnz	%w2, 1b\n",
	/* LSE atomics */
"	mov	%w2, %w5\n"
"	ldadda	%w2, %w0, %3\n"
	__nops(3)
	)

	/* Did we get the lock? */
"	eor	%w1, %w0, %w0, ror #16\n"
"	cbz	%w1, 3f\n"
	/*
	 * No: spin on the owner. Send a local event to avoid missing an
	 * unlock before the exclusive load.
	 */
"	sevl\n"
"2:	wfe\n"
"	ldaxrh	%w2, %4\n"
"	eor	%w1, %w2, %w0, lsr #16\n"
"	cbnz	%w1, 2b\n"
	/* We got the lock. Critical section starts here. */
"3:"
	: "=&r" (lockval), "=&r" (newval), "=&r" (tmp), "+Q" (*lock)
	: "Q" (lock->owner), "I" (1 << TICKET_SHIFT)
	: "memory");
}

<font style="color:rgb(64, 64, 64);">arch_spin_lock</font> 函数用于获取自旋锁。<font style="color:rgb(64, 64, 64);">spinlock</font> 实现使用了票号机制来保证锁的公平性。通过原子操作和忙等待的方式,确保在多核系统中只有一个CPU核心可以进入临界区。<font style="color:rgb(64, 64, 64);">spinlock</font> 的实现依赖于ARM64的原子指令和事件机制,以高效地实现锁的获取和释放。

它的主要步骤如下:

  1. 原子地增加**** **<font style="color:rgb(64, 64, 64);">next</font>** ****票号
    • 使用ARM64 LSE(Large System Extensions)原子指令LL/SC(Load-Linked/Store-Conditional)指令实现原子地增加**next**票号。
      • LL/SC指令:使用 <font style="color:rgb(64, 64, 64);">ldaxr</font><font style="color:rgb(64, 64, 64);">stxr</font> 指令(Load-Exclusive 和 Store-Exclusive)来实现原子操作增加<font style="color:rgb(64, 64, 64);">next</font>票号。
      • 如果系统支持 LSE(Large System Extensions),则使用 <font style="color:rgb(64, 64, 64);">ldadda</font> 指令来原子地增加 <font style="color:rgb(64, 64, 64);">next</font> 票号。
    • 下一个排队的进程申请的排队号就是增加后的next票号。
  2. 检查是否获取到锁
    • 通过 <font style="color:rgb(64, 64, 64);">eor</font><font style="color:rgb(64, 64, 64);">cbz</font> 指令检查当前 **<font style="color:rgb(64, 64, 64);">owner</font>** (服务号)是否等于自己持有的票号(增加前的<font style="color:rgb(64, 64, 64);">next</font>票号)。如果相等,则表示获取到了锁,跳转到标签 <font style="color:rgb(64, 64, 64);">3</font>进入临界区
    • 如果不相等,则表示锁已经被其他核心持有,当前核心需要自旋等待(忙等待)
  3. 忙等待
    • sevl (Set Event Local) 指令的功能是发送一个本地事件,避免错过其它处理器释放自旋锁时发送的事件。
    • wfe (Wait For Event) 让CPU进入低功耗等待状态,等待其它处理器 sev 触发事件。
      • **wfe****不仅仅受 ****sev**唤醒还可以被内存访问(如写入某个地址)间接触发
      • arch_spin_unlock中并没有触发sev,但是将owner增加时,也能够唤醒执行wfe的处理器。
    • 使用 <font style="color:rgb(64, 64, 64);">ldaxrh</font><font style="color:rgb(64, 64, 64);">eor</font>指令不断检查 <font style="color:rgb(64, 64, 64);">owner</font> 字段,直到 <font style="color:rgb(64, 64, 64);">owner</font> 等于自己持有的票号,表示锁已经被释放,当前核心可以获取锁。
  4. 进入临界区
    • 当获取到锁后,代码跳转到标签 <font style="color:rgb(64, 64, 64);">3</font>,进入临界区。

arch_spin_unlock

spin_unlock() -> raw_spin_unlock() -> _raw_sspin_unlock() -> __raw_spin_unlock() -> do_raw_spin_unlock() -> arch_spin_unlock()

static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
	unsigned long tmp;

	asm volatile(ARM64_LSE_ATOMIC_INSN(
	/* LL/SC */
	"	ldrh	%w1, %0\n"
	"	add	%w1, %w1, #1\n"
	"	stlrh	%w1, %0",
	/* LSE atomics */
	"	mov	%w1, #1\n"
	"	staddlh	%w1, %0\n"
	__nops(1))
	: "=Q" (lock->owner), "=&r" (tmp)
	:
	: "memory");
}

<font style="color:rgb(64, 64, 64);">arch_spin_unlock</font> 的功能是释放自旋锁,具体步骤如下:

  1. 增加**** **<font style="color:rgb(64, 64, 64);">owner</font>** ****字段
    • <font style="color:rgb(64, 64, 64);">lock->owner</font> 的值加 1,表示当前持有锁的 CPU 核心已经释放了锁。
    • 如果系统支持 LSE,则使用原子指令 <font style="color:rgb(64, 64, 64);">staddlh</font> 直接完成加 1 操作。
    • 如果系统不支持 LSE,则使用 LL/SC 指令序列(<font style="color:rgb(64, 64, 64);">ldrh</font><font style="color:rgb(64, 64, 64);">add</font><font style="color:rgb(64, 64, 64);">stlrh</font>)完成加 1 操作。
  2. 内存同步
    • 使用 <font style="color:rgb(64, 64, 64);">stlrh</font><font style="color:rgb(64, 64, 64);">staddlh</font> 指令确保之前的加载和存储操作按顺序完成,避免内存访问乱序。
  3. 通知其他 CPU 核心
    • 通过增加 <font style="color:rgb(64, 64, 64);">owner</font> 字段的值,通知其他正在等待锁的 CPU 核心,锁已经被释放,它们可以尝试获取锁。

工作原理

为了更好地理解 ARM64 架构下 票号锁 (ticket lock)的工作原理,我们可以通过图示来说明 <font style="color:rgb(64, 64, 64);">owner</font><font style="color:rgb(64, 64, 64);">next</font> 的变化过程。以下是详细的图示和解释。


1. 初始状态

在初始状态下,<font style="color:rgb(64, 64, 64);">owner</font><font style="color:rgb(64, 64, 64);">next</font> 的值均为 0,表示锁未被任何 CPU 核心持有,且没有 CPU 核心在等待锁。

+-------------------+-------------------+
|      owner        |       next        |
+-------------------+-------------------+
|         0         |         0         |
+-------------------+-------------------+
  • **<font style="color:rgb(64, 64, 64);">owner = 0</font>**:表示当前持有锁的票号为 0。
  • **<font style="color:rgb(64, 64, 64);">next = 0</font>**:表示下一个等待锁的票号将从 0 开始分配。

2. 第一次执行 <font style="color:rgb(64, 64, 64);">arch_spin_lock</font>

当一个 CPU 核心(假设为 CPU 0)第一次调用 <font style="color:rgb(64, 64, 64);">arch_spin_lock</font> 时,会发生以下变化:

2.1 原子地增加 <font style="color:rgb(64, 64, 64);">next</font>
  • <font style="color:rgb(64, 64, 64);">next</font> 的值从 0 增加到 1。
  • CPU 0 获取的票号为 <font style="color:rgb(64, 64, 64);">next</font> 的旧值,即 0。
+-------------------+-------------------+
|      owner        |       next        |
+-------------------+-------------------+
|         0         |         1         |
+-------------------+-------------------+
  • **<font style="color:rgb(64, 64, 64);">owner = 0</font>**:未变化。
  • **<font style="color:rgb(64, 64, 64);">next = 1</font>**:已增加,表示下一个等待锁的票号将从 1 开始分配。
2.2 检查是否获取到锁
  • CPU 0 的票号为 0,与 <font style="color:rgb(64, 64, 64);">owner</font> 的值相等,因此 CPU 0 成功获取锁。

3. 第一次执行 <font style="color:rgb(64, 64, 64);">arch_spin_unlock</font>

当 CPU 0 调用 <font style="color:rgb(64, 64, 64);">arch_spin_unlock</font> 释放锁时,会发生以下变化:

3.1 原子地增加 <font style="color:rgb(64, 64, 64);">owner</font>
  • <font style="color:rgb(64, 64, 64);">owner</font> 的值从 0 增加到 1。
+-------------------+-------------------+
|      owner        |       next        |
+-------------------+-------------------+
|         1         |         1         |
+-------------------+-------------------+
  • **<font style="color:rgb(64, 64, 64);">owner = 1</font>**:已增加,表示锁已被释放,下一个等待锁的 CPU 核心可以获取锁。
  • **<font style="color:rgb(64, 64, 64);">next = 1</font>**:未变化。

4. 第二次执行 <font style="color:rgb(64, 64, 64);">arch_spin_lock</font>

当另一个 CPU 核心(假设为 CPU 1)调用 <font style="color:rgb(64, 64, 64);">arch_spin_lock</font> 时,会发生以下变化:

4.1 原子地增加 <font style="color:rgb(64, 64, 64);">next</font>
  • <font style="color:rgb(64, 64, 64);">next</font> 的值从 1 增加到 2。
  • CPU 1 获取的票号为 <font style="color:rgb(64, 64, 64);">next</font> 的旧值,即 1。
+-------------------+-------------------+
|      owner        |       next        |
+-------------------+-------------------+
|         1         |         2         |
+-------------------+-------------------+
  • **<font style="color:rgb(64, 64, 64);">owner = 1</font>**:未变化。
  • **<font style="color:rgb(64, 64, 64);">next = 2</font>**:已增加,表示下一个等待锁的票号将从 2 开始分配。
4.2 检查是否获取到锁
  • CPU 1 的票号为 1,与 <font style="color:rgb(64, 64, 64);">owner</font> 的值相等,因此 CPU 1 成功获取锁。

5. 第二次执行 <font style="color:rgb(64, 64, 64);">arch_spin_unlock</font>

当 CPU 1 调用 <font style="color:rgb(64, 64, 64);">arch_spin_unlock</font> 释放锁时,会发生以下变化:

5.1 原子地增加 <font style="color:rgb(64, 64, 64);">owner</font>
  • <font style="color:rgb(64, 64, 64);">owner</font> 的值从 1 增加到 2。
+-------------------+-------------------+
|      owner        |       next        |
+-------------------+-------------------+
|         2         |         2         |
+-------------------+-------------------+
  • **<font style="color:rgb(64, 64, 64);">owner = 2</font>**:已增加,表示锁已被释放,下一个等待锁的 CPU 核心可以获取锁。
  • **<font style="color:rgb(64, 64, 64);">next = 2</font>**:未变化。

6. 总结图示

初始状态
+-------------------+-------------------+
|      owner        |       next        |
+-------------------+-------------------+
|         0         |         0         |
+-------------------+-------------------+
CPU 0 获取锁
+-------------------+-------------------+
|      owner        |       next        |
+-------------------+-------------------+
|         0         |         1         |
+-------------------+-------------------+
CPU 0 释放锁
+-------------------+-------------------+
|      owner        |       next        |
+-------------------+-------------------+
|         1         |         1         |
+-------------------+-------------------+
CPU 1 获取锁
+-------------------+-------------------+
|      owner        |       next        |
+-------------------+-------------------+
|         1         |         2         |
+-------------------+-------------------+
CPU 1 释放锁
+-------------------+-------------------+
|      owner        |       next        |
+-------------------+-------------------+
|         2         |         2         |
+-------------------+-------------------+

7. 票号锁的工作原理

通过上述图示和过程,可以看出票号锁的工作原理:

  1. **<font style="color:rgb(64, 64, 64);">next</font>**:表示下一个等待锁的票号,每次有 CPU 核心尝试获取锁时,<font style="color:rgb(64, 64, 64);">next</font> 的值会原子地增加。
  2. **<font style="color:rgb(64, 64, 64);">owner</font>**:表示当前持有锁的票号,每次有 CPU 核心释放锁时,<font style="color:rgb(64, 64, 64);">owner</font> 的值会原子地增加。
  3. 公平性:CPU 核心按票号的顺序依次获取锁,确保每个 CPU 核心都能公平地获取锁。

这种机制避免了某些 CPU 核心长时间无法获取锁的情况,实现了公平的锁获取策略。

注意事项

一般使用spin_lock时,需要禁止内核抢占以及中断。内核实现spin_lock时,已经进行了preempt_disable禁止内核抢占。如果要禁止硬中断,需要使用spin_lock_irq。禁止软中断,使用spin_lock_bh

禁止内核抢占、中断的原因

  • 防止死锁:避免持有锁的任务被抢占,导致其他任务无法获取锁。
  • 减少锁持有时间:确保自旋锁的持有时间尽可能短。
  • 保证数据一致性:防止临界区内的操作被中断或抢占。

内核抢占的概念

内核抢占是指在内核模式下,当前任务可以被更高优先级的任务抢占,即使当前任务没有主动让出 CPU。

自旋锁、原子变量、互斥量比较

特性自旋锁(spinlock_t)互斥锁(mutex)原子变量(atomic_t)
等待方式忙等待阻塞等待无等待
是否可睡眠不可睡眠可睡眠无睡眠
适用上下文中断上下文、进程上下文进程上下文中断上下文、进程上下文
开销低(短临界区)高(上下文切换)极低
适用场景短临界区、锁竞争不激烈长临界区、锁竞争激烈简单操作、无锁数据结构
复杂性中等

用户空间自旋锁

linux用户空间一般使用posix 线程库(pthread)中的自旋锁。它的实现原理、使用方法与内核自旋锁类似。

pthread中的spinlock既可以用于线程间通信,也可以用于进程间。因为linux不区分进程和线程。

用于进程间的spinlock需要依赖共享内存以及PTHREAD_PROCESS_SHARED

线程间spinlock:

// gcc -o posix_spinlock_test posix_spinlock_test.c -pthread
#include <stdio.h>
#include <pthread.h>

pthread_spinlock_t lock; // 自旋锁

void *thread_func(void *arg) {
    pthread_spin_lock(&lock); // 获取锁
    printf("Thread %ld acquired the lock\n", (long)pthread_self());
    // 模拟临界区
    for (int i = 0; i < 1000000; i++) {}
    printf("Thread %ld released the lock\n", (long)pthread_self());
    pthread_spin_unlock(&lock); // 释放锁
    return NULL;
}

int main() {
    pthread_t thread1, thread2;

    // 初始化自旋锁
    pthread_spin_init(&lock, PTHREAD_PROCESS_PRIVATE);

    // 创建两个线程
    pthread_create(&thread1, NULL, thread_func, NULL);
    pthread_create(&thread2, NULL, thread_func, NULL);

    // 等待线程结束
    pthread_join(thread1, NULL);
    pthread_join(thread2, NULL);

    // 销毁自旋锁
    pthread_spin_destroy(&lock);

    return 0;
}

进程间spinlock:

// gcc -o posix_spinlock_processA_test posix_spinlock_processA_test.c -pthread -lrt
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

#define SHM_NAME "/spinlock_shm"

typedef struct {
    pthread_spinlock_t spinlock;
    int shared_counter;
} shared_data_t;

int main() {
    // 创建共享内存
    int shm_fd = shm_open(SHM_NAME, O_CREAT | O_RDWR, 0666);
    if (shm_fd < 0) {
        perror("shm_open");
        exit(EXIT_FAILURE);
    }

    // 调整共享内存大小
    ftruncate(shm_fd, sizeof(shared_data_t));

    // 映射共享内存
    shared_data_t *data = mmap(NULL, sizeof(shared_data_t), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
    if (data == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    // 初始化自旋锁
    pthread_spin_init(&data->spinlock, PTHREAD_PROCESS_SHARED);
    data->shared_counter = 0;

    printf("Process A: Initialized spinlock and shared counter.\n");

    // 使用自旋锁
    for (int i = 0; i < 5; i++) {
        pthread_spin_lock(&data->spinlock);
        data->shared_counter++;
        printf("Process A: Incremented counter to %d\n", data->shared_counter);
        pthread_spin_unlock(&data->spinlock);
        sleep(1);
    }

    // 等待用户确认退出
    getchar();

    // 销毁自旋锁并清理共享内存
    pthread_spin_destroy(&data->spinlock);
    munmap(data, sizeof(shared_data_t));
    shm_unlink(SHM_NAME);

    return 0;
}

// gcc -o posix_spinlock_processB_test posix_spinlock_processB_test.c -pthread -lrt
#include <stdio.h>
#include <stdlib.h>
#include <pthread.h>
#include <fcntl.h>
#include <sys/mman.h>
#include <unistd.h>

#define SHM_NAME "/spinlock_shm"

typedef struct {
    pthread_spinlock_t spinlock;
    int shared_counter;
} shared_data_t;

int main() {
    // 打开已经存在的共享内存
    int shm_fd = shm_open(SHM_NAME, O_RDWR, 0666);
    if (shm_fd < 0) {
        perror("shm_open");
        exit(EXIT_FAILURE);
    }

    // 映射共享内存
    shared_data_t *data = mmap(NULL, sizeof(shared_data_t), PROT_READ | PROT_WRITE, MAP_SHARED, shm_fd, 0);
    if (data == MAP_FAILED) {
        perror("mmap");
        exit(EXIT_FAILURE);
    }

    printf("Process B: Attached to shared memory.\n");

    // 使用自旋锁
    for (int i = 0; i < 5; i++) {
        pthread_spin_lock(&data->spinlock);
        data->shared_counter++;
        printf("Process B: Incremented counter to %d\n", data->shared_counter);
        pthread_spin_unlock(&data->spinlock);
        sleep(1);
    }

    // 清理共享内存映射
    munmap(data, sizeof(shared_data_t));

    return 0;
}

参考资料

  1. Professional Linux Kernel Architecture,Wolfgang Mauerer
  2. Linux内核深度解析,余华兵
  3. Linux设备驱动开发详解,宋宝华
  4. linux kernel 4.12
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值