(笔记)spin lock

Linux spin_lock的实现

Spin_lockLinux内核的一种同步机制。内核代码可以通过获得spin_lock宣称对某一资源的占有,直到其释放该spin_lock;如果内核代码试图获得一个已经锁定的spin_lock,则这部分代码会一直忙等待,直到获得该spin_lock

 Spin_lockkernel中的实现对单核(UP),多核(SMP)有不同的处理方式。对单核来说,如果spin_lock不处于中断上下文,则spin_lock锁定的代码丢失CPU拥有权,只会在内核抢占的时候发生。所以,对于单核来说,只需要在spin_lock获得锁的时候禁止抢占,释放锁的时候开放抢占。对多核来说,存在两段代码同时在多核上执行的情况,这时候才需要一个真正的锁来宣告代码对资源的占有。

 include/linux/spinlock.h文件中,给出了UPSMP所涉及的不同的头文件,也很清楚的将两者实现的不同体现出来。

UP中spin_lock的实现

实现在include/linux/spinlock_api_up.h

/* * In the UP-nondebug case there's no real locking going on, so the * only thing we have to do is to keep the preempt counts and irq * flags straight, to suppress compiler warnings of unused lock * variables, and to add the proper checker annotations: */
#define __LOCK(lock) \ 
do { 
preempt_disable();
 __acquire(lock); 
(void)(lock); 
} while (0)
#define _raw_spin_lock(lock) __LOCK(lock)


代码表明,spin_lockUP中实际上被处理为三个语句:
 
 

preempt_disable();

__acquire(lock);

(void)(lock);


Preempt_disable()将当前进程的preempt_count加1,表示禁止内核抢占,那么内核从中断上下文返回时不会发生进程调度。

__acquire(lock)只是使用sparse工具对lock进行检查,否则该宏为空。

另在make 中加入C=1/C=2的参数,则会导致编译时进行sparse检查。

(void)(lock)仅仅是为了防止编译器对lock的未使用报警。


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

同样,SMP上的实现被分解为三句话。

Preempt_disable()不用解释

Spin_acquire()同样是sparse检查需要

LOCK_CONTENDED()是一个宏,如果不考虑CONFIG_LOCK_STAT(该宏是为了统计lock的操作),则:

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

则第三句话等同于:

do_raw_spin_lock(lock)

而do_raw_spin_lock()则可以从spinlock.h中找到痕迹:

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

看到 arch ,我们明白这个函数是体系相关的。下面分别分析 ARMx86 体现结构下该函数的实现。

ARMspin_lock的实现

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

	__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();
}
Ldrex/strexARMarm v6 中新增的指令,用于对内存区域的独占访问 , WFE 指令则可以在空等时间内暂停 CPU 的时钟,以达到省电的目的。




前阵子有网友发短消息问:“...在研究自旋锁的时候,发现在 spin_lock_irq函数,也就是在自旋锁中关闭中的这类函数中,既然已经关闭了本地中断,再禁止抢占有没有多余。也就是说,既然本地中断已经禁止 了,在本处理器上是无法被打断的,本地调度器也无法运行,也就不可以被本地调度程序调度出去..."

从spinlock设计原理看,使用它的时候,在临界区间是务必确保不会发生进程切换。现在的问题是,如果已经关闭了中断,在同一处理器上如果不关掉内核抢占的特性,会不会有进程调度的情况发生,如果没有,那我个人的理解是,在local_irq_disable之后再使用peempt_disable就多此一举了。

这个在SMP系统上最好理解了,假设有A和B两个处理器,使用spin lock的进程(简称"焦点进程"好了)运行在处理器A上,一种很明显的情形就是如果有个进程(简称“睡眠进程”好了)先于焦点运行,但是因为等待网卡的一个数据包,它进入了sleep状态,然后焦点开始被调度运行,后者在spin lock获得锁后进入临界区,此时网卡收到了"睡眠进程“的数据包,因为焦点只是关闭了A上的中断,所以B还是会接收并处理该中断,然后唤醒“睡眠进程“,后者进入运行队列,此时出现一个调度点,如果”睡眠“的优先级高于”焦点“,那么就有进程切换发生了,但是如果焦点所使用的spin lock中关闭了内核抢占,那么就使得先前的进程切换成为不可能。

上面标红色这个例子个人认为是错误的,  在SMP的情况下, 任务是和核绑定的, 因此每个cpu只能调度自己的队列里面的任务, 当A处理器关闭掉中断后, 是不响应任何外部中断的, 而且核间中断也会被屏蔽. 那么当B处理器接收到中断后,并不能唤醒A处理器调度队列里的任务, A处理器就不会发生调度.进而不会发生任务切换.


