自旋锁
Linux的的内核最常见的锁是自旋锁。自旋锁最多只能被一个可执行线程持有。如果一个执行线程试图获得一个被已经持有(争用)的自旋锁,那么该线程就会一直进行忙循环-旋转-等待锁重新可用要是锁未被争用,请求锁的执行线程就可以立即得到它,继续执行。
在任意时间,自旋锁都可以防止多于一个的执行线程同时进入临界区。
同一个锁可以用在多个位置,例如,对于给定数据的所有访问都可以得到保护和同步。
-------------------------------------------------- -------------------------------------------
在Linux的2.6.11.12内核版本中,自旋锁的实现接口定义在包含\ linux的\ <spinlock.h>中,与体系结构相关的代码定义包含在\ ASM \ <spinlock.h>中。
基本结构
自旋锁的结构体是spinlock_t,
typedef struct { / ** *该字段表示自旋锁的状态,值为1表示未加锁,任何负数和0都表示加锁 * / volatile unsigned int slock; #ifdef CONFIG_DEBUG_SPINLOCK 无符号魔法; #万一 #ifdef CONFIG_PREEMPT / ** *表示进程正在忙等待自旋锁。 * /只有内核支持SMP和内核抢占时才使用本标志。 * / unsigned int break_lock; #万一 } spinlock_t;
用用spin_lock()
void __lockfunc _spin_lock(spinlock_t * lock) { preempt_disable(); _raw_spin_lock(锁); }
函数_raw_spin_lock()对自旋锁的SLOCK字段执行原子性的测试和设置操作。
static inline void _raw_spin_lock(spinlock_t * lock) { #ifdef CONFIG_DEBUG_SPINLOCK 如果(不太可能(锁定 - >魔术= SPINLOCK_MAGIC)){ printk(“eip:%p \ n”,__ builtin_return_address(0)); BUG(); } #万一 __asm__ __volatile __( spin_lock_string / *进入宏函数spin_lock_string,传参锁 - > slock * / :“= m”(lock-> slock)::“memory”); }
宏函数spin_lock_string
#define spin_lock_string \ “\ n1:\ t”\ “lock; DECB%0 \ n \ t”的\ // DECB递减自旋锁的值。它有锁前缀,因此是原子的。 / *如果结果为0(不是负数),说明锁是打开的,跳到3F处继续执行。* / “jns 3f \ n”\ / ** *否则,结果为负,说明锁是关闭的。就执行死循环,等待它的值变化。 *需要注意的是rep后面跟了一句nop ,这是不能省略的。否则,总线可能被锁死。 * / “2:\ t”\ “rep; nmp \ n \ t“\ / ** *比较锁定值,直到它变化,才跳到开头,试图再次获得锁。 *否则,继续死循环等锁值变化。 * / ”cmpb $ 0,%0 \ n \ t“\ ”jle 2b \ n \ t“\ ”jmp 1b \ n“\ ”3:\ n \ t“的
spin_unlock()保护保护
void __lockfunc _spin_unlock(spinlock_t * lock) {_ raw_spin_unlock(锁); preempt_enable(); }
static inline void _raw_spin_unlock(spinlock_t * lock) { char oldval = 1; #ifdef CONFIG_DEBUG_SPINLOCK BUG_ON(lock-> magic!= SPINLOCK_MAGIC); BUG_ON(spin_is_locked(锁定)!); #万一 __asm__ __volatile __( spin_unlock_string / *调用宏函数spin_unlock_string * / ); }
宏函数spin_unlock_string
在spin_unlock_string中,%0即为 锁 - > s 锁,movb指令将锁 - > s 锁定为1,movb指令本身就是原子操作,所以不需要锁总线。#define spin_unlock_string \ “movb $ 1,%0”\ :“= m”(lock-> slock)::“memory”
自旋锁在同一时刻至多被一个执行线程持有,所以一个时刻只有一个线程位于临界区内,这就为多处理器机器提供了防止并发访问所需的保护机制。
在单处理机器上,编译的时候不会加入自旋锁,仅会被当作一个设置内核抢占机制是否被启用的开关。如果禁止内核抢占,那么在编译时自旋锁就会被剔除出内核。
内核提供的禁止中断同时请求锁的接口
(1)_spin_lock_irqsave()
保存中断的当前状态,并禁止本地中断,然后再去获取指定的锁。
#define _spin_lock_irqsave(锁,标志)\ 做{\ local_irq_save(标志); \ preempt_disable(); \ _raw_spin_lock(锁); \ __acquire(锁定); \ (0)
(2)_spin_unlock_irqrestore()
对指定的锁解锁,然后让中断恢复到加锁前的状态
void __lockfunc _spin_unlock_irqrestore(spinlock_t * lock,无符号长标志) {_ raw_spin_unlock(锁); local_irq_restore(标志); preempt_enable(); } EXPORT_SYMBOL(_spin_unlock_irqrestore);
如果能确定中断在加锁前是激活的,那就不需要在解锁后恢复中断以前的状态。也就可以无条件地在解锁时激活中断。这时可以使用spin_lock_irq()和spin_unlock_irq()。
_spin_lock_irq()
禁止本地中断并获取指定的锁 。#define _spin_lock_irq(锁定)\ 做{\ local_irq_disable(); \ preempt_disable(); \ _raw_spin_lock(锁); \ __acquire(锁定); \ (0)
_spin_unlock_irq()
释放指定的锁,并激活本地中断。
void __lockfunc _spin_unlock_irq(spinlock_t * lock) {_ raw_spin_unlock(锁); local_irq_enable(); preempt_enable(); } EXPORT_SYMBOL(_spin_unlock_irq);在使用spin_lock_irq()方法时,需要确定中断原来是否处于激活状态。一般不建议使用。
spin_lock_init()
动态初始化指定的spinlock_t,(此时只有一个指向spinlock_t类型地指针,没有它的实体)
/ ** *把自旋锁置为1(未锁) * / #define spin_lock_init(x)do {*(x)= SPIN_LOCK_UNLOCKED; (0)
spin_try_lock()
试图获的某个特定的自旋锁。如果该锁已经被争用,那么该函数立即返回一个非0值,而不会自旋等待锁被释放;
如果成功地获得了这个自旋锁,该函数返回0。
int __lockfunc _spin_trylock(spinlock_t * lock) { preempt_disable(); //使抢占计数加1 if(_raw_spin_trylock(lock))//判断该锁是否被争用 返回1; preempt_enable(); //使抢占计数减1,并在的thread_info描述符的TIF_NEED_RESCHED标志被置为1的情况下,调用preempt_schedule() 返回0; }
spin_is_locked()
用于检查特定的锁当前是否已被占用,如果已被占用,返回非0值;否则返回0。
/ ** *如果自旋锁被置为1(未锁),返回0,否则返回1 * / #define spin_is_locked(x)(*(volatile signed char *)(&(x) - > slock) 0)
-------------------------------------------------- ----------------
总结
(1) 一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋,会特别浪费处理器时间。所以自旋锁不应该被长时间持有。因此,自旋锁应该使用在:短时间内进行轻量级加锁。
(2)还可以采取另外的方式来处理对锁的争用:让请求线程睡眠,直到锁重新可用时再唤醒它这样处理器不必循环等待,可以执行其他任务。
但是让请求线程睡眠的处理也会带来一定开销:会有两次上下文切换,被阻塞的线程要换出和换入所以,自旋持有锁的时间最好小于完成两次上下文e月刊的耗时,也就是让持有自旋锁的时间尽可能短。(在抢占式内核中,的锁持有等价于系统-的调度等待时间),信号量可以在发生争用时,等待的线程能投入睡眠,而不是旋转。
(3)在单处理机器上,自旋锁是无意义的。因为在编译时不会加入自旋锁,仅仅被当作一个设置内核抢占机制是否被启用的开关。如果禁止内核抢占,那么在编译时自旋锁会被完全剔除出内核。
(4)Linux内核中,自旋锁是不可递归的。如果试图得到一个你正在持有的锁,你必须去自旋,等待你自己释放这个锁。但这时你处于自旋忙等待中,所以永远不会释放锁,就会造成死锁现象。
(5)在中断处理程序中,获取锁之前一定要先禁止本地中断(当前处理器的中断),否则,中断程序就会打断正持有锁的内核代码,有可能会试图去争用这个已经被持有的自旋锁。这样就会造成双重请求死锁(中断处理程序会自旋,等待该锁重新可用,但锁的持有者在这个处理程序执行完之前是不可能运行的)
(6)锁真正保护的是数据(共享数据),而不是代码。对于BLK(大内核锁)保护的是代码。
补充:
BLK:大内核锁
BLK是一个全局自旋锁,主要目的是使Linux的最初的SMP过渡到细粒度加锁机制。
特性如下:
·持有BLK的任务可以睡眠的,是安全的。因为当任务无法被调度时,所加锁会自动被丢弃;当任务被调度时,锁会被重新获得。
·BLK是一种递归锁。一个进程可以多次请求一个锁,而不会像自旋锁那样造成死锁现象。
·BLK只可以用在进程上下文中。不同于自旋锁可在中断上下文中加锁。
·BLK锁保护的是代码。
参考:
“的Linux的内核设计与实现”
#define _spin_lock_irq(锁定)\ 做{\ local_irq_disable(); \ preempt_disable(); \ _raw_spin_lock(锁); \ __acquire(锁定); \ (0)
void __lockfunc _spin_unlock_irq(spinlock_t * lock) {_ raw_spin_unlock(锁); local_irq_enable(); preempt_enable(); } EXPORT_SYMBOL(_spin_unlock_irq);