首页 > 代码库 > 自旋锁&读/写锁
自旋锁&读/写锁
自旋锁
自旋锁(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 检查是否可以加读锁
由于读写自旋锁和自旋锁比较类似,就不再分析具体实现了。