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