一、概述
自旋锁是内核编程中常见的上锁方式。对于自旋锁spinlock_t,若A上锁后,B获取锁,此时B会在原地等待,不会释放CPU,直到A释放互斥锁,B才能获得锁。
二、自旋锁
自旋锁属于忙等待的方式,所以适用于临界区耗时很短的情况。自旋锁主要针对SMP(对称多处理器)或单CPU但内核可抢占的情况,对于单CPU和内核不支持抢占的系统,自旋锁退化为空操作。
//定义自旋锁
spinlock_t lock;
//初始化自旋锁
spin_lock_init(&lock);
//获得自旋锁
spin_lock(&lock);
spin_trylock(&lock);
//释放自旋锁
spin_unlock(&lock);
spin_lock如果能够立即获得锁,就会马上返回,否则它将在原地打转,知道该锁持有者释放,而spin_trylock如果立即获得锁,则返回true,若没有获得,也会立即返回false。
使用自旋锁后,可以保证临界区不受本CPU和别的CPU进程抢占的影响,但是还有可能受到中断和底半部(bottom half)的影响,所以若中断也要访问临界区资源,则需要用到以下函数
spin_lock_irq() = spin_lock() + local_irq_disable() //禁止本地中断,并获取自旋锁
spin_unlock_irq() = spin_unlock() + local_irq_enable() //激活本地中断,并释放自旋锁
spin_lock_irqsave() = spin_lock() + local_irq_save() //保存中断状态,禁止本地中断,并获取自旋锁
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore() //将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁
spin_lock_bh() = spin_lock() + local_bh_disable() //禁止底半部中断,并获取自旋锁
spin_unlock_bh() = spin_unlock() + local_bh_enable() //激活底半部中断,并释放自旋锁
在进程上下文中调用spin_lock_irqsave(flags)和spin_unlock_irqrestore(flags),中断上下文中调用spin_lock()和spin_unlock(),这样一来可以避免一切核间并发的可能性,并同时也避免和一切核内并发的可能性。
使用自旋锁应注意以下几个问题:
- 由于自旋锁属于忙等待的方式,所以自旋锁适用于临界区耗时很小的情况,如果临界区很大,使用自旋锁会非常耗费CPU性能
- 同一个CPU递归使用自旋锁会造成死锁,这一点跟互斥锁是一样的
- 在自旋锁锁定期间不能调用可能引起进程调度的函数,或者引起阻塞的函数,比如copy_to_user()、copy_from_user()、kmalloc()、msleep()等等函数,因为自旋锁上锁期间会禁止抢占,无法执行调度,所以会造成内核崩溃
- 由于spin_lock_irqsave并不能屏蔽别的CPU中断,所以进程上下文调用了spin_lock_irqsave和spin_unlock_irqrestore,中断里最好也调用spin_lock和spin_unlock
三、自旋锁源码分析
以下是spin_lock调用流程,在__raw_spin_lock中,先调用preempt_disable()关闭抢占,具体是怎么实现抢占关闭的,稍后分析。最主要的函数就是do_raw_spin_lock()—>arch_spin_lock(),里面是具体的汇编语言,不做过多的说明,也不太了解。可能就是在这个汇编里加锁循环的,若加锁失败就一直重复直到加锁成功然后返回。
// include/linux/spinlock_type.h
typedef struct raw_spinlock {
arch_spinlock_t raw_lock;
#ifdef CONFIG_GENERIC_LOCKBREAK
unsigned int break_lock;
#endif
#ifdef CONFIG_DEBUG_SPINLOCK
unsigned int magic, owner_cpu;
void *owner;
#endif
#ifdef CONFIG_DEBUG_LOCK_ALLOC
struct lockdep_map dep_map;
#endif
} raw_spinlock_t;
// arch/arm/include/asm/spinlock_type.h
typedef struct {
volatile unsigned int lock;
} arch_spinlock_t;
// include/linux/spinlock.h
#define raw_spin_lock(lock) _raw_spin_lock(lock)
#define raw_spin_unlock(lock) _raw_spin_unlock(lock)
static inline void spin_lock(spinlock_t *lock)
{
raw_spin_lock(&lock->rlock);
}
static __always_inline void spin_unlock(spinlock_t *lock)
{
raw_spin_unlock(&lock->rlock);
}
static inline void do_raw_spin_lock(raw_spinlock_t *lock) __acquires(lock)
{
__acquire(lock);
arch_spin_lock(&lock->raw_lock);
}
static inline void do_raw_spin_unlock(raw_spinlock_t *lock) __releases(lock)
{
arch_spin_unlock(&lock->raw_lock);
__release(lock);
}
// include/linux/spinlock_api_smp.h
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 __raw_spin_unlock(raw_spinlock_t *lock)
{
spin_release(&lock->dep_map, 1, _RET_IP_);
do_raw_spin_unlock(lock);
preempt_enable();
}
//include/linux/preempt.h
#define add_preempt_count(val) do { preempt_count() += (val); } while (0)
#define inc_preempt_count() add_preempt_count(1)
#define dec_preempt_count() sub_preempt_count(1)
#define preempt_count() (current_thread_info()->preempt_count)
#define preempt_disable() \
do { \
inc_preempt_count(); \
barrier(); \
} while (0)
//arch/arm/include/asm/spinlock.h
static inline void arch_spin_lock(arch_spinlock_t *lock)
{
unsigned long tmp;
__asm__ __volatile__(
"1: ldrex %0, [%1]\n"
" teq %0, #0\n"
WFE("ne")
" strexeq %0, %2, [%1]\n"
" teqeq %0, #0\n"
" bne 1b"
: "=&r" (tmp)
: "r" (&lock->lock), "r" (1)
: "cc");
smp_mb();
}
static inline void arch_spin_unlock(arch_spinlock_t *lock)
{
smp_mb();
__asm__ __volatile__(
" str %1, [%0]\n"
:
: "r" (&lock->lock), "r" (0)
: "cc");
dsb_sev();
}
四、preempt_disable()是如何关闭抢占的
从代码上看,preempt_disable只是使preempt_count()计数加1,那么它是如何禁止抢占的呢?
以下情况会发生内核抢占:
- 当中断处理程序退出时,返回内核空间。
- 当内核代码再次被抢占时。
- 如果内核中的任务显式调用schedule()
- 如果该内核中的任务阻塞(导致对schedule()的调用),比如调用sleep_on,wait_event,msleep等函数
禁止内核抢占的情况列出如下:
- 内核执行中断处理例程时不允许内核抢占,中断返回时再执行内核抢占。
- 当内核执行软中断或tasklet时,禁止内核抢占,软中断返回时再执行内核抢占。
- 在临界区禁止内核抢占,临界区保护函数通过抢占计数宏控制抢占,计数大于0,表示禁止内核抢占。
在arch/arm/kernel/entry-armv.S中,有如下汇编,由于汇编不熟,分析仅作参考:
(1) 第5行和第6行获取抢占计数preempt_count和是否需要调度的标志flags
(2)第7行判断抢占计数不为0,那就把标志flags清0
(3)第8行判断r0寄存器的值和_TIF_NEED_RESCHED(在驱动代码中值为2)的值:不相等,所以不会执行blne svc_preempt跳转指令,直接执行svc_exit,恢复中断现场,回到内核态运行用户进程。若相等,就会执行preempt_schedule_irq进行抢占。具体代码在kernel/sched/core.c中找到preempt_schedule_irq函数。
__irq_svc:
svc_entry //保护中断现场
irq_handler //执行中断函数
#ifdef CONFIG_PREEMPT
ldr r8, [tsk, #TI_PREEMPT] @ get preempt count
ldr r0, [tsk, #TI_FLAGS] @ get flags
teq r8, #0 @ if preempt count != 0
movne r0, #0 @ force flags to 0
tst r0, #_TIF_NEED_RESCHED
blne svc_preempt
#endif
svc_exit r5, irq = 1 @ return from exception
UNWIND(.fnend )
ENDPROC(__irq_svc)
.ltorg
#ifdef CONFIG_PREEMPT
svc_preempt:
mov r8, lr
1: bl preempt_schedule_irq @ irq en/disable is done inside
ldr r0, [tsk, #TI_FLAGS] @ get new tasks TI_FLAGS
tst r0, #_TIF_NEED_RESCHED
reteq r8 @ go again
b 1b
#endif
那什么时候r0(flags)会等于_TIF_NEED_RESCHED呢?这个应该是周期性调度的函数决定的。
具体产生调度的时间点,一种是主动调度,需要代码主动执行schedule()函数,一种是周期性调度scheduler_tick(),伴随系统的tick中断自动调用。
在kernel/sched/core.c中,可以找到scheduler_tick()函数,主要调用如下:
—> curr->sched_class->task_tick(rq, curr, 0);
—> task_tick_fair
—> entity_tick
—>check_preempt_tick
if (delta_exec > ideal_runtime)判断当前进程已运行时间和理想时间比较,如果超过了,就会执行resched_curr()函数设置flags标志为TIF_NEED_RESCHED,标志需要进行一次调度。
// in kernel/sched/core.c
/*
* This function gets called by the timer code, with HZ frequency.
* We call it with interrupts disabled.
*/
void scheduler_tick(void)
{
int cpu = smp_processor_id();
struct rq *rq = cpu_rq(cpu);
struct task_struct *curr = rq->curr;
struct rq_flags rf;
sched_clock_tick();
rq_lock(rq, &rf);
update_rq_clock(rq);
curr->sched_class->task_tick(rq, curr, 0);
cpu_load_update_active(rq);
calc_global_load_tick(rq);
rq_unlock(rq, &rf);
perf_event_task_tick();
#ifdef CONFIG_SMP
rq->idle_balance = idle_cpu(cpu);
trigger_load_balance(rq);
#endif
rq_last_tick_reset(rq);
}
//in kernel/sched/fair.c
static void
check_preempt_tick(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
unsigned long ideal_runtime, delta_exec;
struct sched_entity *se;
s64 delta;
ideal_runtime = sched_slice(cfs_rq, curr);
delta_exec = curr->sum_exec_runtime - curr->prev_sum_exec_runtime;
if (delta_exec > ideal_runtime) {
resched_curr(rq_of(cfs_rq));
/*
* The current task ran long enough, ensure it doesn't get
* re-elected due to buddy favours.
*/
clear_buddies(cfs_rq, curr);
return;
}
/*
* Ensure that a task that missed wakeup preemption by a
* narrow margin doesn't have to wait for a full slice.
* This also mitigates buddy induced latencies under load.
*/
if (delta_exec < sysctl_sched_min_granularity)
return;
se = __pick_first_entity(cfs_rq);
delta = curr->vruntime - se->vruntime;
if (delta < 0)
return;
if (delta > ideal_runtime)
resched_curr(rq_of(cfs_rq));
}
当执行自旋锁解锁spin_unlock,调用preempt_enable开启抢占。调用流程为:
—>spin_unlock
—> preempt_enable
—> preempt_count_dec_and_test //抢占计数减1
—> preempt_schedule 执行调度
若在上锁期间手动调用schedule,则会执行schedule_debug打印error
五、自旋锁上锁期间不允许睡眠
由于自旋锁上锁期间关闭了本CPU的抢占,所以如果此时执行了睡眠,但又无法执行CPU调度,可能导致进程无法唤醒而睡死过去。
No pains, no gains