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
	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锁保护的是代码



参考:

《 Linux的内核设计与实现 》

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



























宏函数spin_lock_string

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

_spin_lock_irq()

         禁止本地中断并获取指定的锁


-------------------------------------------------- ----------------

总结

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值