1.概述
1.1.自旋锁的由来
自旋锁最初是为了SMP系统设计的,实现了在多处理器情况下保护临界区,所以在SMP系统中,自旋锁的实现是完整的,实现了真正的自旋(忙)等待。但是对于UP(单处理器)系统,自旋锁并没有自旋,而是通过关闭系统抢占来保护临界区。
1.2.自旋锁的目的
自旋锁的实现是为了保护一段短小的临界区操作代码,保证这个临界区的操作是原子的,从而避免并发的竞争风险。在内核中,可以看到许多内核数据结构中都嵌入了类型为spinlock_t
的变量,这些变量用于保证访问共享数据结构的操作是原子的,即将临界区的并发访问串行化。
在SMP系统中,如果内核控制路径发现可以获取自旋锁,就获取锁访问临界区。相反,如果内核控制路径发现锁已被获取,自旋锁会在原地“旋转”,反复执行一条紧凑的循环检测指令,直到锁被释放。获取自旋锁的过程是一个忙等待的过程,所以自旋锁保护的临界区必须小,且操作过程必须短。而在UP系统中,CPU通过关闭内核抢占来获取自旋锁,释放自旋锁时打开内核抢占,没有忙等待的过程,当然UP系统中的临界区也是越短越好,不然内核抢占关闭的时间过长将导致系统的实时性下降。
1.3.自旋锁使用场景
自旋锁用于将临界区的并发访问串行化,因此有必要梳理一下产生并发操作的场景。
(1)中断
假如线程正在访问临界区,这时中断产生了,中断例程被执行,如果在中断例程中访问了同一个临界区,临界区的原子性就被打破。如果某个临界区都能被线程和中断例程访问,那么就必须用自旋锁保护。不同的中断类型(硬件中断和软件中断)对应于不同版本的自旋锁实现,其中包含了中断禁用和开启的代码。但如果没有中断例程访问临界区,可不考虑中断带来的并发风险。
(2)内核抢占
在2.6以后的内核中,支持内核抢占,并且是可配置的。这使UP系统和SMP类似,会出现内核态下的并发。这种情况下进入临界区就需要避免因抢占造成的并发,所以解决的方法是获取自旋锁的时候禁用内核抢占(preempt_disable),在释放自旋锁时开启内核抢占(preempt_enable,注意此时会执行一次抢占调度)。内核不支持抢占时,自旋锁退化为空操作。
(3)SMP系统
在SMP系统中,多个物理处理器同时工作,存在多个CPU同时访问临界区的可能。这样就需要在内存中加一个标志,每个需要进入临界区的代码都必须检查这个标志,看是否有CPU已经在临界区中。检查标志和设置标志的代码也必须保证原子性,这通常和体系结构相关,由具体的硬件实现,如arm V7指令集的STREX LDREX CLREX
,可实现内存的独占性访问。
2.代码阅读
Linux内核和自旋锁相关的源码文件如下:
(1)include/linux/spinlock_types.h
包含了spinlock通用数据类型及spinlock初始化函数。
(2)include/linux/spinlock_types_up.h
包含了up系统上自旋锁的数据类型及初始化操作接口。如果没有定义SMP宏定义CONFIG_SMP
,则将spinlock_types_up.h头文件包含到spinlock_types.h文件中,即up系统中使用spinlock_types_up.h头文件。内核中其他模块不应该包含此头文件。
(3)include/linux/spinlock.h
包含了通用的spinlock操作接口函数,如spin_lock
、spin_unlock
等,内核中需要使用自旋锁的模块可以直接包含此头文件,无需关心具体实现及结构。
(4)kernel/locking/spinlock.c
SMP系统中的自旋锁底层实现。
(5)include/linux/spinlock_up.h
up系统中调试版本的自旋锁(UP-debug version of spinlocks)。如果没有定义SMP宏定义CONFIG_SMP
,则将spinlock_up.h头文件包含到spinlock.h文件中。内核中其他模块不应该包含此头文件。
(6)include/linux/spinlock_api_up.h
up系统中的自旋锁,如果没有定义CONFIG_SMP
及CONFIG_DEBUG_SPINLOCK
,则将spinlock_api_up.h头文件包含到spinlock.h文件中。内核中其他模块不应该包含此头文件。
(7)linux/spinlock_api_smp.h
SMP系统中使用的自旋锁,如果定义CONFIG_SMP
或CONFIG_DEBUG_SPINLOCK
,则将spinlock_api_smp.h头文件包含到spinlock.h文件中。内核中其他模块不应该包含此头文件。可以看出,当配置自旋锁调试选项后,自旋锁将被编译成SMP版本。
2.1.自旋锁数据类型
自旋锁的基本数据类型为spinlock_t
,其内部又包含了跟配置相关的代码。raw_spinlock_t
中的数据类型arch_spinlock_t
分为SMP系统版本和UP系统版本。
include/linux/spinlock_types.h
typedef struct spinlock {
union {
struct raw_spinlock rlock;
// 删除一些不重要的东西
};
} spinlock_t;
typedef struct raw_spinlock {
arch_spinlock_t raw_lock; // 跟具体配置相关
// 删除一些不重要的东西
} raw_spinlock_t;
arch_spinlock_t
SMP系统版本如下,内部定义了一个u32
整数slock
,分为两个u16
整数,获取自旋锁时next
加1
,释放自旋锁时owner
加1
。将slock
分为两个u16
的整数,和自旋锁的公平竞争有关,后续会讲到。
include/linux/spinlock_types.h
typedef struct {
union {
u32 slock;
struct __raw_tickets {
#ifdef __ARMEB__ // arm大端
u16 next;
u16 owner;
#else // arm小端
u16 owner;
u16 next;
#endif
} tickets;
};
} arch_spinlock_t;
arch_spinlock_t
UP系统中的实现分为调试版本和非调试版本,非调试版本定义为空。
include/linux/spinlock_types_up.h
#ifdef CONFIG_DEBUG_SPINLOCK
typedef struct {
volatile unsigned int slock;
} arch_spinlock_t; // 调试版本
#else
typedef struct { } arch_spinlock_t; // 非调试版本
#endif
2.2.自旋锁基本操作函数分析
自旋锁操作函数有好几种类型,但其都是在基本的操作函数上进行扩展得到的。自旋锁基本的操作函数为spin_lock
和spin_unlock
,在SMP系统和UP系统中内部实现又有所不通。spin_lock
用于获取锁,spin_unlock
用于释放锁。下面重点分析一下SMP系统和UP系统中spin_lock
和spin_unlock
的源码实现。
2.2.1.SMP系统中的实现
首先分析一下spin_lock
的实现,spin_lock
的调用流程如下所示,在获取锁之前会禁止内核抢占,底层调用和体系结构相关的函数arch_spin_lock
获取锁。
spin_lock
raw_spin_lock // 宏定义,会被替换为_raw_spin_lock
->_raw_spin_lock // 定义在spinlock.c中的函数
->__raw_spin_lock
preempt_disable // 禁止内核抢占
->spin_acquire // 和自旋锁检查相关的函数,可忽略
LOCK_CONTENDED // 宏定义
->do_raw_spin_lock
->__acquire
->arch_spin_lock // 最底层的执行函数
arch_spin_lock
内部嵌入了汇编代码,与体系结构相关。ldrex
和strex
为arm架构特有汇编指令,具体意义可以参考《ARM Cortex-A Series Programmers Guide》中的18.8节,LDREX
、STREX
和CLREX
三条汇编指令可以实现独占性的内存访问,每条指令的含义如下:
• LDREX (Load Exclusive) performs a load of memory, but also tags the physical address to
be monitored for exclusive access by that core.
• STREX (Store Exclusive) performs a conditional store to memory, succeeding only if the
target location is tagged as being monitored for exclusive access by that core. This
instruction returns the value of 1 in a general purpose register if the store does not take
place, and a value of 0 if the store is successful.
• CLREX (Clear Exclusive) clears any exclusive access tag for that core.
执行LDREX
的CPU会将访问的内存地址标记为独占访问,随后执行STREX
时会检查这个标记,如果标记存在,说明没有CPU访问这个内存地址,则会将数据写到内存并返回0,表示更新成功,如果标记不存在,说明此内存地址的值被修改过,则不会将数据写入内存,此时返回1,表示更新失败,需要重新执行LDREX
和STREX
写入数据。
C语言的内联汇编语法可参考《Linux内核源代码情景分析》1.5节。内联汇编代码可分为4部分,第一部分为指令部,表示执行的汇编指令,arch_spin_lock
函数执行了ldrex
、add
、strex
、teq
、bne
五条汇编指令。第二部分为输出部,表示指令部输出的数据应该保存在那个变量中,按顺序进行标记,arch_spin_lock
函数中%0
对应lockval
变量,%1
对应newval
变量,%2
对应tmp
变量。第三部分为输入部,表示指令部应该从那个变量中读取数据,接着输出部的顺序进行标记,arch_spin_lock
函数中%3
对应slock
变量的地址,%4
对应常数1 << TICKET_SHIFT
。第四部分为损坏部,用于告诉C语言编译器汇编代码改变了那个寄存器的值,让C语言编译器做出相应的处理。下面将对arch_spin_lock
函数中重要的语句添加注释进行解释,同时说明其执行过程。
(1)首先执行ldrex
指令,将自旋锁的数据slock
加载到局部变量lockval
中
(2)执行add
指令,将slock
的next
字段加1,并将结果保存到局部变量newval
中,表示获取自旋锁的线程数量增加了一个,注意lockval
的值并没有变化。
(3)执行strex
指令,将newval
的值更新到自旋锁的数据slock
中,更新的结果保存在tmp
中
(4)执行teq
指令判断tmp
是否等于0
(5)执行bnq
,如果tmp
不等于0,则说明strex
更新自旋锁数据失败,则跳转到标号1处,继续更新
(6)如果更新成功,则进入到while
循环中,判断next
和owner
是否相等,如相等,则说明可以获取自旋锁,如不相等,则说明锁已被占用,需要等待锁被释放。获取锁时next
加1,释放锁时owner
加1,锁被占用时,next
大于owner
,最先等待自旋锁的CPU其next
字段的值越接近owner
字段,因此也越容易获取自旋锁,保证了自旋锁的公平竞争,等待的越久越容易获取自旋锁。
(7)next
和owner
不相等,则需要循环读取owner
的值,并与next
进行比较,判断是否能获取自旋锁
#define TICKET_SHIFT 16
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
unsigned long tmp;
u32 newval;
arch_spinlock_t lockval;
prefetchw(&lock->slock); // 数据预取,加快访问速度
__asm__ __volatile__( // 告诉编译器后续为汇编代码,不要优化
@ 指令部开始
"1: ldrex %0, [%3]\n" @ 将slock的值加载到变量lockval中
" add %1, %0, %4\n" @ 将lockval中的next字段加1并将结果保存到newval中,此时lockval的值没变,和slock相等
" strex %2, %1, [%3]\n" @ 将newval的值更新到slock中,更新结果保存在tmp中
" teq %2, #0\n" @ 判断tmp的值是否为0
" bne 1b" @ 不为0,表示更新失败,则跳转到标号1处继续执行
@ 输出部开始
: "=&r" (lockval), "=&r" (newval), "=&r" (tmp) @ 输出部 %0(lockval) %1(newval) %2(tmp)
@ 输入部开始
: "r" (&lock->slock), "I" (1 << TICKET_SHIFT) @ 输入部 "I"表示常数,1左移16位对应next字段
@ 损坏部开始
: "cc"); @ 损坏部
// 判断next和owner是否相等,相等则跳过while循环,表示获取锁成功,开始访问临界区
while (lockval.tickets.next != lockval.tickets.owner) {
// 不相等则执行wfe指令,此时CPU处于挂起状态,等待事件发生。wfe是arm架构特有指令,目的是降低功耗。
wfe();
// 事件发生后被唤醒,获取owner的值,继续执行while循环判断next和owner是否相等
lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);
}
/* smp_mb是SMP系统中的内存屏障,用来同步多个CPU,防止CPU把smp_mb附近的指令重新排序,进行乱序执行。确保执行完smp_mb()之前的所有指令,才能访问临界区 */
smp_mb();
}
接着分析一下spin_unlock
的实现,spin_unlock
实现较简单,底层调用和体系结构相关的函数arch_spin_unlock
来释放锁。
spin_unlock
raw_spin_unlock // 宏定义,会被替换为_raw_spin_unlock
->_raw_spin_unlock // 定义在spinlock.c中的函数
->__raw_spin_unlock
->spin_release // 和自旋锁检查相关的函数,可忽略
->do_raw_spin_unlock
->arch_spin_unlock // 最底层的执行函数
preempt_enable // 使能内核抢占
arch_spin_unlock
函数最核心的语句是将owner
加1,owner
加1表示释放锁。这里没有使用独占性访问指令,是因为任何时刻只有一个CPU获取锁,也即只有一个CPU释放锁,不会产生竞态现象。
dsb_sev()
执行了两条和arm架构相关的汇编指令,分别为dsb
和sev
。dsb
为数据同步隔离指令,表示在这之前的存储器访问操作必须执行完成后才能执行后面的指令。执行sev
指令后会给所有CPU发送信号,唤醒执行wfe
指令后处于等待事件发生状态的CPU。
static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
smp_mb(); // SMP系统中的内存屏障
lock->tickets.owner++;
dsb_sev(); // 执行dsb和sev指令,这两条指令属于arm架构指令
}
从获取锁和释放锁的过程,可以总结出一下几点:
(1)SMP系统中,自旋锁中的32位数据成员slock
被分为两个16位的变量next
和owner
。获取锁时next
字段加1,释放锁时owner
加1,这两个变量保证了自旋锁的公平竞争。
(2)获取自旋锁时要更新next
,采用arm架构特有的指令实现了独占性访问,确保同一时刻只有一个CPU更新next
成功。
(3)获取自旋锁前,禁止了内核抢占,释放自旋锁后开启了内核抢占。
2.2.2.UP系统中的实现
UP系统中实现很简单,获取自旋锁的时候禁止内核抢占,释放自旋锁的时候开启内核抢占,并没有实现真正的自旋。
// 获取自旋锁
spin_lock
raw_spin_lock // 宏定义
_raw_spin_lock // 宏定义
__LOCK // 宏定义
preempt_disable // 禁止内核抢占
___LOCK
// 释放自旋锁
spin_unlock
raw_spin_unlock
_raw_spin_unlock
__UNLOCK
preempt_enable // 开启内核抢占
___UNLOCK
2.4.定义和初始化自旋锁
// 定义自旋锁
spinlock_t lock;
// 初始化自旋锁
spin_lock_init(lock); // 宏定义
2.5.禁止中断的自旋锁API
前面分析过,当中断、中断下半部和线程存在同时访问临界区的可能时,需要使用禁止中断、中断下半部的自旋锁API保护临界区。
void spin_lock_irq(spinlock_t *lock) // 获取自旋锁同时禁止本地中断
void spin_unlock_irq(spinlock_t *lock) // 释放自旋锁同时开启本地中断
spin_lock_irqsave(lock, flags) // 获取自旋锁同时禁止本地中断,并保存中断状态
// 释放自旋锁同时开启本地中断,并恢复中断状态,使中断状态和获取自旋锁之前的中断状态保持一致
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)
void spin_lock_bh(spinlock_t *lock) // 获取自旋锁同时禁止本地中断下半部分
void spin_unlock_bh(spinlock_t *lock) // 释放自旋锁同时开启本地中断下半部分
下面分析一下spin_lock_irq
和spin_unlock_irq
的函数调用:
spin_lock_irq
raw_spin_lock_irq // 宏定义
_raw_spin_lock_irq
/*======UP系统中的调用========*/
__LOCK_IRQ
local_irq_disable // 禁止本地中断
__LOCK
preempt_disable // 禁止内核抢占
___LOCK
/*======SMP系统中的调用=======*/
->__raw_spin_lock_irq
->local_irq_disable // 禁止本地中断
raw_local_irq_disable
arch_local_irq_disable
// cpsid为arm(V6及以上)架构禁止中断的汇编指令
asm volatile("cpsid i @ arch_local_irq_disable"
: : : "memory", "cc");
->preempt_disable // 禁止抢占
->spin_acquire
->LOCK_CONTENDED // 获取自旋锁,和spin_lock的一致,不再分析
spin_unlock_irq
raw_spin_unlock_irq
_raw_spin_unlock_irq
/*======UP系统中的调用========*/
__UNLOCK_IRQ
local_irq_enable
__UNLOCK
preempt_enable
___UNLOCK
/*======SMP系统中的调用=======*/
->__raw_spin_unlock_irq
->spin_release
->do_raw_spin_unlock
->arch_spin_unlock // 释放自旋锁
->__release
->local_irq_enable // 开启中断
raw_local_irq_enable
arch_local_irq_enable
// cpsie为arm(V6及以上)架构开启中断的汇编指令
asm volatile("cpsie i @ arch_local_irq_disable"
: : : "memory", "cc");
->preempt_enable // 开启内核抢占