首页 > 代码库 > 自旋锁&读/写锁

自旋锁&读/写锁

自旋锁

自旋锁(spin lock)是用来在多处理器环境中工作的一种特殊的锁。如果内核控制路径发现自旋锁“开着”,就获取锁并继续自己的执行。相反,如果内核控制路径发现由运行在另一个CPU上的内核控制路径“锁着”,就在一直循环等待,反复执行一条紧凑的循环指令,直到锁被释放。


一般来说,由自旋锁所保护的每个临界区都是禁止内核抢占的。在单处理器系统上,这种锁本身并不起锁的作用,自旋锁原语仅仅是禁止或启用内核抢占。请注意,在自旋锁忙等期间,内核抢占还是有效的,因此,等待自旋锁释放的进程有可能被更高优先级的进程替代。

自旋锁宏

spin_lock_init     把自旋锁置为1

spin_lock          循环,直到自旋锁变为1(未锁),然后,把自旋锁置为0(锁上)

spin_unlock        把自旋锁置为1

spin_unlock_wait() 等待,直到自旋锁变为1

spin_is_locked()   如果自旋锁被置为1(未锁),返回0,否则,返回1

spin_try_lock()    把自旋锁置为0(锁上),如果原来锁的值是1,则返回1,否则,返回0

所有这些宏都是基于原子操作的

typedef struct {
	volatile unsigned int slock;
#ifdef CONFIG_DEBUG_SPINLOCK
	unsigned magic;
#endif
#ifdef CONFIG_PREEMPT
	unsigned int break_lock;
#endif
} spinlock_t;

在Linux中,每个自旋锁都用spinlock_t结构表示:

lock 该字段表示自旋锁的状态,值为1表示未加锁状态,而任何负数和零都表示加锁状态

break_lock 表示进程正在忙等自旋锁

#ifndef CONFIG_PREEMPT
#define spin_lock_string 	"\n1:\t" 	"lock ; decb %0\n\t" 	"jns 3f\n" 	"2:\t" 	"rep;nop\n\t" 	"cmpb $0,%0\n\t" 	"jle 2b\n\t" 	"jmp 1b\n" 	"3:\n\t"
static inline void _raw_spin_lock(spinlock_t *lock)
{
#ifdef CONFIG_DEBUG_SPINLOCK
	if (unlikely(lock->magic != SPINLOCK_MAGIC)) {
		printk("eip: %p\n", __builtin_return_address(0));
		BUG();
	}
#endif
	__asm__ __volatile__(
		spin_lock_string
		:"=m" (lock->slock) : : "memory");
}
#else /* CONFIG_PREEMPT: */
#define BUILD_LOCK_OPS(op, locktype)					void __lockfunc _##op##_lock(locktype##_t *lock)			{										preempt_disable();							for (;;) {									if (likely(_raw_##op##_trylock(lock)))						break;								preempt_enable();							if (!(lock)->break_lock)							(lock)->break_lock = 1;						while (!op##_can_lock(lock) && (lock)->break_lock)				cpu_relax();							preempt_disable();						}								}			
BUILD_LOCK_OPS(spin, spinlock);

分析spin_lock的实现

非抢占式内核

1、首先调用preempt_disable()禁止内核抢占,实际什么也不执行。

2、执行spin_lock_string汇编指令

   decb递减自旋锁的值,该指令是原子的,因为它带有lock字节前缀。

   检查符号标志,如果它被清零,说明自旋锁被设置为1,因此从标签3(后缀f表示标签是向前的)继续执行,否则

   在标签2(后缀b表示标签是向后的)执行紧凑循环直到自旋锁出现正值。然后从标签2开始重新执行。

抢占式内核

1、首先调用preempt_disable()禁止内核抢占

2、调用函数_raw_spin_trylock(),它对自旋锁slock字段执行原子性的测试和设置操作。执行下面的指令:

static inline int _raw_spin_trylock(spinlock_t *lock)

{
	char oldval;
	__asm__ __volatile__(
		"xchgb %b0,%1"
		:"=q" (oldval), "=m" (lock->slock)
		:"0" (0) : "memory");
	return oldval > 0;
}
首先初始化oldval为0,然后使用xchgb指令交换oldvar与lock->slock的内容,然后判断如果oldval>0 返回1,否则返回0

如果返回1,说明加锁成功,就直接退出循环。否则调用preempt_enable()开启内核抢占,设置break_lock忙等标记。调用cpu_relax()循环等待。

static inline void rep_nop(void)
{
	__asm__ __volatile__("rep;nop": : :"memory");
}

#define cpu_relax()	rep_nop()
XCHG Exchange

REP  Repeat while ECX not zero

读写锁

读/写自旋锁的引入是为了增加内核的并发能力。只要没有内核控制路径对数据进行修改,读/写自旋锁就允许多个内核控制路径同时读一个数据结构。如果一个内核控制路径想对这个结构进行写操作,那么它必须首先获取读/写锁的写锁,写锁授权独占访问这个资源。


typedef struct {
	volatile unsigned int lock;
#ifdef CONFIG_DEBUG_SPINLOCK
	unsigned magic;
#endif
#ifdef CONFIG_PREEMPT
	unsigned int break_lock;
#endif
} rwlock_t;
每个读/写自旋锁都是一个rwlock_t结构,其中lock字段是一个32位的字段,分为两个不同的部分:

1、24位计数器,表示对受保护的数据结构并发地读操作的内核控制路径的数目。这个计数器的二进制补码存放在这个字段的0~23位。

2、“未锁”标志字段,当没有内核控制路径在读或写时设置该位,否则清零。这个“未锁”标志存放在lock字段的第24位。

如果读写锁为空,那么lock字段的值为0x01000000;如果写者已经获得自旋锁,那么lock字段值为0x00000000;

如果一个、两个或多个进程因为读获取了自旋锁,那么,lock字段的值为0x00ffffff,0x00fffffe。

rwlock_t结构中的break_lock与spinlock_t中的字段一样。

Linux中读写锁使用的宏

rwlock_init    初始化lock字段为0x01000000

write_lock     加写锁

read_lock      加读锁

write_unlock   解写锁

read_unlock    解读锁

write_trylock  尝试获取写锁,获取不到返回,非阻塞

read_trylock   尝试获取读锁,获取不到返回,非阻塞

write_can_lock 检查是否可以加写锁

read_can_lock  检查是否可以加读锁

由于读写自旋锁和自旋锁比较类似,就不再分析具体实现了。