内核-自旋锁

一、内核自旋锁

自旋锁就是一个二状态的原子(atomic)变量:unlockedlocked

当任务A希望访问被自旋锁保护的临界区(critical section),首先需要这个自旋锁目前要处于unlocked状态,然后会去尝试(acquire)这个自旋锁(将这个变量状态修改为locked)。

如果在这之后有另一个任务B同样希望去访问这段临界区,那么必须要等到任务A释放(release)掉自旋锁才行,在这之前,任务B一直等待此处,不断尝试获取(acquire),这样就被称为自旋在这里。

1.1 为什么内核需要引入自旋锁

up & SMP

UP表示单处理器,SMP表示对称多处理器(多CPU)。一个处理器就可以看作一个执行单元,在任何一个时刻,只能运行在一个进程上下文或者中断上下文

中断(interrupt)

中断可以发生在任务的指令过程中,如果中断处于使能,会从任务所处的进程上下文切换到中断上下文,在中断上下文中进行所谓的中断处理(ISR)。

内核中使用local_irq_disable()或者local_irq_save(&flags)来去使能中断。两者的区别时后者会将当前的中断使能状态先保存到flag中。

相反,内核使用local_irq_enable()来无条件的使能中断,而使用local_irq_restore(&flags)来恢复之前的中断状态。

无论时开中断还是关中断的函数都有local前缀,这表示开关中断的只在当前CPU生效。

内核态抢占(preemept)

抢占,通俗的理解就是内核调度时,高优先级的任务从低优先的任务中抢到CPU的控制权,开始运行,其中又分为用户态抢占内核态抢占,本文需要关心的是内核态抢占。

目前的内核都配置为抢占式内核(默认),在一些时机(比如说中断处理结束,返回内核空间时),会触发重新调度,此时高优先级的任务可以抢占原来占用CPU的低优先级任务。

需要特别指出的是,抢占同样需要中断处于打开状态。

void __sched notrace preempt_schedule(void)
{
	struct thread_info *ti = current_thread_info();
	/*
	 * If there is a non-zero preempt_count or interrupts are disabled,
	 * we do not want to preempt the current task. Just return..
	 */
	if (likely(ti->preempt_count || irqs_disabled()))
		return;
    ......
}

代码中的preempt_count表示当前任务是否可以被抢占,结果为0表示可以被抢占,而大于0表示不可以。而irqs_disabled用来看中断是否关闭

内核中使用preemt_disable()禁止抢占,使用preemt_enable()使能可以抢占

什么是临界区

每个进程中访问临界资源的那段程序称为临界区(临界资源是一次仅允许一个进程使用的共享资源)。每次值准许一个进程进入临界区,进入后不允许其他进程进入。

1.2 单处理器和多处理器上临界区问题

Case1 单处理器-任务上下文抢占

对于单处理器来说,任何一个时刻只会有一个执行单元,因此不存在多个执行单元同时访问临界区的情况。但是依然存在下面的情形需要保护。

低优先级任务A进入临界区,但此时发生了调度(比如发生了中断,然后从中断中返回),高优先级任务B开始运行访问临界区。

解决方案:进入临界区前禁止抢占就好了。这样即使发生了中断,中断返回也只能回到任务A。

Case2 单处理器-中断上下文抢占

任务A进入临界区,此时发生了中断,中断处理函数中也去访问修改临界区。当中断处理结束时,返回任务A的上下文,但此时临界区已经变了。

解决方案:进入临界区前禁止中断(这样也顺便禁止了抢占)

Case3 多处理器-其他CPU访问

除了单处理器上的问题以外,多处理上还会面临一种需要保护的情形。

任务A运行在CPU_a上,进入临界区关闭了中断(本地),而此时运行在CPU_b上的任务B还是可以进入临界区!没有人能限制它。

解决方案:任务A进入临界区前持有一个互斥结构,阻止其他CPU上的任务进入临界区,知道任务A退出临界区,释放互斥结构。 这样,这个互斥结构就是自旋锁的来历。所以本质上,自旋锁时为了针对SMP体系下的同时访问临界区而设计的,考虑并发问题。

1.3 内核中的自旋锁实现

内核定义

内核使用spinlock结构表示一个自旋锁,如果不开调试信息的话,这个结构就是一个 raw_spinlock:

typedef struct spinlock {
	union {
		struct raw_spinlock rlock;
        // code omitted
	};
} spinlock_t;

将raw_spinlock结构展开,可以看到这是一个体系相关的arch_spinlock_t结构

typedef struct raw_spinlock {
	arch_spinlock_t raw_lock;
    // code omitted
} raw_spinlock_t;

本文只关心常见的x86_64体系来说,这种情况上述结构展开为

typedef struct qspinlock {
	atomic_t	val;
} arch_spinlock_t;

上述的结构时SMP上的定义,对于UP来说。arch_spinlock_t就是一个空结构。

typedef struct { } arch_spinlock_t;

所以自旋锁就是一个原子变量(修改这个变量会LOCK总线,因此可以避免多个CPU同时对其进行修改) 。

1.4 自旋锁API介绍

