[Linux]互斥機制(中斷遮蔽、原子操作、自旋鎖、訊號量)
基本概念
臨界區
對某段程式碼而言,可能會在程式中多次被執行,每次執行的過程我們稱作程式碼的執行路徑。
當兩個或多個程式碼路徑要競爭共同的資源的時候,該程式碼段就是臨界區。
互斥機制
訪問共享資源的程式碼叫做臨界區。共享資源被多個執行緒需要,但共享資源又不能被同時訪問。
所以臨界區需要以某種互斥機制來加以保護,確保共享資源被互斥訪問。
使用者空間和核心空間
為了安全考慮,Linux系統分為核心態和使用者態,分別執行在核心空間和使用者空間。
核心態的程式可以執行特權指令,作業系統本身也在其中執行;
使用者態則不允許直接訪問作業系統的核心資料、裝置等關鍵資源,必須先通過系統呼叫或者中斷進入核心態才可以訪問,當系統呼叫或中斷返回時,重新回到使用者空間執行。
Linux 的互斥機制
四種方式:中斷遮蔽、原子操作、自旋鎖、訊號量
核心空間互斥方式:中斷遮蔽、原子操作、自旋鎖
使用者空間互斥方式:訊號量
中斷遮蔽
中斷是一個完全非同步的事件,它的發生與正在執行的程序沒有任何關係,它沒有程序上下文切換。
CPU具備遮蔽中斷和開啟中斷的功能,這項功能可以保證正在執行的核心執行路徑不被中斷處理程式搶佔,防止競態的產生。
但是,核心的正常執行依賴於中斷機制。在遮蔽中斷期間,任何中斷都無法得到處理,而必須等待遮蔽解除。因此長時間遮蔽中斷對核心的執行起到很大的影響,其後果可能導致資料丟失,甚至系統崩潰。
實際情況是:在中斷服務全過程遮蔽中斷會丟失中斷;如果開中斷,又容易引起互斥問題。
為了解決這個問題,Linux 把中斷分為頂半部TH(Top Half)和底半部BH(Bottom Half)。
TH 遮蔽中斷,執行一些少量的關鍵性動作;BH 可以開中斷,允許中斷延遲執行。
原子操作
原子操作底層表現為一條彙編指令(ldrex、strex)。所以他們在執行過程中不會被別的程式碼路徑所中斷。
Linux 核心提供了兩類函式來實現核心中的原子操作,分別是整型原子操作和位原子操作。它們的共同點是所有的操作都是原子的,核心可以安全的呼叫它們而不被中斷,而且它們都依賴底層CPU的原子操作實現,因此所有的這些函式都是與CPU架構相關的。
自旋鎖
自旋鎖是為實現保護共享資源而提出一種鎖機制。
自旋鎖的原理:
一個執行單元要想訪問被自旋鎖保護的共享資源,必須先得到鎖,並且在任何時刻最多隻能有一個執行單元獲得鎖;
而在訪問完共享資源後,必須釋放鎖。
如果在獲取自旋鎖時,沒有任何執行單元保持該鎖,那麼將立即得到鎖;
如果在獲取自旋鎖時鎖已經有保持者,那麼獲取鎖操作將一直迴圈在那裡,直到該自旋鎖的保持者釋放了鎖,”自旋”一詞就是因此而得名。
事實上,自旋鎖的初衷是:在短期間內進行輕量級的鎖定。一個被爭用的自旋鎖使得請求它的執行緒在等待鎖重新可用的期間進行自旋(特別浪費處理器時間),所以自旋鎖被持有的時間不應該過長。如果需要長時間鎖定的話, 最好使用訊號量。
訊號量
在使用者空間只有程序的概念。當一個臨界區有多個使用者態程序競爭時,最好的方法是用訊號量保護這個臨界區。
只有得到訊號量程序才能執行臨界區程式碼,當獲取不到訊號量時,程序進入休眠狀態。
因此,我們可以說,訊號量是程序級的互斥機制,它代表程序來爭奪共享資源,如果競爭失敗,就會發生程序上下文切換,當前程序進入睡眠狀態,CPU執行其他程序。由於程序上下文切換的開銷很大,因此,只有當程序佔用資源時間較長時,用訊號量才是最好的選擇。
此外,訊號量在SMP(對稱多處理器)系統同樣起作用。
淺顯的比方
淺顯的來說,可以理解為大家在一套房子裡合租,共用一個廁所。廁所就是共享資源,去上廁所的行為被稱作程式碼路徑。
中斷遮蔽就是,有一個人想要用廁所,但是呢他在上廁所前在門口貼上紙條說廁所壞了,如果他很快出來倒還不要緊,但是如果他要上很長時間,那一起住的其他人可能就要憋爆了。所以中斷遮蔽最開始不會被用於處理需要耗時很長的操作。但是大家想,這樣不是個解決辦法啊,我有時候確實要拉很長時間怎麼辦呢。
於是發明了頂半部TH和底半部BH,TH用於執行少量的關鍵性的動作,BH用於處理中斷中耗時的部分。
可以理解為,某人A特別特別想上廁所的時候,就進入TH(可以看作一個狀態),此時A去應個急,拉一點點,讓肚子不那麼疼,此時A是不可以被打斷的。如果沒人用廁所他就直接慢條斯理的開始拉了(BH)。但是他這個BH狀態是可以打斷的,如果此時來個人B 非常非常急,B進入TH說,我受不了啦要憋死啦,A就會暫停自己的狀態(保護現場)讓B進來拉一會(TH),等B拉了一點點,讓肚子不那麼疼了就出去。此時 A繼續(恢復現場)。等A 的BH部分完全結束後B再執行B的BH部分。
原子操作很好理解,就是大家每次上廁所都用時非常短,短到什麼程度呢,只要一條彙編指令的時間。當然拉的量也非常少(只改變一個整型或者是位)。所以就不存在搶廁所的問題了。
自旋鎖顧名思義,給這個廁所上把鎖,只有擁有這個鎖鑰匙的人A才能進廁所。進去後把鎖鎖上,外面的人B急得團團轉(自旋),出來後把鎖釋放,在門口等著的B拿了鑰匙趕緊開了鎖進去了。但是缺點就是,B在外面團團轉,沒有功夫去做別的事情,所以一旦 A 上廁所的時間很長,B就浪費了很長時間在自旋上。對系統的效能有所影響。
訊號量訊號量就是,我們的房子有 N 個廁所,N 不為 1, 且 N 為有限個,上廁所的人是有限的。即共享這一塊資源的程序是有限個數的。這時候我們就可以在廁所門口掛上 N 吧鑰匙,拿到鑰匙的就可以進去,鑰匙架空了,其他程序就只能在門口等待出來的人還鑰匙。
區別分析
程式碼實現
中斷遮蔽
local_irq_disable() /local_irq_save(flags);
// 訪問臨界區
local_irq_enable() /local_irq_restore(flags);
原子操作
位原子操作:set_bit/clear_bit/change_bit/test_bit
整型原子操作:atomic_set/atomic_read/atomic_add/atomic_sub/atomic_inc/atomic_dec/
步驟:
//1. 分配整形原子變數
atomic_t v = ATOMIC_INIT(1);
//2.操作原子變數
atomic_set/atomic_read/atomic_add/atomic_sub/atomic_inc加加/atomic_dec減減/...
例項:
static int open_cnt = 1;
open_cnt++; //不具備原子性
方法一,中斷遮蔽,不適用於多核
unsigned long flags;
local_irq_save(flags);
open_cnt++;
local_irq_restore(flags);
方法二,原子操作
static atomic_t open_cnt = ATOMIC_INIT(1);
atomic_inc(&open_cnt); //具有原子性
自旋鎖
//1.分配自旋鎖
spinlock_t lock;
//2.初始化自旋鎖
spin_lock_init(&lock);
//3.訪問臨界區之前獲取鎖:
spin_lock(&lock); //獲取自旋鎖,立即返回,如果沒有獲取鎖,將進行忙等待
或者
spin_trylock(&lock); //獲取鎖,返回true,否則返回false,所以這個函式一定要對返回值進行判斷!
//4 .訪問臨界區
//5.釋放自旋鎖
spin_unlock(&lock);
衍生自旋鎖
//1.分配自旋鎖
spinlock_t lock;
//2.初始化自旋鎖
spin_lock_init(&lock);
//3.訪問臨界區前獲取鎖:
unsigned long flags;
spin_lock_irq(&lock); // = spin_lock() + local_irq_disable()
或者
spin_lock_irqsave(&lock, flags); // = spin_lock() local_irq_save()
//4.訪問臨界區
//5.釋放自旋鎖
spin_unlock_irq(&lock); // = spin_unlock()+ local_irq_enable()
或者
spin_unlock_irqrestore(&lock, flags); // = spin_unlock() + local_irq_restore()
訊號量
//1.分配訊號量物件
struct semaphore sema;
//2.初始化為互斥訊號量
init_MUTEX(&sema);
或者:
DECLARE_MUTEX(sema);
//3.訪問臨界區之前獲取訊號量
down(&sema);
//如果獲取訊號量,立即返回
//如果訊號量不可用,程序將在此休眠,並且休眠的狀態是 [ 不可中斷的休眠狀態 TASK_UNINTERRUPTIBLE] !
或者
down_interruptible(&sema);
//如果訊號量不可用,程序將進入 [ 可中斷的休眠狀態 TASK_INTERRUPTIBLE ],如果返回0表示正常獲取訊號,如果返回非0,表示接受到了訊號
down_trylock();
//獲取訊號,如果訊號量不可用,返回非0,如果訊號量可用,返回0;不會引起休眠,可以在中斷上下文使用。返回值也要做判斷!
//4.訪問臨界區:臨界區可以休眠
//5.釋放訊號量
up(&sema);
//不僅僅釋放訊號量,然後喚醒休眠的程序,讓這個程序去獲取訊號量來訪問臨界區