互斥与同步
当多个执行路径并发执行时,确保对共享资源的访问安全是驱动程序员不得不面对的问题
互斥:对资源的排他性访问
同步:对进程执行的先后顺序做出妥善的安排
一些概念:
临界区:对共享的资源进行访问的代码片段称为临界区
并发源:导致出现多个执行路径的因素称为并发源
本节首要目标:
- 考察linux内核中并发执行的来源
- 讨论内核为资源互斥访问所提供的设施的幕后机制以及各自的应用场景
并发来源
可能导致对共享资源的访问出现竞争状态的若干执行路径都可以被称为并发,不一定是严格的时间意义上的并发执行才算并发的来源
并发来源:
- 中断处理路径:系统正在执行当前进程时,发生了中断,中断处理函数和被中断的进程之间形成的并发。软中断的执行也可以归结在此
- 调度器的可抢占性:单处理器上,调度器的可抢占性导致的进程与进程之间的并发,类似地,多处理器上进程与进程之间的并发。
- 多处理器的并发执行:多处理器的并发执行
local_irq_enable与local_irq_disable
这两个宏主要是为了解决单处理器不可抢占的调度器上的并发竞争问题,在进入临界区时local_irq_disable关闭中断,退出临界区时调用local_irq_enable来打开中断,保证临界区中系统不会出现异步的并发源。
驱动中应该避免使用这两个宏,但spinlock等互斥机制中常常使用这两个宏。
源码:
#define local_irq_enable() \
do { trace_hardirqs_on(); raw_local_irq_enable(); } while (0)
#define local_irq_disable() \
do { raw_local_irq_disable(); trace_hardirqs_off(); } while (0)
抛开两个trace前缀的调试接口不谈,raw前缀的两个接口才是此处的主角,他们是arch-specific的,不同的处理器体系结构会有不同指令来开启和关闭处理响应外部中断的能力:x86使用sti和cli,arm使用CPSIE。都旨在置位或者清除当前处理器状态寄存器这类寄存器上的中断使能位,这个flag一般决定了整个处理器能否接收中断。
此处源码为arm体系下的实现
// /include/linux/irqflags.h
/*
* Wrap the arch provided IRQ routines to provide appropriate checks.
*/
#define raw_local_irq_disable() arch_local_irq_disable()
#define raw_local_irq_enable() arch_local_irq_enable()
// /arch/arm/include/asm/irqflags.h
static inline void arch_local_irq_enable(void)
{
asm volatile(
" cpsie i @ arch_local_irq_enable"
:
:
: "memory", "cc");
}
static inline void arch_local_irq_disable(void)
{
asm volatile(
" cpsid i @ arch_local_irq_disable"
:
:
: "memory", "cc");
}
变体 local_irq_save和local_irq_restore
local_irq_save会将arch_local_irq_save接口中读到的中断使能状态位返回,并关闭中断。此时如果返回的flg状态是关闭状态,那么就属于再次关闭。
local_irq_restore会将local_irq_save读到的flg状态传入arch_local_irq_restore,后者会将flg写回到CPSR中,恢复调用local_irq_save前的状态。
#define raw_local_irq_save(flags) \
do { \
typecheck(unsigned long, flags); \
flags = arch_local_irq_save(); \
} while (0)
#define raw_local_irq_restore(flags) \
do { \
typecheck(unsigned long, flags); \
arch_local_irq_restore(flags); \
} while (0)
static inline unsigned long arch_local_irq_save(void)
{
unsigned long flags;
asm volatile(
" mrs %0, cpsr @ arch_local_irq_save\n"
" cpsid i"
: "=r" (flags) : : "memory", "cc");
return flags;
}
/*
* restore saved IRQ & FIQ state
*/
static inline void arch_local_irq_restore(unsigned long flags)
{
asm volatile(
" msr cpsr_c, %0 @ local_irq_restore"
:
: "r" (flags)
: "memory", "cc");
}
设想这样一个情况:
- 本身CPSR中中断flg就是关闭的状态
- 调用了local_irq_disable
- 执行临界区中代码
- 调用了local_irq_enable
这个过程的结果就把本来的中断flg修改了。local_irq_save和local_irq_restore就是为了解决这种问题。
自旋锁
设计自旋锁的最初目的是在多处理器系统中提供对共享数据的保护,其背后的核心思想是设置一个在多处理器之间共享的全局变量锁 V,并定义当 V=1时为上锁状态,V=0为解锁状态。如果处理器 A 上的代码要进入临界区,它要先读取 V 的值,判断其是否为 0,如果V头0表明有其他处理器上的代码正在对共享数据进行访问,此时处理器 A 进入忙等待即自旋状态,如果 V=0 表明当前没有其他处理器上的代码进入临界区,此时处理器 A 可以访问该资源,它先把V置1(自旋锁的上锁状态),然后进入临界区,访问完毕离开临界区时将V置0(自旋锁的解锁状态)。
上述自旋锁的设计思想在用具体代码实现时的关键之处在于,必须确保处理器 A“读取V判断V的值与更新V”这一操作序列是个原子操作(atomic operation)。所谓原子操作,简单地说就是执行这个操作的指令序列在处理器上执行时等同于单条指令,也即该指令序列在执行时是不可分割的
spin_lock
spin_lock也是arch-specific的,不同的体系结构有不同的原子操作指令。
//精简过后的代码
typedef struct raw_spinlock {
volatile unsigned int raw_lock;
} raw_spinlock_t;
typedef struct spinlock {
union {
strcut raw_spinlock rlock;
};
} spinlock_t;
static inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}
//raw_spin_lock是个宏,最后展开是这个样子(arm体系下)
static inline void raw_spin_lock(raw_spinlock_t *lock)
{
preempt_disable(); //如果定义了CONFIG_PREEMPT,内核支持可抢占的调度系统,将关闭调度器的可抢占特性,否则是空定义
do_raw_spin_lock(lock);
}
static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
__acquire(lock);
arch_spin_lock(&lock->raw_lock);
}
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
unsigned long tmp;
__asm__ __volatile__(
"1: ldrex %0, [%1]\n" //tmp = lock->raw_lock L1
" teq %0, #0\n" //测试tmp是否等于0 L2
WFE("ne") //如果tmp!=0 ,代表此时资源不空闲,执行WFE指令,让core进入low-power state
" strexeq %0, %2, [%1]\n" //tmp==0,此时资源空闲,执行将1写入lock,将操作结果写入tmp L3
" teqeq %0, #0\n" //判断tmp是否为0,如果为0,则更新lock成功,此时lock=1,PC可以进入临界区。 L4
" bne 1b" //如果tmp不等于0,则跳到label 1 L5
: "=&r" (tmp)
: "r" (&lock->lock), "r" (1)
: "cc");
smp_mb();
}
preempt_disable();如果定义了CONFIG_PREEMPT,内核支持可抢占的调度系统,将关闭调度器的可抢占特性,否则是空定义。这里为什么要关闭调度器的可抢占特性:
防止因为调度器的可抢占性导致的竞态
spin_lock的变体
关闭中断,防止中断处理函数中的临界区与被中断的进程的临界区产生死锁。
spin_lock_irq
spin_lock_irqsave
引用
LDREX和STREX
从ARMv6架构开始,ARM处理器提供了Exclusive accesses同步原语,包含两条指令:
- LDREX
- STREX
LDREX和STREX指令,将对一个内存地址的原子操作拆分成两个步骤,
同处理器内置的记录exclusive accesses的exclusive monitors一起,完成对内存的原子操作。
LDREX
LDREX与LDR指令类似,完成将内存中的数据加载进寄存器的操作。
与LDR指令不同的是,该指令也会同时初始化exclusive monitor来记录对该地址的同步访问。例如
LDREX R1, [R0]
会将R0寄存器中内存地址的数据,加载进R1中并更新exclusive monitor。
STREX
该指令的格式为:
STREX Rd, Rm, [Rn]
STREX会根据exclusive monitor的指示决定是否将寄存器中的值写回内存中。
如果exclusive monitor许可这次写入,则STREX会将寄存器Rm的值写回Rn所存储的内存地址中,并将Rd寄存器设置为0表示操作成功。
如果exclusive monitor禁止这次写入,则STREX指令会将Rd寄存器的值设置为1表示操作失败并放弃这次写入。
应用程序可以根据Rd中的值来判断写回是否成功。