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