Linux内核互斥同步之自旋锁

1.自旋锁

​ 原子变量的适用范围主要是计数,但是代码临界区往往是一个操作的集合,原子变量在此显然是不合适的,需要使用锁机制来完成同步工作,spin_lock就是内核中比较常见的锁机制。

​ spin_lock锁在同一个时刻只有一个内核代码路径持有,如果还有内核路径试图获取已经被持有的spin_lock,那么该内核代码路径需要一直自旋等待,直到锁持有者释放该锁。如果该锁没有被人持有,可立即获得

1.1自旋锁定义

自旋锁的结构体如下:

typedef struct spinlock {
    
    
		struct raw_spinlock rlock;

} spinlock_t;

typedef struct raw_spinlock {
    
    
	arch_spinlock_t raw_lock;
} raw_spinlock_t;

typedef struct {
    
    
	union {
    
    
		u32 slock;
		struct __raw_tickets {
    
    
			u16 owner;
			u16 next;
		} tickets;
	};
} arch_spinlock_t;

​ 在Linux2.6.25以后,spinlock实现了"FIFO tick-based"算法的机制,简称排队自旋锁,排队自旋锁的原理就像只有一个座位的饭店,当第一个客人来时,直接就餐,后面来的人排队领号,当就餐的客人离开后,按照排队领号的顺序,安排下一位就餐的客人,

1.2加锁

​ 具体实现如下:

static inline void spin_lock(spinlock_t *lock)
{
	raw_spin_lock(&lock->rlock);
}
#define raw_spin_lock(lock)	_raw_spin_lock(lock)

void __lockfunc _raw_spin_lock(raw_spinlock_t *lock)
{
	__raw_spin_lock(lock);
}
static inline void __raw_spin_lock(raw_spinlock_t *lock)
{
	preempt_disable();     //关闭内核抢占
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
	__acquire(lock);
	arch_spin_lock(&lock->raw_lock);
}

static inline void arch_spin_lock(arch_spinlock_t *lock)
{
	unsigned long tmp;
	u32 newval;
	arch_spinlock_t lockval;

	prefetchw(&lock->slock);
	__asm__ __volatile__(
"1:	ldrex	%0, [%3]\n"
"	add	%1, %0, %4\n"
"	strex	%2, %1, [%3]\n"
"	teq	%2, #0\n"
"	bne	1b"
	: "=&r" (lockval), "=&r" (newval), "=&r" (tmp)
	: "r" (&lock->slock), "I" (1 << TICKET_SHIFT)
	: "cc");

	while (lockval.tickets.next != lockval.tickets.owner) {
		wfe();
		lockval.tickets.owner = ACCESS_ONCE(lock->tickets.owner);
	}

	smp_mb();
}

​ arch_spin_lock函数通过ldrex指令把lock->slock的值加入到lockval中,将lockval中的next加1,并且保存到newval变量中,然后把newval值写回lock->slock。接着判断变量lockval的next域与owner域是否相等,如果不相等调用wfe指令让CPU进入等待状态。当有其它CPU唤醒本CPU时,说明该spinlock锁的owner域发生了变量,有人释放了锁。当新的owner的值与next相等的时候,即当前owner代表的CPU获得了该锁。WFE为ARM体系结构指令该指令可以让ARM核进入standby睡眠模式。

1.3解锁

​ Spinlock锁释放的过程如下:

static inline void spin_unlock(spinlock_t *lock)
{
	raw_spin_unlock(&lock->rlock);
}

#define raw_spin_unlock(lock)		_raw_spin_unlock(lock)

void __lockfunc _raw_spin_unlock(raw_spinlock_t *lock)
{
	__raw_spin_unlock(lock);
}

static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
	spin_release(&lock->dep_map, 1, _RET_IP_);
	do_raw_spin_unlock(lock);
	preempt_enable();
}

void do_raw_spin_unlock(raw_spinlock_t *lock)
{
	debug_spin_unlock(lock);
	arch_spin_unlock(&lock->raw_lock);
}


static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
	smp_mb();
	lock->tickets.owner++;
	dsb_sev();
}

​ 释放过程比较简单,首先调用smp_mb内存屏障指令,保证该函数之前所有的内存访问都执行完毕,然后给lock->owner域加1。最后调用dsb_sev()函数:该函数有两个作用,第一dsb指令保证owner域已经写入内存,其次调用SEV指令通过WFE唤醒睡眠状态的CPU。

1.4自旋锁变种

​ 对于上述的自旋锁,如果在spinlock的临界区中发生了中断,在中断中又去获取该锁的话就会发生死锁,这个时候我们需要适用spinlock的变种如下:

static inline void spin_lock_irq(spinlock_t *lock)
{
	raw_spin_lock_irq(&lock->rlock);
}
void __lockfunc _raw_spin_lock_irq(raw_spinlock_t *lock)
{
	__raw_spin_lock_irq(lock);
}
static inline void __raw_spin_lock_irq(raw_spinlock_t *lock)
{
	local_irq_disable();
	preempt_disable();
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	LOCK_CONTENDED(lock, do_raw_spin_trylock, do_raw_spin_lock);
}

spin_lock_irq的实现比spin_lock多了local_irq_disable函数,该函数用于关闭本地处理器(只关闭本地处理器的中断,防止死锁即可),保证在获取锁的过程中不会响应本地中断。

另外spin_lock_irqsave会保存CPU当前的irq状态,然后关闭中断。

spinlock还有另外一个spin_lock_bh变种,用于处理进程和延迟处理机制导致的并发访问的互斥问题。

1.5自旋锁使用场景

在前面介绍了几种自旋锁,spin_lock,spin_lock_irq,spin_lock_irqsave,spin_lock_bh,下面看下他们的使用场景。

  • spin_lock

    如果整个临界区都只位于进程上下文或者工作队列中,,那么只需要采用最为方便的spin_lock即可,因为他不会发生中断抢占锁,哪怕中断抢占进程上下文也不会导致中断由于申请自旋锁而导致死锁。

  • spin_lock_irq

    spin_lock_irq锁适合进程上下文/软中断+硬件中断这样的组合中使用,当然这种类型的变种同样适合软中断+进程上下文的组合,因为关闭了硬件中断,从源头上禁止了软中断代码,不过这种类型的中断最好的方式时使用spin_lock_bh的方式,因为他只锁定软中断代码执行,而不关闭硬件中断,这样性能损耗更小

  • spin_lock_irqsave

    跟spin_lock_irq相比,spin_lock_irqsave函数会保存本地CPU当前的irq状态,关闭本地CPU中断,然后再获取spin_lock锁,释放锁时会调用local_irq_restore恢复中断状态。这种类型的锁最为安全以及便捷,但是也是性能损耗最大的代码,能不使用尽量不要使用。

  • spin_lock_bh

    spin_lock_bh是一种比spin_lock_irq更轻量的变种,只关闭中断底半部,也就是关闭了软中断以及Timer等的抢占能力,如果能够确认编写代码临界区只存在软中断+进程上下文这样的组合。那么最好考虑使用spin_lock_bh。

总结:如果给硬件中断,软中断,进程上下文排高中低的优先级的话,spin_lock,spin_lock_bh,spin_lock_irq应该这样使用,使用存在代码临界区的最高级别的自旋锁,即硬中断有代码临界区则使用spin_lock_irq,硬中断没有代码临界区,软中断有的话,则使用spin_lock_bh,如果只有进程上下文的话,则只需要使用spin_lock即可。

猜你喜欢

转载自blog.csdn.net/wll1228/article/details/107719153