自旋锁原语锁需要包含的文件是<linux/spinlock.h>。实际的锁具有spinlock_t类型。和其他任何数据结构类似,一个自旋锁必须被初始化。当然初始化可以在编译时通过下面的代码:

spinlock_t my_lock = SPIN_LOCK_UNLOCKED

或者在运行时,调用以下函数:spin_lock_init()

void spin_lock_init(spinlock_t *lock)

# define raw_spin_lock_init(lock)				\
	do { *(lock) = __RAW_SPIN_LOCK_UNLOCKED(lock); } while (0)
	
#define spin_lock_init(_lock)				\
do {							\
	spinlock_check(_lock);				\
	raw_spin_lock_init(&(_lock)->rlock);		\
} while (0)

 最终val会设置为0(对于UP,不存在这个赋值)。

进入临界区之前,我们的代码必须调用下面的函数获取需要的锁:

void spin_lock(spinlock_t *lock);

注意,所有的自旋锁等待,在本质上都是不可以中断的。一旦调用了spin_lock, 就会一直处于自旋状态。

分析一下spin_lock函数:

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

对于UP,raw_spin_lock最后会展开为_LOCK

# define __acquire(x) (void)0

#define __LOCK(lock) \
  do { preempt_disable(); __acquire(lock); (void)(lock); } while (0)

可以看到,就是但单纯地禁止抢占。这是上面Case1的解决方法

对于SMP,raw_spin_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);
}

这里同样会禁止抢占,由于spin_acquire在没设置CONFIG_DEBUG_LOCK_ALLOC时是空操作,所以关键的语句是最后一句,将其展开后是

#define LOCK_CONTENDED(_lock, try, lock) \
	lock(_lock)

所以,真正生效的是

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

__acquire并不重要。而arch_spin_lock定义在include/asm-generic/qspinlock.h,这里会检测val,如果当前锁没有被持有(值为0),那么就通过原子操作将其修改为1并返回 。

否者就调用queued_spin_lock_slowpath一直自旋。

#define arch_spin_lock(l)		queued_spin_lock(l)

static __always_inline void queued_spin_lock(struct qspinlock *lock)
{
	u32 val;

	val = atomic_cmpxchg(&lock->val, 0, _Q_LOCKED_VAL);
	if (likely(val == 0))
		return;
	queued_spin_lock_slowpath(lock, val);
}

以上就是spin_lock()的实现过程,可以发现除了熟知的等待自旋操作之外,会在之前先调用preempt_disable禁止抢占,不过它并没有禁止中断,也就是说解决前面的Case1和Case3的问题,但是 Case2还是有问题。

使用这种自选锁加锁方式时,如果本地CPU发生了中断,在中断上下文中也去获取该自选锁,就会导致死锁

因此,使用spin_lock()需要保证知道该锁不会在该CPU的中断中使用(其他CPU的中断没问题)。

既然有“锁定”就可能必须要“解锁”。解锁时成对使用spin_unlock,基本就是加锁的逆向操作,在设置了val重新为0之后,使能抢占

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();
}

分析spin_lock_irq/spin_unlock_irq

只关注SMP的情形时,相比之前的spin_lock中调用__raw_spin_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);
}

前面说过,实际禁止中断的时候时不会发生抢占的,这里使用preemt_disable禁止抢占是个有点多余的动作。

对于解锁操作,spin_unlock_irq会调用__raw_spin_unlock_irq。相比如前一种方式,多了一个local_irq_enable。

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

这种方式也就解决了Case2。

分析spin_lock_irqsave / spin_unlock_irqsave:

spin_lock_irq中最后使用local_irq_enable打开了中断,如果进入临界区前中断本来就是关闭的,但是通过一进一出,中断竟然变成打开的了,这显然是不合适。

因此就有了spin_lock_irqsave和对应的spin_unlock_irqsave。与上一种的区别在于加锁时将中断使能状态保存在了flags

static inline unsigned long __raw_spin_lock_irqsave(raw_spinlock_t *lock)
{
	unsigned long flags;
	local_irq_save(flags);   // 保存中断状态到flags
	preempt_disable();
	spin_acquire(&lock->dep_map, 0, 0, _RET_IP_);
	do_raw_spin_lock_flags(lock, &flags);
	return flags;
}

 而在对应的解锁时,根据flags中保存的状态对中断状态进行恢复,这样保证在进出临界区前后,中断使能状态是不变的。

static inline void __raw_spin_unlock_irqrestore(raw_spinlock_t *lock,
					    unsigned long flags)
{
	spin_release(&lock->dep_map, 1, _RET_IP_);
	do_raw_spin_unlock(lock);
	local_irq_restore(flags);   // 从 flags 恢复
	preempt_enable();
}

最后,自旋锁使用的一个非常重要的规则就是,自旋锁必须在可能的最短时间内拥有。拥有自旋锁的时间越长,其他处理器不得不自旋以等待释放该自旋锁的时间就越长,而它不得不永远自旋的可能性就越大。

猜你喜欢

转载自blog.csdn.net/Forever_change/article/details/134979591