我們先考慮單CPU的情況。在這樣情況下,不管在什麼執行級別,我們只要簡單地把CPU的中斷關掉就可以達到獨佔處理的目的。從這個角度來說,spinlock的實現簡單地令人乍舌:cli/sti。只要這樣,我們就關閉了preemption帶來的複雜之門。
單CPU的情況很簡單,多CPU就不那麼簡單了。單純地關掉當前CPU的中斷並不會給我們帶來好運。當我們的代碼存取一個shared variable時,另一顆CPU隨時會把數據改得面目全非。我們需要有手段通知它(或它們,你知道我的意思)——spinlock正爲此設。這個例子是我們的第一次嘗試:
extern spinlock_t lock;
// ...
spin_lock(&lock);
// do something
spin_unlock(&lock);
他能正常工作嗎?答案是有可能。在某些情況下,這段代碼可以正常工作,但想一想會不會發生這樣的事:
// in normal run level
extern spinlock_t lock;
// ...
spin_lock(&lock);
// do something
// interrupted by IRQ ...
// in IRQ
extern spinlock_t lock;
spin_lock(&lock);
喔,我們在normal級別下獲得了一個spinlock,正當我們想做什麼的時候,我們被interrupt打斷了,CPU轉而執行interrupt level的代碼,它也想獲得這個lock,於是“死鎖”發生了!解決方法很簡單,看看我們第二次嘗試:
extern spinlock_t lock;
// ...
cli; // disable interrupt on current CPU
spin_lock(&lock);
// do something
spin_unlock(&lock);
sti; // enable interrupt on current CPU
在獲得spinlock之前,我們先把當前CPU的中斷禁止掉,然後獲得一個lock;在釋放lock之後再把中斷打開。這樣,我們就防止了死鎖。事實上,Linux提供了一個更爲快捷的方式來實現這個功能:
extern spinlock_t lock;
// ...
spin_lock_irq(&lock);
// do something
spin_unlock_irq(&lock);
如果沒有nested interrupt,所有這一切都很好。加上nested interrupt,我們再來看看這個例子:
// code 1
extern spinlock_t lock;
// ...
spin_lock_irq(&lock);
// do something
spin_unlock_irq(&lock);
// code 2
extern spinlock_t lock;
// ...
spin_lock_irq(&lock);
// do something
spin_unlock_irq(&lock);
Code 1和code 2都運行在interrupt context下,由於中斷可以嵌套執行,我們很容易就可以想到這樣的運行次序:
Code 1 Code 2
extern spinlock_t lock;
// ...
spin_lock_irq(&lock);
extern spinlock_t lock;
// ...
spin_lock_irq(&lock);
// do something
spin_unlock_irq(&lock);
// do something
spin_unlock_irq(&lock);
問題是在第一個spin_unlock_irq後這個CPU的中斷已經被打開,“死鎖”的問題又會回到我們身邊!
解決方法是我們在每次關閉中斷前紀錄當前中斷的狀態,然後恢復它而不是直接把中斷打開。
unsigned long flags;
local_irq_save(flags);
spin_lock(&lock);
// do something
spin_unlock(&lock);
local_irq_restore(flags);
Linux同樣提供了更爲簡便的方式:
unsigned long flags;
spin_lock_irqsave(&lock, flags);
// do something
spin_unlock_irqrestore(&lock, flags);