如果是在单处理器系统上,local_irq_disable实际上关闭了所有(其实就一个)处理器的中断,所有有中断引起的调度点都不可能存在,此时有无其他与中断无关的调度点出现呢?在2.4上,因为没有抢占,这种情形绝无可能,事实上,早期的内核很大程度上是依赖local_irq_disable来做资源保护,这个看看2.4的内核源码就很清楚了,里面有大量的对local_irq_disable函数的直接调用。2.6有了抢占的概念,UP下关闭中断,如前所述,实际上已经杜绝了内部因素导致的“就绪队列中加入一个进程”这个调度点的可能(内部因素实际上只剩下了一个处理器的异常,但是关中断的情形下,即便有异常也不会导致进程的切换),因此到这里我们可以这样说,在UP上关闭中断情形下,preempt_disable其实是多余的。但是我们知道,spin lock是一种内核API,不只是kernel的开发者在用,更多的内核模块(.ko,实际当中更多地表现形式是设备驱动程序)开发者也在使用。内核的设计者总是试图将其不能控的代码(所谓的外部因素了)可能给内核带来的损失降低至最小的程度,这个表现在内核对中断处理框架的设计时尤其明显,所以在UP系统下先后使用local_disable_irq和preempt_disable,只是尽量让你我可能在spin lock/unlock的临界区中某些混了头的代码不至于给系统带来灾难,因为难保某些人不会在spin lock的临界区中,比如去wake_up_interruptible()一个进程,而被唤醒的进程在可抢占的系统里就是一个打开的潘多拉盒子。


//

下面是自己的理解和总结:

中断, 抢占和锁是三个独立的概念, 不能混淆.  

spin_lock . 在单处理器上, 如果不考虑任务切换,那么同一时间只有一个进程在运行状态, 这时候不需要锁. 但是由于调度机制的存在,  进程如果在执行临界代码时发生进程切换(被换出).这时新换入的进程可能会进入相同的临界代码, 这时候需要锁. 但那样会有锁死风险, 所以持有spin_lock的进程绝对不能在执行临界代码时发生进程切换. 换句话说单核下只要遇到需要使用锁的情况就可能有死锁的风险.所以在单核下通过禁止内核抢占来实现spin_lock(等于直接禁止了使用锁的情况的出现), 但这样做并不是最安全的,最安全的是同时禁止抢占和中断, 这样就能防止在中断处理程序中的代码请求自旋锁从而导致锁死这种case.

对于多处理器,同样最安全的做法是同时禁止中断和内核抢占, 如果是仅仅禁用内核抢占,有可能发生在中断处理程序中请求锁这种特殊情况.

由上面所说的可知,在多处理器下,禁止了本地中断,等于禁止了本地调度,所以我认为无论是单核还是多核下禁止了中断就不需要再禁止内核抢占了.


还有一点,在UP下持有spin_lock的进程绝对不能在执行临界代码时发生进程切换,那样会有锁死风险。但SMP下的spin lock禁止抢占不是因为怕被死锁,因为SMP下被抢占的进程可以调度到其他核运行。SMP下的spin lock禁止抢占的原因个人认为是尽量缩短一个进程持有锁的时间,好让其他进程等待锁的时间尽量短。举两个例子:

(1)假设CPU1上的进程(A)持有一个锁,这时CPU2上的进程(B)请求同样的锁(进入忙等待), 这时候如果CPU1上的进程A被切换, 那么这样的话进程B等待获得锁的时间是不可预期的, 违背了自旋锁的设计初衷.所以才说"持有自旋锁的进程绝对不能在执行临界代码时发生进程切换"

(2)假设中断没有关闭,CPU1上的进程(A)持有一个锁,这时如果发生了任务切换,进程(B)开始运行, 进程(B)又请求同样的锁,这时虽然不会发生死锁(理由如上所述:(A)可以被调度到CPU2上继续运行),但是进程(A)被调度到其他核上继续运行的时间不可预期,同样违背了自旋锁的设计初衷.所以才说"持有自旋锁的进程绝对不能在执行临界代码时发生进程切换"。


最后补充一点: spin lock不仅用于声明对某一资源(变量)的独占式访问(实现对变量的原子性访问)。对于一组有逻辑相关性的操作(比如需要这组操作原子性完成),也要使用spin lock来保证在SMP下并发执行时状态不会乱掉。


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值