我们使用ULK所介绍的Linux内核2.6.11版本。
在linux内核代码中持有spinlock时为什么不能够睡眠。
首先,本质原因是spinlock的设计目的是保证数据修改的原子性,因此没有理由在spinlock锁住的区域内停留。
然后我们来看具体实现上的原因。阅读内核源码之后发现,持有spinlock时为什么不能够睡眠的原因与SMP和内核抢占紧密相关。所以我们分以下四种情况来讨论:
1. 单处理器不可抢占(!CONFIG_SMP &&!CONFIG_PREEMPT):
这种配置下,在内核中与spinlock相关的具体实现如下
#define_spin_lock(lock)
\
do { \
preempt_disable(); \空
_raw_spin_lock(lock); \空
__acquire(lock); \空
} while(0)
#definepreempt_disable()
do { } while (0)
#define_raw_spin_lock(lock)
do { (void)(lock); } while(0)
即,实际上此时spin_lock()是空操作。
对于spinlock来说,在此种配置下,正常情况:当前进程只能够被中断抢占(如果使用spin_lock_irq()甚至中断都不能够抢占),其他任何进程都不能换入,直到本进程完成临界区的执行。如果持有spinlock时睡眠:则会换入其他进程,如果这个进程正好也需要
这个锁,最好的情况下该进程要等待很长的时间,最坏的情况下系统出现了死锁状态。
2. 单处理器可抢占(!CONFIG_SMP &&CONFIG_PREEMPT):
这种配置下,在内核中与spinlock相关的具体实现如下
#define_spin_lock(lock)
\
do { \
preempt_disable(); \
_raw_spin_lock(lock); \空
__acquire(lock); \空
} while(0)
#define preempt_disable() \
do { \
inc_preempt_count(); \
barrier(); \
} while (0)
实际此时spin_lock()上仅仅做了禁止抢占的操作,而且在禁止抢占之后单独占有CPU,就与第一种单处理器不可抢占的情况完全相同了。
3. 多处理器不可抢占(CONFIG_SMP &&!CONFIG_PREEMPT):
这种配置下,在内核中与spinlock相关的具体实现如下
void __lockfunc _spin_lock(spinlock_t *lock)
{
preempt_disable();
_raw_spin_lock(lock);
}
#definepreempt_disable()
do { } while (0)
static inline void _raw_spin_lock(spinlock_t *lock)
{
__asm__ __volatile__(
spin_lock_string
:"=m" (lock->slock) : : "memory");
}
#define spin_lock_string \
"\n1:\t" \
"lock ; decb %0\n\t" \
"jns 3f\n" \
"2:\t"\
"rep;nop\n\t" \
"cmpb $0,%0\n\t" \
"jle 2b\n\t"\
"jmp 1b\n" \
"3:\n\t"
spin_lock()在这种配置下的操作实际上是做了对lock的原子减1,特点是在自旋等待获取lock时不可被抢占。
a.对于spinlock来说,正常情况:当前进程在本处理器上只能被中断抢占,在其他处理器上若有进程想要访问该临界区,则由于试图获取被当前进程持有的lock而自旋等待,直到本进程执行完临界区,其他处理器上的等待进程才能进入临界区。如果当前进程在持有spinlock的时候睡眠:则本处理器上换入了其他进程,如果之后换入了一个想要获取同一个自旋锁执行同一段临界区的进程,则会停在自旋检测lock的值处,若多处理器上均换入了这样的进程,而原始的进程始终没得到机会执行并跳出临界区,则此时系统会死锁崩溃。
b.对于信号量来说:假设当前进程持有信号量然后睡眠,因为其他试图执行相同临界区的进程在获取信号量的时候不会自旋等待,这些进程会直接把自己放入等待队列然后调度自己,等待原始进程在执行完临界区之后做唤醒操作,所以不会产生死锁。
4.多处理器可抢占(CONFIG_SMP&&CONFIG_PREEMPT):
void __lockfunc _##op##_lock(locktype##_t*lock)
\
{
\
preempt_disable();
\
for (;;){
\
if(likely(_raw_##op##_trylock(lock)))
\
break;
\
preempt_enable();
\
if(!(lock)->break_lock)
\
(lock)->break_lock =1;
\
while (!op##_can_lock(lock) &&(lock)->break_lock)
\
cpu_relax();
\
preempt_disable();
\
}
\
}
\
\
EXPORT_SYMBOL(_##op##_lock);
#define preempt_disable() \
do { \
inc_preempt_count(); \
barrier(); \
} while (0)
static inline int _raw_spin_trylock(spinlock_t *lock)
{
char oldval;
__asm__ __volatile__(
"xchgb �,%1"
:"=q" (oldval), "=m" (lock->slock)
:"0" (0) : "memory");
return oldval > 0;
}
spin_lock()在这种配置下的操作实际上是做了preempt_disable()和对lock的原子减1,特点是在自旋等待获取lock时可被抢占。
与第3种情况不同的是此种配置下加入了自旋等待时可以被抢占的特性,本来的目的是减少自旋占用的系统时间。这个特性带来了一定改变:发生前述可能会导致死锁的情况时,由于可被抢占,系统仍然能够响应优先级高的进程,假如原始进程优先级很高,则可能解开死锁状态;如果没有解开,则自旋等待获取lock的进程在耗尽时间片后可能会被调入过期队列,然后原始进程可能会获得执行,解开死锁状态。但都只是可能,仍然有死锁的可能性存在。
信号量的情况则与第3种相同。
总结:
spinlock的具体实现与对称多处理器和内核抢占相关,在SMP和PREEMPT分别开启关闭一共四种的配置情况下,持有spinlock的时候睡眠均有可能产生灾难性的后果。即使碰巧不出现死锁或破坏临界区的情况,在持有spinlock的时候睡眠仍然是对系统资源的严重浪费,会导致系统性能严重下降,这也是持有spinlock的时候不可以睡眠的原因之一。
补充:
CONFIG_PREEMPT_NONE表示内核不可抢占,适用于计算型任务系统,允许很长的延时。
CONFIG_PREEMPT表示内核允许抢占,及优先级高的进程可以抢占优先级低的进程。毫秒级低延时系统。
http://blog.sina.com.cn/s/blog_953826fc01011dtc.html
http://www.ibm.com/developerworks/cn/linux/l-real-time-linux/ (PS:发现IBM服务器上文章都不错啊)