Linux是一個多任務操作系統,肯定會存在多個任務共同操作同一段內存或者設備的情況,多個任務甚至中斷都能訪問的資源叫做共享資源。在驅動開發中要注意對共享資源的保護,也就是要處理對共享資源的并發訪問。
并發就是多個“用戶”同時訪問同一個共享資源,Linux 系統是個多任務操作系統,會存在多個任務同時訪問同一片內存區域,這些任務可能會相互覆蓋這段內存中的數據,造成內存數據混亂。針對這個問題必須要做處理,嚴重的話可能會導致系統崩潰。現在的 Linux 系統并發產生的原因很復雜,總結一下有下面幾個主要原因:
多線程并發訪問,Linux 是多任務(線程)的系統,所以多線程訪問是最基本的原因。
搶占式并發訪問,從 2.6 版本內核開始,Linux 內核支持搶占,也就是說調度程序可以在任意時刻搶占正在運行的線程,從而運行其他的線程。
中斷程序并發訪問,這個無需多說,學過 STM32 的同學應該知道,硬件中斷的權利可是很大的。
SMP(多核)核間并發訪問,現在 ARM 架構的多核 SOC 很常見,多核 CPU 存在核間并發訪問。
并發訪問帶來的問題就是競爭,在編寫驅動的時候要適當處理。并發和競爭往往不容易查找,導致驅動調試難度加大、費時費力。并發和競爭保護的不是代碼,而是數據!某個線程的局部變量不需要保護,我們要保護的是多個線程都會訪問的共享數據。 ? ?
解決并發和競爭有不同的處理方式,這里主要講:原子操作、自旋鎖、信號量、互斥體。
?原子操作
原子操作就是指不能再進一步分割的操作,一般原子操作用于變量或者位操作。簡單理解就是在匯編指令下是一條指令,這樣就可以避免在多線程的時候被干擾導致異常。
原子整形操作 API 函數 ? ?
Linux 內核定義了叫做 atomic_t 的結構體來完成整形數據的原子操作,在使用中用原子變量來代替整形變量,此結構體定義在 include/linux/types.h 文件中,定義如下:
typedef?struct?{ ????int?counter; }?atomic_t; //?簡單使用 atomic_t?v?=?ATOMIC_INIT(0);?/*?定義并初始化原子變零 v=0?*/ atomic_set(&v,?10);?/*?設置?v=10?*/ atomic_read(&v);?/*?讀取?v?的值,肯定是?10?*/ atomic_inc(&v);?/*?v?的值加?1,v=11?*/?
常用API:
?
? 位操作也是很常用的操作,Linux 內核也提供了一系列的原子位操作 API 函數,只不過原子位操作不像原子整形變量那樣有個 atomic_t 的數據結構,原子位操作是直接對內存進行操作:
? ?
?自旋鎖
原子操作只能對整形變量或者位進行保護,但是,在實際的使用環境中怎么可能只有整形變量或位這么簡單的臨界區。這就引出鎖機制,在 Linux內核中就是自旋鎖。當一個線程要訪問某個共享資源的時候首先要先獲取相應的鎖,鎖只能被一個線程持有,只要此線程不釋放持有的鎖,那么其他的線程就不能獲取此鎖。 ? ? ?
對于自旋鎖而言,如果自旋鎖正在被線程 A 持有,線程 B 想要獲取自旋鎖,那么線程 B 就會處于忙循環-旋轉-等待狀態,線程 B 不會進入休眠狀態或者說去做其他的處理,而是會一直傻傻的在那里“轉圈圈”的等待鎖可用。自旋鎖的“自旋”也就是“原地打轉”的意思,“原地打轉”的目的是為了等待自旋鎖可以用,可以訪問共享資源。 ?
自旋鎖的一個缺點:那就等待自旋鎖的線程會一直處于自旋狀態,這樣會浪費處理器時間,降低系統性能,所以自旋鎖的持有時間不能太長。所以自旋鎖適用于短時期的輕量級加鎖,如果遇到需要長時間持有鎖的場景那就需要換其他的方法了。
Linux 內核使用結構體 spinlock_t 表示自旋鎖,結構體定義如下所示: ?
typedef?struct?spinlock?{ ????union?{ ????????struct?raw_spinlock?rlock; #ifdef?CONFIG_DEBUG_LOCK_ALLOC #define?LOCK_PADSIZE?(offsetof(struct?raw_spinlock,?dep_map)) ???????struct?{ ????????????u8?__padding[LOCK_PADSIZE]; ????????????struct?lockdep_map?dep_map; ????????}; #endif ????}; }?spinlock_t;? 在使用自旋鎖之前,肯定要先定義一個自旋鎖變量,定義方法如下所示: ?
spinlock_t?lock;?//定義自旋鎖? 定義好自旋鎖變量以后就可以使用相應的 API 函數來操作自旋鎖。 ?
自旋鎖 API 函數??
自旋鎖會自動禁止搶占,也就說當線程 A得到鎖以后會暫時禁止內核搶占。如果線程 A 在持有鎖期間進入了休眠狀態,那么線程 A 會自動放棄 CPU 使用權。線程 B 開始運行,線程 B 也想要獲取鎖,但是此時鎖被 A 線程持有,而且內核搶占還被禁止了!線程 B 無法被調度出去,那么線程 A 就無法運行,鎖也就無法釋放,就容易發生死鎖!
最好的解決方法就是獲取鎖之前關閉本地中斷,Linux 內核提供了相應的 API 函數: ?
使用 spin_lock_irq/spin_unlock_irq 的時候需要用戶能夠確定加鎖之前的中斷狀態,但實際上內核很龐大,運行也是“千變萬化”,我們是很難確定某個時刻的中斷狀態,因此不推薦使用spin_lock_irq/spin_unlock_irq。建議使用 spin_lock_irqsave/spin_unlock_irqrestore,因為這一組函數會保存中斷狀態,在釋放鎖的時候會恢復中斷狀態。一般在線程中使用 spin_lock_irqsave/spin_unlock_irqrestore,在中斷中使用 spin_lock/spin_unlock:
下半部里面使用自旋鎖,可以使用的 API 函數:
? ?
?信號量
Linux 內核也提供了信號量機制,信號量常常用于控制對共享資源的訪問。相比于自旋鎖,信號量可以使線程進入休眠狀態,使用信號量會提高處理器的使用效率,但是信號量的開銷要比自旋鎖大,因為信號量使線程進入休眠狀態以后會切換線程,切換線程就會有開銷。 ? ?
信號量的特點: ?
①、因為信號量可以使等待資源線程進入休眠狀態,因此適用于那些占用資源比較久的場合。
②、因此信號量不能用于中斷中,因為信號量會引起休眠,中斷不能休眠。
③、如果共享資源的持有時間比較短,那就不適合使用信號量了,因為頻繁的休眠、切換線程引起的開銷要遠大于信號量帶來的那點優勢。
信號量 API 函數
? Linux 內核使用 semaphore 結構體表示信號量,結構體內容如下所示: ?
struct?semaphore?{ ????raw_spinlock_t?lock; ????unsigned?int?count; ????struct?list_head?wait_list; };要想使用信號量就得先定義,然后初始化信號量。有關信號量的 API 函數: ?
?
信號量的使用:
?
//?簡單使用 struct?semaphore?sem;?/*?定義信號量?*/ sema_init(&sem,?1);?/*?初始化信號量?*/ down(&sem);?/*?申請信號量?*/ /*?臨界區?*/ up(&sem);?/*?釋放信號量?*/?互斥體 ?
將信號量的值設置為 1 就可以使用信號量進行互斥訪問了,雖然可以通過信號量實現互斥,但是 Linux 提供了一個比信號量更專業的機制來進行互斥,它就是互斥體—mutex。互斥訪問表示一次只有一個線程可以訪問共享資源,不能遞歸申請互斥體。在我們編寫 Linux 驅動的時候遇到需要互斥訪問的地方建議使用 mutex。Linux 內核使用 mutex 結構體表示互斥體: ?
struct?mutex?{ ? /*?1:?unlocked,?0:?locked,?negative:?locked,?possible?waiters?*/ ? atomic_t?count; ? spinlock_t?wait_lock; };? ?
在使用 mutex 之前要先定義一個 mutex 變量。在使用 mutex 的時候要注意如下幾點:
①、mutex 可以導致休眠,因此不能在中斷中使用 mutex,中斷中只能使用自旋鎖。
②、和信號量一樣,mutex 保護的臨界區可以調用引起阻塞的 API 函數。
③、因為一次只有一個線程可以持有 mutex,因此,必須由 mutex 的持有者釋放 mutex。并且 mutex 不能遞歸上鎖和解鎖。
互斥體 API 函數 ?
?
互斥體的使用如下:
struct?mutex?lock;?/*?定義一個互斥體?*/ mutex_init(&lock);?/*?初始化互斥體?*/ mutex_lock(&lock);?/*?上鎖?*/ /*?臨界區?*/ mutex_unlock(&lock);?/*?解鎖?*/? Linux 內核還有很多其他的處理并發和競爭的機制,常用的方法有原子操作、自旋鎖、信號量和互斥體。
?
審核編輯:湯梓紅
?
評論
查看更多