Linux内核同步方法——自旋锁(spin lock)

自旋锁

    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的内核设计与实现”

Linux的内核中的互斥操作(2) - 自旋锁









#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);

猜你喜欢

转载自blog.csdn.net/hhhanpan/article/details/80558917