您的位置:首页 > 其它

关于Spinlock机制的一点思考

2014-11-22 00:11 225 查看
Spin_lock的kernel中的实现对单核(UP),多核(SMP)有不同的处理方式。在这之前经常搞混,重新理一理。 

单核情况:

目前2.6以后的内核都是可抢占的了:

如果spin_lock不处于中断上下文,则spin_lock锁定的代码只会在内核发生抢占的时候才会丢失CPU拥有权。所以,对于单核来说,需要在spin_lock获得锁的时候禁止抢占,释放锁的时候开放抢占。因此这不是真正意义上的锁。

1 内核线程里调用spinlock准备获取临界区资源时,为防止其它高优先级的线程此时被调度导致破坏这段临界区,我们需要禁止抢占。

2 内核线程里调用spinlock准备获取临界区资源时,如果此时有中断产生,中断服务程序也会调用该spinlock,这是很危险的事情。假设线程已经获取到spinlock,那么中断服务程序将会挂起。

3 中断里面获取spinlock时,driver里应该都会成功,一般不会跟其它driver共享spinlock吧。现在内核是不支持中断嵌套的,应该能保证每次中断都能正确获取spinlock。

 

补充一些关于中断的注释:

Linux中的中断处理程序是无需重入的。当一个给定的中断处理程序正在执行时,相应的中断线在所有处理器上都会被屏蔽掉,以防止在一中断线上接收另一个新的中断。通常情况下,所有其他的中断都是打开的,所以这些不同中断线上的其他中断都能被处理,但当前中断线总是被禁止的。由此可以看出,同一个中断处理程序绝对不会被同时调用以处理嵌套的中断这极大的简化了中断处理程序的编写。

说明:

Linux会把处理硬件中断的过程分为两个部分叫做tophalves 和 Bottom halves 上半部分做的工作是时间要求比较紧,操作硬件,或不能被别的中断打断的的重要工作,这时会在所有处理器上屏蔽当前中断线,如果这个中断处理是SA_INTERRUPT标记的,那么所有的本地中断都会全局的被屏蔽掉。下半部分则会恢复响应所有中断,这就使系统处于中断屏蔽状态的时间尽可能的短了,中断响应能力自然也就高了。下半部分完成的工作对时间也不那么敏感,和硬件无关了,可以稍后点执行。

 

再补充一些关于bottom halves的注释:

一般都是在中断退出irq_exit函数里会去调用do_softirq,去调度tasklet等执行,基本上每次中断退出,tasklet也会完成。但也有一些极限情况,假设如果中断来的太频繁,应该会将部分tasklet的执行换到内核守护线程ksoftirq去完成。

 

举例:

进程A中调用了spin_lock(&lock)然后进入临界区,此时来了一个中断(interrupt),该中断也运行在和进程A相同的CPU上,并且在该中断处理程序中恰巧也会spin_lock(&lock)试图获取同一个锁。由于是在同一个CPU上被中断,进程A会被设置为TASK_INTERRUPT状态,

中断处理程序无法获得锁,会不停的忙等,由于进程A被设置为中断状态,schedule()进程调度就无法再调度进程A运行,这样就导致了死锁!但是如果该中断处理程序运行在不同的CPU上就不会触发死锁。因为在不同的CPU上出现中断不会导致进程A的状态被设为TASK_INTERRUPT,只是换出。当中断处理程序忙等被换出后,进程A还是有机会获得CPU,执行并退出临界区。

所以在使用spin_lock时要明确知道该锁不会在中断处理程序中使用。如果有,那就需要使用spinlock_irq_save,该函数即会关抢占,也会关本地中断。

 

多核情况:

存在两段代码同时在多核上执行的情况,这时候才需要一个真正的锁来宣告代码对资源的占有。

几个核可能会同时access临界区,这时的spinlock是如何实现的呢?

要用到CPU提供的一些特殊指令,对lock变量进行原子操作。

SMP中spin_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 intdo_raw_spin_trylock(raw_spinlock_t *lock){    return arch_spin_trylock(&(lock)->raw_lock);}

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

 

ARM中spin_lock的实现

static inline voidarch_spin_lock(arch_spinlock_t *lock)

{

unsigned long tmp;

__asm__ __volatile__("

1: ldrex %0, [%1]\n"

@将&lock->lock地址中的值,即lock->lock加载到tmp中,并设置&lock->lock为独占访问"

teq %0, #0\n"

@测试tmp是否为0

 

WFE("ne")

@不为0,则执行WFE指令。不为0,代表锁已被锁定,则通过WFE指令进入suspendmode(clock停止),直到该锁被释放时发出的SEV指令,CPU才会跳出suspend mode"

strexeq %0, %2, [%1]\n"

@将lock->lock加1,并解除lock->lock的锁定状态,tmp中存入返回状态"

teqeq %0, #0\n"

@如果执行成功,则tmp为0,成功获得所"

bne 1b"

@如果执行不成功,则tmp不为0,跳转到标号1处,继续获得锁。

 

: "=&r" (tmp)

: "r" (&lock->lock),"r" (1) : "cc")

smp_mb(); }

代码是一段内联汇编。Tmp为输出,放在寄存器中,在代码中以%0表示,&lock->lock为输入参数1,放在寄存器中,在代码中以%1表示,常数

 

1为输入参数2,放在寄存器中,在代码中以2%表示。

代码中,ldrex/strex以及WFE指令是关键。因lock->lock放在内存中,那么将lock->lock加1这一操作会经过读取内存,+1,写内存的操作,这一过程如果不是原子操作,那么其他核有可能在这一过程中访问lock->lock,造成错误。Ldrex/strex是ARM在arm v6中新增的指令,用于对内存区域的独占访问,WFE指令则可以在空等时间内暂停CPU的时钟,以达到省电的目的。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: