【Linux开发】linux设备驱动归纳总结(四):5.多处理器下的竞态和并发
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
這節(jié)將在上一節(jié)的基礎(chǔ)上介紹支持多處理器和內(nèi)核搶占的內(nèi)核如何避免并發(fā)。除了內(nèi)核搶占和中斷外,由于多處理起的緣故,它可以做到多個程序同時執(zhí)行。所以,進(jìn)程除了要防自己的處理器外,還要防別的處理器,這個就是這節(jié)要介紹的內(nèi)容。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
一、多處理器搶占式內(nèi)核的內(nèi)核同步需要防什么
1)防內(nèi)核搶占。
2)防中斷打斷。
3)防其他處理器也來插一腳。
所以,在上一節(jié)講的防搶占和防中斷,接下來的內(nèi)容實在這兩個的基礎(chǔ)上說一下如何防其他處理器。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
二、自旋鎖
內(nèi)核中是有很多的鎖,自旋鎖是其中的一種。它的作用在于,只要代碼在進(jìn)入臨界區(qū)前加上鎖,在進(jìn)程還沒出臨界區(qū)之前,別的進(jìn)程(包括自身處理器和別的處理器上的進(jìn)程)都不能進(jìn)入臨界區(qū)。
自旋鎖的可以這樣理解,每個進(jìn)程進(jìn)入上鎖的臨界區(qū)前,必須先獲得鎖,否則在獲得鎖這條代碼上查詢(注意,不是休眠,是忙等待,循環(huán)執(zhí)行指令),知道臨界區(qū)里面的進(jìn)程走出臨界區(qū),別的進(jìn)程獲得鎖后進(jìn)入臨界區(qū)。有且只有一個獲得鎖的進(jìn)程進(jìn)入臨界區(qū)。
也來個生活上的例子,公司有一個上鎖的廁所,A在上廁所時,拿到鑰匙,把門鎖上后歡快地上廁所。這時B也想上廁所,但他看到門鎖上了,沒辦法,只好在門口等待,直到A開門出來,把鑰匙交給B,B才能去上廁所。
接下來說一下如何讓使用,需要包含頭文件
1)使用自旋鎖需要先定義并初始化自旋鎖:
同樣的,你可以使用靜態(tài)定義并初始化:
spinlock_t lock = SPIN_LOCK_UNLOCKED;
也可以使用動態(tài)定義并初始化:
spinlock_t lock;
spin_lock_init(&lock);
2)在進(jìn)入臨界區(qū)前,必須先獲得鎖,使用函數(shù):
spin_lock(&lock);
3)在退出臨界區(qū)后,需要釋放鎖,使用函數(shù):
spin_unlock(&lock);
所以,一個完整的上鎖代碼應(yīng)該這樣使用:
#include
spinlock_t lock; //1.定義一個自旋鎖
spin_lock_init(&my_dev.lock); //2.初始化鎖
spin_lock(&lock); //3.獲得鎖
臨界區(qū)。。。。。
spin_unlock(&lock); //4.釋放鎖
我將這段代碼加上了驅(qū)動程序4th_mutex_5/1st/test.c,注意,這段函數(shù)并不是很規(guī)范,我只是想舉例示范一下這幾個函數(shù)應(yīng)該加在代碼中的什么位置。其中,代碼中的臨界區(qū)我只是打印了一句話,并不是什么共享數(shù)據(jù)。
驗證一下效果:
[root: 1st]# insmod test.ko
alloc major[253], minor[0]
hello kernel
[root: 1st]# mknod /dev/test c 253 0
[root: 1st]# insmod irq/irq.ko
hello irq
[root: 1st]# cd app/
[root: app]# ./app&
[root: app]#?runing
runing
[root: app]# ./app_read
[test_open]
pid[400]
[test_read]task pid[400], context [app_read]
[test_read]task pid[400], context [app_read]
[test_read]task pid[400], context [app_read]
key down
key down
key down
key down
key down
[test_read]task pid[400], context [app_read]
會發(fā)現(xiàn),因為我在一個死循環(huán)上了自旋鎖(當(dāng)然這種做法是不恰當(dāng)?shù)?#xff09;,程序運(yùn)行起來就和關(guān)了搶占效果一樣!內(nèi)核線程陷入循環(huán),只有中斷能夠打斷。
接著說函數(shù)spin_lock()實現(xiàn)了什么操作:
第一步:關(guān)搶占。
第二步:獲得鎖,防止別的處理器訪問。
相對的,spin_unlock()實現(xiàn)了相反的操作:
第一步:開搶占。
第二步:釋放鎖。
所以,如果在單處理器支持內(nèi)核搶占的內(nèi)核下,spin_lock()函數(shù)會退化成關(guān)搶占。在單處理器不支持內(nèi)核搶占的內(nèi)核下,這將會是一條空語句。
上面的代碼防了兩種情況,但還沒防中斷,防中斷有兩種方法:
方法一:在需要訪問臨界區(qū)的中斷代碼也加鎖:
do_irq() //中斷處理函數(shù)
{
spin_lock();
/*臨界區(qū)。。*/
spin_unlock();
}
方法二:直接在加鎖的同時把中斷也禁掉:
#include
spinlock_t lock;
spin_lock_init(&my_dev.lock);
unsigned long flag = 0;
loacl_irq_save(flag);
spin_lock(&lock);
臨界區(qū)。。。。。
local_irq_restroe(flag);
spin_unlock(&lock);
當(dāng)然,貼心的內(nèi)核工作者將兩個函數(shù)合成一個函數(shù),只用調(diào)用一個函數(shù)就能既上鎖有關(guān)中斷了:
spin_lock_irq(spinlock_t *lock) = spin_lock(spinlock_t *lock) + local_irq_disable()
spin_unlock_irq(spinlock_t *lock) = spin_unlock(spinlock_t *lock) + local_irq_enable()
spin_lock_irqsave(spinlick_t *lock, unsigned long falg) = spin_lock(spinlock_t *lock) + local_irq_save(unsigned long flag)
spin_unlock_irqrestore(spinlick_t *lock, unsigned long falg) = spin_unlock(spinlock_t *lock) + local_irq_restorr(unsigned long flag)
自旋鎖的一個重要特征是,只要沒獲得鎖,進(jìn)程會占用CPU查詢,直到獲得鎖,有些人不想查詢,可以使用以下函數(shù):
int spin_try_trylock(spinlock_t *lock);
一看函數(shù)名字就知道,他是嘗試獲得鎖,成功返回非零,失敗返回零。
這個強(qiáng)大的功能必定有他的弊端:
弊端一:持有鎖的時間必須盡量的短:
進(jìn)程在沒獲得鎖前不進(jìn)入睡眠,而是會占用CPU查詢,這樣的做法是為了節(jié)省進(jìn)程從TASK_RUNNING切換至TASK_INTERRUPTIBLE后又切換回來消耗的時間。同時也是出于這樣的原因,被上鎖的臨界區(qū)代碼必須盡量的短。
弊端二:持有鎖的期間不能睡眠:
也就是說,在臨界區(qū)的代碼里不能有引起睡眠的操作。譬如,一個進(jìn)程上鎖后睡眠,此時切換執(zhí)行中斷處理函數(shù),可中斷處理函數(shù)也要獲得鎖,這樣就會使中斷自旋,并且沒人能打斷。
最簡單的生活例子,上廁所的時候你鎖上門睡覺了,還讓別人在門口瞎等!這種事情多不合理!
弊端三:要注意上鎖的順序:
如果進(jìn)程進(jìn)入臨界區(qū)前需要那A、B兩把鎖,一個進(jìn)程拿了A,另一個進(jìn)程拿了B,它們死活也不讓步,都不能獲得另外一把鎖,那只好在臨界區(qū)代碼前死等了。
弊端四:不能嵌套上鎖:
簡單的說,就是獲得鎖后后的進(jìn)程不能再上一次同樣的鎖。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
三、信號量
上面說了自旋鎖的缺點(diǎn),如不能睡眠,要求臨界區(qū)執(zhí)行時間盡可能的短。出于這樣的情況,就有了另一種內(nèi)核同步的機(jī)制——信號量。
信號量是一種睡眠鎖,當(dāng)進(jìn)程試圖獲取已經(jīng)被占同的信號量,他就會被放到等待隊列中,直到信后信號里釋放后被喚醒。
繼續(xù)剛才上鎖的廁所,話說A把門鎖上后上廁所,B要來上廁所是看到廁所被占用了,于是,他在門口上貼張紙條“我是B,你出來后叫我上廁所”,然后就離開了。A出來后,看到門口有紙條,就按照紙條所說的去通知B。
所以,信號量就是允許長時間上鎖的睡眠鎖。
接下來看一下怎么使用信號量,信號量有兩種:互斥信號量和計數(shù)信號量。互斥信號量,就是說同一時間只能有一個進(jìn)程獲得鎖并進(jìn)入臨界區(qū)。而計數(shù)信號量,那就是鎖的數(shù)量可以多于一個,允許多個獲得鎖的進(jìn)程進(jìn)入臨界區(qū),同時這也是和自旋鎖不同的地方。
以下的函數(shù)需要包含頭文件,信號量使用數(shù)據(jù)類型struct semaphore表示。
一、創(chuàng)建和初始化信號量:
同樣有兩種方法。
第一種是靜態(tài)定義并初始化
static DECLARE_SEMAPHORE_GENERIC(name, count)
定義并初始化一個叫name的計數(shù)信號量,允許conut個進(jìn)程同時持有鎖。
static DECLARE_MUTEX(name)\
定義并初始化一個叫name的互斥信號量。
第二種是動態(tài)定義并初始化
首先你要定義一個信號量結(jié)構(gòu)體:
struct semaphore sem;
然后初始化:初始化是指定信號量的個數(shù)
sema_init(&sem, count);
當(dāng)然也有一些方便定義互斥信號量的函數(shù):
/*初始化一個互斥信號量*/
#define init_MUTEX(sem) sema_init(sem, 1)
/*初始化一個互斥信號量并加鎖*/
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0)
二、使用信號量:
一般的獲得信號量有三個函數(shù):
1/*獲取信號量sem,如果不能獲取,切換狀態(tài)至TASK_UNINTERRUPTIBLE*/
voud down(struct semaphore *sem)
上面的函數(shù)不太常用,因為它的睡眠不能被中斷打斷,一般使用下面的函數(shù)
2/*獲取信號量sem,如果不能獲取,切換狀態(tài)至TASK_INTERRUPTIBLE,如果睡眠期間被中斷打斷,函數(shù)返回非0值*/
int down_interruputible(struct semaphore *sem)
3/*嘗試獲得信號量,如果獲得信號量就返回零,不能獲得也不睡眠,返回非零值*/
int down_try_lock(struct semaphore *sem)
因為上面的函數(shù)在睡眠時會被中斷打斷,一般會如下使用:
if (down_interruptible(&sem)){
return – ERESTARTSYS;
}
即如果在睡眠期間被中斷打斷,返回-ERESTARTSYS給用戶,告知用戶重新執(zhí)行。如果是被喚醒,則會往下執(zhí)行。
釋放信號量函數(shù):
void up(struct semaphore *sem);
所以,信號量一般這樣使用:
#include
struct semaphore sem;
sema_init(&sem, 1);
if (down_interruptible(&sem)){
return – ERESTARTSYS;
}
臨界區(qū)代碼。。。。。
up(&sem);
這4th_mutex_5/2nd/test.c我寫了加上信號量的代碼,還是那一句,代碼不規(guī)范(在死循環(huán)加信號量無疑是自殺),我只是想告訴大家這幾條函數(shù)一般使用在什么地方。在搶占式內(nèi)核的情況下,使用信號量和使用自旋鎖保護(hù)代碼會不一樣。
[root: /]# cd review_driver/4th_mutex/4th_mutex_5/2nd/
[root: 2nd]# insmod test.ko //加載模塊
alloc major[253], minor[0]
hello kernel
[root: 2nd]# mknod /dev/test c 253 0
[root: 2nd]# insmod irq/irq.ko //加載中斷
hello irq
[root: 2nd]# cd app/
[root: app]# ./app_read& //先后臺運(yùn)行app_read
[test_open]
pid[400] //注意進(jìn)程號400
[test_read]task pid[400], context [app_read]
[root: app]#?[test_read]task pid[400], context [app_read]
[test_read]task pid[400], context [app_read]
[test_read]task pid[400], context [app_read]
[root: app]# ./app_read& //再后臺運(yùn)行一個app_read
[test_open]
pid[401] //注意進(jìn)程號401,后面的打印沒有一個是401!!!
[root: app]#?[test_read]task pid[400], context [app_read]
[test_read]task pid[400], context [app_read]
[test_read]task pid[400], context [app_read]
[test_read]task pid[400], context [app_read]
[test_read]task pid[400], context [app_read]
[test_read]task pid[400], context [app_read]
[test_read]task pid[400], context [app_read]
[root: app]# ./app //后臺運(yùn)行app
runing
[test_read
key down
key down
key down
[test_read]task pid[400], context [app_read]
runing
[test_read]task pid[400], context [app_read]
runing
]task pid[400], context [app_read]
runing //app能打印!!!!!
[test_read]task pid[400], context [app_read]
[test_read]task pid[400], context [app_read]
runing
key down //中斷也能執(zhí)行!!!
key down
key down
key down
key down
[test_read]task pid[400], context [app_read]
runing
[test_read]task pid[400], context [app_read]
runing
不知道各位注意到上面的現(xiàn)象與自旋鎖的有什么區(qū)別。
第一:信號量沒有關(guān)搶占,如果別的進(jìn)程沒有訪問上鎖的臨界區(qū)(如app),這個進(jìn)程照樣可以運(yùn)行。
第二:訪問了上鎖臨界區(qū)的進(jìn)程,就不能執(zhí)行了(如第二次運(yùn)行的app_read)。
第三:臨界區(qū)還是可以被中斷打斷的,因為信號量根本沒關(guān)中斷,如果臨界區(qū)的資源不能被中斷訪問,那就像之前說的處理,要不在中斷處理函數(shù)在進(jìn)入臨界區(qū)前獲得鎖,要不就把中斷也關(guān)了。
所以,簡單的說,信號量就是一個數(shù),你獲得這個數(shù)了,你就可以進(jìn)去臨界區(qū)。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
四、信號量與自旋鎖的區(qū)別:
既然上面介紹了兩種鎖的機(jī)制和使用的方法,接下來就到對比一下兩種鎖的區(qū)別,應(yīng)該在哪里使用。
區(qū)別一:實現(xiàn)方式
自旋鎖是自旋等待,進(jìn)程狀態(tài)始終處于TASK_RUNNING。
信號量是睡眠等待,進(jìn)程在等待是處于TASK_INTERRUPTIBLE。
區(qū)別二:睡眠死鎖陷阱:
在自旋鎖的臨界區(qū)中,進(jìn)程是不能陷入睡眠的。
而信號量可以睡眠。
同時,基于上面的原因,中斷上下文中只能使用自旋鎖(中斷里不能休眠),在有睡眠代碼的臨界區(qū)只能使用信號量
區(qū)別三:CPU的使用情況:
明顯的,信號量對系統(tǒng)的負(fù)載小,因為它睡眠了。
區(qū)別四:執(zhí)行的效率方面:
自旋鎖的效率比較高,因為它少了進(jìn)程狀態(tài)切換的消耗。
相對的信號量的效率比較低,因為進(jìn)程的等待需要切換進(jìn)程狀態(tài)。
區(qū)別五:上鎖的時間長短:
因為自旋鎖是忙等待,所以臨界區(qū)的代碼不能太長。
而信號量可以使用在運(yùn)行時間較長的臨界區(qū)代碼。
區(qū)別六:是否關(guān)搶占:
自旋鎖是關(guān)搶占的,所以在單處理器非搶占的內(nèi)核下,自旋鎖是沒用的。是空操作。
信號量并沒有關(guān)搶占,所以,只有需要獲得鎖的進(jìn)程才會睡眠,其他進(jìn)程還可以繼續(xù)運(yùn)行,如上面的例子。
居于上面的區(qū)別,有這樣的一個表:
| 需求 | 建議的加鎖方法 |
| 低開銷的加鎖 | 優(yōu)先考慮自旋鎖 |
| 短時間的加鎖 | 優(yōu)先考慮自旋鎖 |
| 長時間的加鎖 | 優(yōu)先是使用信號量 |
| 中斷上下文中加鎖 | 必須使用自旋鎖 |
| 上鎖后會有睡眠 | 必須使用信號量 |
還是那一句,個人喜好與需求,像我這種小白一般是不需要用到內(nèi)核同步的機(jī)制的,因為我的開發(fā)板是單處理器非搶占內(nèi)核。
接下來介紹一下其他的內(nèi)核同步方法,但是我全都沒用過。。。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
五、互斥量
這是2.6內(nèi)核新加的,是互斥信號量的升級版。
其實上面介紹了兩種鎖使用的情況,其實,可以睡眠的臨界區(qū),都可以使用信號量,這就是信號量強(qiáng)大的地方。然而,越強(qiáng)大的功能,內(nèi)核實現(xiàn)起來就越是困難。所以。內(nèi)核開發(fā)者實現(xiàn)了輕量級的睡眠鎖——互斥量。
使用互斥量使用結(jié)構(gòu)體struct mutex來表示:
一、定義并初始化,兩種方法:
靜態(tài)定義:
DEFINE_MUTEX(name)
動態(tài)定義并初始化:
struct mutex mutex;
mutex_init(&mutex);
二、互斥量的操作:
獲得互斥里
void inline __sched mutex_lock(struct mutex *lock) //不能獲得鎖是進(jìn)入不可中斷睡眠
int __sched mutex_lock_interruptible(struct mutex *lock) //進(jìn)入可中斷睡眠
int __sched mutex_trylock(struct mutex *lock) //嘗試獲得鎖
這三個函數(shù)的用法的信號量的三個完全一樣,返回值也是,所以我就不細(xì)講了。
釋放信號量:
void __sched mutex_unlock(struct mutex *lock)
當(dāng)然,互斥量是升級版的輕量級信號量,它必然會有限制:
1)同一時間只能有一個進(jìn)程獲得鎖,這是互斥的概念。
2)只能在同一進(jìn)程上鎖和解鎖,而信號量不一樣,可以在這個進(jìn)程上鎖,另外的進(jìn)程解鎖。
3)同一個進(jìn)程獲得鎖后這段期間在獲得這個鎖,也就是說不能遞歸使用,原因很簡單,因為是互斥,上鎖的只有一次,只能解鎖有在上鎖。
4)進(jìn)程持有鎖是不能退出。
5)中斷上下文不能使用鎖,即使是mutex_trylock()。
6)互斥鎖只能通過內(nèi)核提供的API接口來操作。
內(nèi)核推薦,在能使用互斥鎖的情況下優(yōu)先考慮,而不是使用信號量。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
六、原子操作(atomic_t)
所謂的原子操作,就是這段代碼不會被其它進(jìn)程打斷,所以,加上自旋鎖等鎖之后的操作也算是原子操作。
而這里要介紹的原子操作和上面的不一樣。考慮一下,如果你加鎖只是為了保護(hù)一個整數(shù),你有必要大費(fèi)周章的使用自旋鎖了,只要你把操作這個數(shù)的代碼濃縮成一條指令,不就可以了嗎?
內(nèi)核提供了兩種的原子操作:原子整數(shù)操作和原子位操作。顧名思義,就是在操作這個整數(shù)或者設(shè)置一個數(shù)的位數(shù)時,是不會被打斷的。
具體的函數(shù)操作我就不講了,我也沒用過,書上也講得很詳細(xì):《linux內(nèi)核設(shè)計與實現(xiàn)(第三版)》P175頁。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
七、總結(jié):
這節(jié)講了在多處理器的情況下如何實現(xiàn)內(nèi)核同步,避免臨界區(qū)并發(fā)訪問,當(dāng)然,上面介紹的方法需要用在真正需要的地方,因為我使用的是單處理器非搶占式內(nèi)核,所以也沒有太多的例子和代碼,只能粗略的描述各種鎖機(jī)制的優(yōu)缺點(diǎn)和實現(xiàn)的機(jī)制。可能講得不好,如果有疑問可以提出,我盡量改善。
同時,內(nèi)核同步的機(jī)制還有很多,譬如讀寫鎖等,都在書上有詳細(xì)的描述。
當(dāng)我還是個小小白的時候,我一直在納悶自旋鎖信號量究竟使用在什么地方,現(xiàn)在才發(fā)現(xiàn),在我開發(fā)板如此低級的內(nèi)核上,只要防中斷就可以了。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
源代碼:?4th_mutex_5.rar??
轉(zhuǎn)載于:https://www.cnblogs.com/huty/p/8518585.html
總結(jié)
以上是生活随笔為你收集整理的【Linux开发】linux设备驱动归纳总结(四):5.多处理器下的竞态和并发的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 分别使用委托、接口、匿名方法、泛型委托实
- 下一篇: 关于this的指向问题