linux内核编程之四:同步方法自旋锁(spin lock)

313 阅读6分钟

​ 自旋锁

    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     unsigned magic; #endif #ifdef CONFIG_PREEMPT     /*      * 表示进程正在忙等待自旋锁。      * 只有内核支持SMP和内核抢占时才使用本标志。      /     unsigned int break_lock; #endif } spinlock_t; spin_lock() /*  * 当内核不可抢占时,spin_lock的实现过程。  / #define _spin_lock(lock)    
do {
/
*      * 调用preempt_disable禁用抢占。      /     preempt_disable();
/
*      * _raw_spin_lock对自旋锁的slock字段执行原子性的测试和设置操作。      */     _raw_spin_lock(lock);
__acquire(lock);
} while(0)      函数_raw_spin_lock()对自旋锁的SLOCK字段执行原子性的测试和设置操作。

#define _raw_spin_lock(x)        
do {
CHECK_LOCK(x);
if ((x)->lock&&(x)->babble) {
(x)->babble--;
printk("%s:%d: spin_lock(%s:%p) already locked by %s/%d\n",
FILE,LINE, (x)->module,
(x), (x)->owner, (x)->oline);
}
(x)->lock = 1;
(x)->owner = FILE;
(x)->oline = LINE;
} while (0)

spin_unlock() #define _spin_unlock(lock)
do {
_raw_spin_unlock(lock);
preempt_enable();
__release(lock);
} while (0) static inline void _raw_spin_unlock(spinlock_t *lock) { #ifdef CONFIG_DEBUG_SPINLOCK     BUG_ON(lock->magic != SPINLOCK_MAGIC);     BUG_ON(!spin_is_locked(lock)); #endif     asm volatile(         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()     保存中断的当前状态,并禁止本地中断,然后再去获取指定的锁。

unsigned long __lockfunc _spin_lock_irqsave(spinlock_t *lock) {     unsigned long flags;       local_irq_save(flags);     preempt_disable();     _raw_spin_lock_flags(lock, flags);     return flags; } (2)_spin_unlock_irqrestore()          对指定的锁解锁,然后让中断恢复到加锁前的状态

void __lockfunc _write_unlock_irqrestore(rwlock_t *lock, unsigned long flags) {     _raw_write_unlock(lock);     local_irq_restore(flags);     preempt_enable(); }

    如果能确定中断在加锁前是激活的,那就不需要在解锁后恢复中断以前的状态。也就可以无条件地在解锁时激活中断。这时可以使用spin_lock_irq()和spin_unlock_irq()。

_spin_lock_irq()          禁止本地中断并获取指定的锁 。 void __lockfunc _read_lock_irq(rwlock_t *lock) {     local_irq_disable();     preempt_disable();     _raw_read_lock(lock); } _spin_unlock_irq()         释放指定的锁,并激活本地中断。

void __lockfunc _spin_unlock_irq(spinlock_t *lock) {     _raw_spin_unlock(lock);     local_irq_enable();     preempt_enable(); }    在使用spin_lock_irq()方法时,需要确定中断原来是否处于激活状态。一般不建议使用。

spin_lock_init()      动态初始化指定的spinlock_t,(此时只有一个指向spinlock_t类型地指针,没有它的实体)

#define spin_lock_init(lock)    do { (void)(lock); } while(0)

spin_try_lock()     试图获的某个特定的自旋锁。如果该锁已经被争用,那么该函数立即返回一个非0值,而不会自旋等待锁被释放;                

如果成功地获得了这个自旋锁,该函数返回0。

int __lockfunc _spin_trylock(spinlock_t *lock) {     preempt_disable();    //使抢占计数加1     if (_raw_spin_trylock(lock))         return 1;          preempt_enable();    // 使抢占计数减1,并在thread_info描述符的TIF_NEED_RESCHED标志被置为1的情况下,调用preempt_schedule()     return 0; }

spin_is_locked()     用于检查特定的锁当前是否已被占用,如果已被占用,返回非0值;否则返回0。

#define spin_is_locked(x)
({
CHECK_LOCK(x);
if ((x)->lock&&(x)->babble) {
(x)->babble--;
printk("%s:%d: spin_is_locked(%s:%p) already locked by %s/%d\n",
FILE,LINE, (x)->module,
(x), (x)->owner, (x)->oline);
}
0;
})


总结

(1) 一个被争用的自旋锁使得请求它的线程在等待锁重新可用时自旋,会特别浪费处理器时间。所以自旋锁不应该被长时间持有。因此,自旋锁应该使用在:短时间内进行轻量级加锁。

(2)还可以采取另外的方式来处理对锁的争用:让请求线程睡眠,直到锁重新可用时再唤醒它这样处理器不必循环等待,可以执行其他任务。

    但是让请求线程睡眠的处理也会带来一定开销:会有两次上下文切换,被阻塞的线程要换出和换入所以,自旋持有锁的时间最好小于完成两次上下文e月刊的耗时,也就是让持有自旋锁的时间尽可能短。(在抢占式内核中,的锁持有等价于系统-的调度等待时间),信号量可以在发生争用时,等待的线程能投入睡眠,而不是旋转。

(3)在单处理机器上,自旋锁是无意义的。因为在编译时不会加入自旋锁,仅仅被当作一个设置内核抢占机制是否被启用的开关。如果禁止内核抢占,那么在编译时自旋锁会被完全剔除出内核。

(4)Linux内核中,自旋锁是不可递归的。如果试图得到一个你正在持有的锁,你必须去自旋,等待你自己释放这个锁。但这时你处于自旋忙等待中,所以永远不会释放锁,就会造成死锁现象。

(5)在中断处理程序中,获取锁之前一定要先禁止本地中断(当前处理器的中断),否则,中断程序就会打断正持有锁的内核代码,有可能会试图去争用这个已经被持有的自旋锁。这样就会造成双重请求死锁(中断处理程序会自旋,等待该锁重新可用,但锁的持有者在这个处理程序执行完之前是不可能运行的)

(6)锁真正保护的是数据(共享数据),而不是代码。对于BLK(大内核锁)保护的是代码。

补充: BLK:大内核锁     BLK是一个全局自旋锁,主要目的是使Linux的最初的SMP过渡到细粒度加锁机制。

特性如下:

·持有BLK的任务可以睡眠的,是安全的。因为当任务无法被调度时,所加锁会自动被丢弃;当任务被调度时,锁会被重新获得。

·BLK是一种递归锁。一个进程可以多次请求一个锁,而不会像自旋锁那样造成死锁现象。

·BLK只可以用在进程上下文中。不同于自旋锁可在中断上下文中加锁。

·BLK锁保护的是代码。