文章目錄
- 👨?💻Linux驅動開發入門 | 并發與互斥機制詳解
- 📌為什么驅動中需要并發和互斥控制?
- 💡常見的并發控制機制
- 🔐自旋鎖和信號量通俗理解
- 🌀自旋鎖(Spinlock)——“廁所排隊鎖”
- 🚦信號量(Semaphore)——“停車場智能顯示器”
- 🆚 自旋鎖 vs 信號量
- 讀寫鎖是什么?
- 🚨死鎖問題和解決策略
- 一、預防死鎖
- 1. 資源一次性分配(破壞請求與保持條件)
- 2. 可剝奪資源(破壞不可剝奪條件)
- 3. 資源有序分配法(破壞循環等待條件)
- 二、避免死鎖
- 銀行家算法
- 三、死鎖檢測
- 步驟如下:
- 四、解除死鎖
- 1. 剝奪資源
- 2. 撤消進程
- 五、避免死鎖的編程實踐
- 1. 加鎖順序(Lock Ordering)
- 2. 加鎖時限(Try Lock with Timeout)
- 3. 死鎖檢測機制
- 總結
- 🧪真實驅動例子:互斥訪問設備寄存器
- 臨界區:
- 🧠Q&A 常見問題
- ?總結
👨?💻Linux驅動開發入門 | 并發與互斥機制詳解
📌為什么驅動中需要并發和互斥控制?
我們知道,在多線程或多任務并行執行的操作系統中,比如Linux內核,多個執行單元(線程或中斷)可能同時訪問共享資源(如全局變量、設備寄存器、緩沖區等),這就帶來了“競態條件(Race Condition)”的風險。
舉個簡單的例子:
假如兩個線程A和B幾乎在同一時間讀取同一個計數變量x的值為10,然后各自+1并寫回。你期望x變為12,但結果可能還是11。
這類問題就需要“互斥”機制來保護——確保同一時刻只能有一個執行單元訪問共享資源,其訪問的代碼區域稱為臨界區(Critical Section)。
💡常見的并發控制機制
Linux驅動開發中,常見的互斥控制方式有以下幾種:
互斥機制 | 特點 | 場景 |
---|---|---|
中斷屏蔽 | 禁止中斷上下文干擾 | 適用于簡單、快速完成的臨界區 |
原子操作 | 使用CPU原子指令保證變量操作完整 | 操作變量極少時 |
自旋鎖(spinlock) | 自旋等待,適合短時間鎖定 | 中斷/進程上下文 |
信號量(semaphore) | 可睡眠等待,適合長時間持鎖 | 進程上下文,驅動任務中常用 |
互斥鎖(mutex) | 是信號量的簡化版本 | 一般用于用戶態/驅動模塊 |
🔐自旋鎖和信號量通俗理解
🌀自旋鎖(Spinlock)——“廁所排隊鎖”
把共享資源想象成一個單人廁所。
- 線程A進入廁所,并鎖門(獲取鎖);
- 線程B也想用廁所,只能在門口一直轉圈圈(不停檢查鎖狀態);
- A出來后釋放鎖,B才能進去。
自旋鎖適合鎖定時間非常短的臨界區,因為等待期間線程一直占用CPU,不睡覺!
? 優點:
- 實時性好(適合中斷上下文)
- 實現簡單
? 缺點:
- CPU占用率高,鎖持有久了會浪費資源
- 不可在臨界區使用可能睡眠的代碼!
🚦信號量(Semaphore)——“停車場智能顯示器”
假設一個停車場有100個車位,信號量就相當于入口處的電子屏:
- 顯示“當前車位:20”,車還能進;
- 顯示“滿”,車就得等;
- 有車離開,車位更新,通知其他等車入場。
信號量適合臨界區操作時間較長、可能會阻塞的場景。
? 優點:
- 可睡眠等待,不占CPU
- 適合處理資源池問題,如連接池、緩存池等
? 缺點:
- 實時性差,不可用于中斷處理
- 實現復雜,需考慮死鎖
- 鎖被短時間持有時,使用信號量就不太適宜了,因為睡眠引起的耗時可能比鎖被占用的全部時間還要長。
🆚 自旋鎖 vs 信號量
對比項 | 自旋鎖 | 信號量 |
---|---|---|
是否睡眠 | ? 不可睡眠 | ? 可睡眠等待 |
適用上下文 | 中斷上下文 | 進程上下文 |
臨界區時長 | 極短 | 可長 |
是否允許搶占 | ? 不允許(禁搶) | ? 允許搶占 |
用于中斷中 | ? 可以 | ? 禁止 |
是否可重入 | ? 否 | ? 是(看實現) |
在你占用信號量的同時不能占用自旋鎖,因為在你等待信號量時可能會睡眠,而在持有自旋鎖時是不允許睡眠的。
讀寫鎖是什么?
當臨界區的一個文件可以被同時讀取,但是并不能被同時讀和寫。如果一個線程在讀,另一個線程在寫,那么很可能會讀取到錯誤的不完整的數據。讀寫自旋鎖是可以允許對臨界區的共享資源進行并發讀操作的。但是并不允許多個線程并發讀寫操作
🚨死鎖問題和解決策略
在操作系統或并發編程中,**死鎖(Deadlock)**是一個經典問題。本文將帶你由淺入深地了解死鎖的處理方式,主要包括四種:預防死鎖、避免死鎖、檢測死鎖以及解除死鎖。
一、預防死鎖
死鎖產生的四個必要條件是:互斥、不可剝奪、請求與保持、循環等待。
為了預防死鎖,我們可以通過破壞其中一個或多個條件來避免死鎖的發生。
1. 資源一次性分配(破壞請求與保持條件)
當一個進程申請資源時,必須一次性申請它執行所需的所有資源。如果一次申請不到,就什么也不分配,避免持有部分資源再申請其他資源。
2. 可剝奪資源(破壞不可剝奪條件)
允許系統在資源不足時,強行從某些進程中回收已分配的資源,重新分配給其他更需要的進程。
3. 資源有序分配法(破壞循環等待條件)
為所有資源編號,進程必須按編號遞增的順序申請資源。釋放時則按編號遞減順序釋放。這樣可以避免資源請求形成閉環。
二、避免死鎖
相比預防死鎖,避免死鎖不要求完全避免死鎖條件的成立,而是在每次資源分配時判斷是否安全。
銀行家算法
預防死鎖的幾種策略,會嚴重地損害系統性能。因此在避免死鎖時,要施加較弱的限制,從而獲得較滿意的系統性能。由于在避免死鎖的策略中,允許進程動態地申請資源。因而,系統在進行資源分配之前預先計算資源分配的安全性。
這是最經典的死鎖避免算法。
- 系統在每次資源分配前,模擬本次資源分配是否會導致系統進入不安全狀態。
- 如果安全,則分配資源;否則讓進程等待。
三、死鎖檢測
死鎖檢測是允許死鎖發生,但系統會定期檢查是否有死鎖存在,一旦檢測到就進行處理。
步驟如下:
- 系統記錄所有進程與資源的指定一個唯一的號碼,構建資源分配圖或等待圖。
- 檢查是否存在環路(循環等待)結構。
- 若有環路,即可判定發生了死鎖。
四、解除死鎖
當檢測到死鎖后,需要采取措施解除死鎖狀態。常見方法如下:
1. 剝奪資源
從非死鎖進程中剝奪資源分配給死鎖進程,讓后者能繼續運行,釋放資源。
2. 撤消進程
- 終止死鎖進程或一些代價較小的進程,釋放資源。
- 代價可以依據優先級、運行時間、完成率、業務重要性來評估。
五、避免死鎖的編程實踐
在多線程編程中(如Java、C++),我們還可以通過一些實際的編程技巧避免死鎖:
1. 加鎖順序(Lock Ordering)
確保所有線程在獲取多個鎖時,始終按照固定順序獲取。例如:線程要獲取鎖A和鎖B,必須先獲取編號小的鎖A,再獲取鎖B。
// Thread 1:
synchronized(lockA) {synchronized(lockB) {// do something}
}
// Thread 2: 也必須先獲取lockA,再獲取lockB
按照順序加鎖是一種有效的死鎖預防機制。但是,這種方式需要你事先知道所有可能會用到的鎖,并對這些鎖做適當的排序),但總有些時候是無法預知的。
2. 加鎖時限(Try Lock with Timeout)
設置鎖獲取的超時時間,如果無法在一定時間內獲取到鎖,就放棄。
if(lock.tryLock(500, TimeUnit.MILLISECONDS)) {try {// do something} finally {lock.unlock();}
} else {// 獲取鎖失敗,執行其他邏輯或重試
}
這種方式可以有效避免長時間等待。
3. 死鎖檢測機制
針對上面兩種不適用的場景。那些不可能實現按序加鎖并且鎖超時也不可行的場景
使用數據結構記錄線程和資源的持有與請求狀態,在失敗時主動檢查是否形成了等待環。
當檢測到環路時:
- 某些線程主動釋放鎖雖然有回退和等待,但是如果有大量的線程競爭同一批鎖,它們還是會重復地死鎖,原因同超時類似,不能從根本上減輕競爭
- 或者優先級較低的線程撤退一段時間再重試
這種方式適合無法提前安排加鎖順序的復雜應用場景。
總結
方法 | 是否允許死鎖發生 | 是否易于實現 | 是否影響性能 |
---|---|---|---|
預防死鎖 | 否 | 較簡單 | 高 |
避免死鎖 | 否 | 中 | 中 |
死鎖檢測 | 是 | 中 | 中 |
解除死鎖 | 是 | 復雜 | 低(只在死鎖發生時影響) |
🧪真實驅動例子:互斥訪問設備寄存器
假設我們要編寫一個字符設備驅動,多個進程可能并發調用 read()
操作,訪問同一片寄存器區域。
臨界區:
static DEFINE_SPINLOCK(my_lock);ssize_t my_read(struct file *file, char __user *buf, size_t len, loff_t *off) {unsigned long flags;spin_lock_irqsave(&my_lock, flags);// 臨界區:訪問共享寄存器data = ioread32(dev->reg_base);spin_unlock_irqrestore(&my_lock, flags);return 0;
}
注意:用
spin_lock_irqsave
是因為中斷中也可能調用,必須禁止中斷防止死鎖!
🧠Q&A 常見問題
Q:單核CPU還需要加鎖嗎?
A:需要!因為即使單核,操作系統依然可以通過搶占調度讓線程切換,導致共享變量被多個線程交叉訪問。
Q:信號量可以用在中斷中嗎?
A:不能!因為信號量可能會休眠,而中斷處理函數不能休眠,否則整個中斷系統會掛死。
Q:spin_lock能不能睡眠?
A:不能!因為它禁止搶占,如果睡眠,系統可能無法調度其他任務,導致死鎖。
?總結
- 多線程 + 共享資源 = 必須互斥
- 自旋鎖適合臨界區非常短的場景;信號量適合長時間、可睡眠的場景
- 死鎖問題復雜,要盡量規避:統一加鎖順序、設置超時、圖算法檢測
- 在驅動中使用鎖時要特別考慮上下文(中斷/進程)和是否可休眠