“锁”的作用就是保护临界区资源,避免不同的CPU同时访问相同的变量(或中断与进程同时访问相同变量)。非原子变量的赋值,大多数都不是一个指令周期能完成的,试想如果CPU1将某变量刚更改了一半,CPU2正好就来读取了这个半成品变量,并基于此去做计算,可能会造成严重后果。
解决办法就是CPU1在改写这个变量前,先“加锁”;CPU2访问这个变量前也先加锁,但由于已经被加锁了,所以获取锁失败,CPU2原地等待锁释放;CPU1改写完变量后,“解锁”;CPU2获取到锁后加锁,然后访问变量,完成后解锁。
上面的“锁”可以是信号量、互斥锁、自旋锁等。本文的自旋锁(spinlock)与其它锁的最大不同,就是在等待锁释放的时候不会睡眠,而是在空转(自旋)。信号量和互斥锁在等待锁释放的时候,会进入睡眠。
不睡眠的好处是:可以在中断上下文运行;另外,对于很快就能获取到锁的场景,这种方式效率更高。
不睡眠的坏处是:如果等待锁释放的时间较长,则极其浪费CPU资源。
/******include/asm-i386/spinlock_types.h***/
typedef struct {
unsigned int slock;
} raw_spinlock_t;
#define __RAW_SPIN_LOCK_UNLOCKED { 1 }
/******include/asm-i386/spinlock.h***/
static inline void __raw_spin_lock(raw_spinlock_t *lock){
asm volatile(
// lock->slock减1
1:LOCK_PREFIX decb %0
//如果不为负.跳转到3f.3f后面没有任何指令,即为退出
jns 3f
//重复执行nop.nop是x86的小延迟函数
2:rep nop
cmpb $0,%0
//如果lock->slock不大于0,跳转到标号2,即继续重复执行nop
jle 2
//如果lock->slock大于0,跳转到标号1,重新判断锁的slock成员
jmp 1
3:: "+m" (lock->slock) : : "memory");
}
在多处理器环境中 LOCK_PREFIX 实际被定义为 “lock”前缀。x86 处理器使用“lock”前缀的方式提供了在指令执行期间对总线加锁的手段。芯片上有一条引线 LOCK,如果在一条汇编指令(ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, XCHG)前加上“lock” 前缀,经过汇编后的机器代码就使得处理器执行该指令时把引线 LOCK 的电位拉低,从而把总线锁住,这样其它处理器或使用DMA的外设暂时无法通过同一总线访问内存。
jns 汇编指令检查 EFLAGS 寄存器的 SF(符号)位,如果为 0,说明 slock 原来的值为 1,则线程获得锁,然后跳到标签 3 的位置结束本次函数调用。如果 SF 位为 1,说明 slock 原来的值为 0 或负数,锁已被占用。那么线程转到标签 2 处不断测试 slock 与 0 的大小关系,假如 slock 小于或等于 0,跳转到标签 2 的位置继续忙等待;假如 slock 大于 0,说明锁已被释放,则跳转到标签 1 的位置重新申请锁。