【Linux驱动篇】同步机制(1)— 自旋锁

一、概述

  自旋锁是内核编程中常见的上锁方式。对于自旋锁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(),这样一来可以避免一切核间并发的可能性,并同时也避免和一切核内并发的可能性。
  使用自旋锁应注意以下几个问题:

  1. 由于自旋锁属于忙等待的方式,所以自旋锁适用于临界区耗时很小的情况,如果临界区很大,使用自旋锁会非常耗费CPU性能
  2. 同一个CPU递归使用自旋锁会造成死锁,这一点跟互斥锁是一样的
  3. 在自旋锁锁定期间不能调用可能引起进程调度的函数,或者引起阻塞的函数,比如copy_to_user()、copy_from_user()、kmalloc()、msleep()等等函数,因为自旋锁上锁期间会禁止抢占,无法执行调度,所以会造成内核崩溃
  4. 由于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

  • 5
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值