我們使用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表示核心允許搶占,及優先級高的程序可以搶占優先級低的程序。毫秒級低延時系統。