linux設備驅動歸納總結(四):5.多處理器下的競態和併發
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
這節將在上一節的基礎上介紹支持多處理器和內核搶佔的內核如何避免併發。除了內核搶佔和中斷外,由於多處理起的緣故,它可以做到多個程序同時執行。所以,進程除了要防自己的處理器外,還要防別的處理器,這個就是這節要介紹的內容。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
一、多處理器搶佔式內核的內核同步需要防什麼
1)防內核搶佔。
2)防中斷打斷。
3)防其他處理器也來插一腳。
所以,在上一節講的防搶佔和防中斷,接下來的內容實在這兩個的基礎上說一下如何防其他處理器。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
二、自旋鎖
內核中是有很多的鎖,自旋鎖是其中的一種。它的作用在於,只要代碼在進入臨界區前加上鎖,在進程還沒出臨界區之前,別的進程(包括自身處理器和別的處理器上的進程)都不能進入臨界區。
自旋鎖的可以這樣理解,每個進程進入上鎖的臨界區前,必須先獲得鎖,否則在獲得鎖這條代碼上查詢(注意,不是休眠,是忙等待,循環執行指令),知道臨界區裏面的進程走出臨界區,別的進程獲得鎖後進入臨界區。有且只有一個獲得鎖的進程進入臨界區。
也來個生活上的例子,公司有一個上鎖的廁所,A在上廁所時,拿到鑰匙,把門鎖上後歡快地上廁所。這時B也想上廁所,但他看到門鎖上了,沒辦法,只好在門口等待,直到A開門出來,把鑰匙交給B,B才能去上廁所。
接下來說一下如何讓使用,需要包含頭文件<linux/spinlock.h>
1)使用自旋鎖需要先定義並初始化自旋鎖:
同樣的,你可以使用靜態定義並初始化:
spinlock_t lock = SPIN_LOCK_UNLOCKED;
也可以使用動態定義並初始化:
spinlock_t lock;
spin_lock_init(&lock);
2)在進入臨界區前,必須先獲得鎖,使用函數:
spin_lock(&lock);
3)在退出臨界區後,需要釋放鎖,使用函數:
spin_unlock(&lock);
所以,一個完整的上鎖代碼應該這樣使用:
#include <linux/spinlock.h>
spinlock_t lock; //1.定義一個自旋鎖
spin_lock_init(&my_dev.lock); //2.初始化鎖
spin_lock(&lock); //3.獲得鎖
臨界區。。。。。
spin_unlock(&lock); //4.釋放鎖
我將這段代碼加上了驅動程序4th_mutex_5/1st/test.c,注意,這段函數並不是很規範,我只是想舉例示範一下這幾個函數應該加在代碼中的什麼位置。其中,代碼中的臨界區我只是打印了一句話,並不是什麼共享數據。
驗證一下效果:
[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]# <app> runing
<app> runing
[root: app]# ./app_read
<kernel>[test_open]
<app_read> pid[400]
<kernel>[test_read]task pid[400], context [app_read]
<kernel>[test_read]task pid[400], context [app_read]
<kernel>[test_read]task pid[400], context [app_read]
key down
key down
key down
key down
key down
<kernel>[test_read]task pid[400], context [app_read]
會發現,因爲我在一個死循環上了自旋鎖(當然這種做法是不恰當的),程序運行起來就和關了搶佔效果一樣!內核線程陷入循環,只有中斷能夠打斷。
接着說函數spin_lock()實現了什麼操作:
第一步:關搶佔。
第二步:獲得鎖,防止別的處理器訪問。
相對的,spin_unlock()實現了相反的操作:
第一步:開搶佔。
第二步:釋放鎖。
所以,如果在單處理器支持內核搶佔的內核下,spin_lock()函數會退化成關搶佔。在單處理器不支持內核搶佔的內核下,這將會是一條空語句。
上面的代碼防了兩種情況,但還沒防中斷,防中斷有兩種方法:
方法一:在需要訪問臨界區的中斷代碼也加鎖:
do_irq() //中斷處理函數
{
spin_lock();
/*臨界區。。*/
spin_unlock();
}
方法二:直接在加鎖的同時把中斷也禁掉:
#include <linux/spinlock.h>
spinlock_t lock;
spin_lock_init(&my_dev.lock);
unsigned long flag = 0;
loacl_irq_save(flag);
spin_lock(&lock);
臨界區。。。。。
local_irq_restroe(flag);
spin_unlock(&lock);
當然,貼心的內核工作者將兩個函數合成一個函數,只用調用一個函數就能既上鎖有關中斷了:
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)
自旋鎖的一個重要特徵是,只要沒獲得鎖,進程會佔用CPU查詢,直到獲得鎖,有些人不想查詢,可以使用以下函數:
int spin_try_trylock(spinlock_t *lock);
一看函數名字就知道,他是嘗試獲得鎖,成功返回非零,失敗返回零。
這個強大的功能必定有他的弊端:
弊端一:持有鎖的時間必須儘量的短:
進程在沒獲得鎖前不進入睡眠,而是會佔用CPU查詢,這樣的做法是爲了節省進程從TASK_RUNNING切換至TASK_INTERRUPTIBLE後又切換回來消耗的時間。同時也是出於這樣的原因,被上鎖的臨界區代碼必須儘量的短。
弊端二:持有鎖的期間不能睡眠:
也就是說,在臨界區的代碼裏不能有引起睡眠的操作。譬如,一個進程上鎖後睡眠,此時切換執行中斷處理函數,可中斷處理函數也要獲得鎖,這樣就會使中斷自旋,並且沒人能打斷。
最簡單的生活例子,上廁所的時候你鎖上門睡覺了,還讓別人在門口瞎等!這種事情多不合理!
弊端三:要注意上鎖的順序:
如果進程進入臨界區前需要那A、B兩把鎖,一個進程拿了A,另一個進程拿了B,它們死活也不讓步,都不能獲得另外一把鎖,那只好在臨界區代碼前死等了。
弊端四:不能嵌套上鎖:
簡單的說,就是獲得鎖後後的進程不能再上一次同樣的鎖。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
三、信號量
上面說了自旋鎖的缺點,如不能睡眠,要求臨界區執行時間儘可能的短。出於這樣的情況,就有了另一種內核同步的機制——信號量。
信號量是一種睡眠鎖,當進程試圖獲取已經被佔同的信號量,他就會被放到等待隊列中,直到信後信號裏釋放後被喚醒。
繼續剛纔上鎖的廁所,話說A把門鎖上後上廁所,B要來上廁所是看到廁所被佔用了,於是,他在門口上貼張紙條“我是B,你出來後叫我上廁所”,然後就離開了。A出來後,看到門口有紙條,就按照紙條所說的去通知B。
所以,信號量就是允許長時間上鎖的睡眠鎖。
接下來看一下怎麼使用信號量,信號量有兩種:互斥信號量和計數信號量。互斥信號量,就是說同一時間只能有一個進程獲得鎖並進入臨界區。而計數信號量,那就是鎖的數量可以多於一個,允許多個獲得鎖的進程進入臨界區,同時這也是和自旋鎖不同的地方。
以下的函數需要包含頭文件<asm?semaphore.h>,信號量使用數據類型struct semaphore表示。
一、創建和初始化信號量:
同樣有兩種方法。
第一種是靜態定義並初始化
static DECLARE_SEMAPHORE_GENERIC(name, count)
定義並初始化一個叫name的計數信號量,允許conut個進程同時持有鎖。
static DECLARE_MUTEX(name)\
定義並初始化一個叫name的互斥信號量。
第二種是動態定義並初始化
首先你要定義一個信號量結構體:
struct semaphore sem;
然後初始化:初始化是指定信號量的個數
sema_init(&sem, count);
當然也有一些方便定義互斥信號量的函數:
/*初始化一個互斥信號量*/
#define init_MUTEX(sem) sema_init(sem, 1)
/*初始化一個互斥信號量並加鎖*/
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0)
二、使用信號量:
一般的獲得信號量有三個函數:
1/*獲取信號量sem,如果不能獲取,切換狀態至TASK_UNINTERRUPTIBLE*/
voud down(struct semaphore *sem)
上面的函數不太常用,因爲它的睡眠不能被中斷打斷,一般使用下面的函數
2/*獲取信號量sem,如果不能獲取,切換狀態至TASK_INTERRUPTIBLE,如果睡眠期間被中斷打斷,函數返回非0值*/
int down_interruputible(struct semaphore *sem)
3/*嘗試獲得信號量,如果獲得信號量就返回零,不能獲得也不睡眠,返回非零值*/
int down_try_lock(struct semaphore *sem)
因爲上面的函數在睡眠時會被中斷打斷,一般會如下使用:
if (down_interruptible(&sem)){
return – ERESTARTSYS;
}
即如果在睡眠期間被中斷打斷,返回-ERESTARTSYS給用戶,告知用戶重新執行。如果是被喚醒,則會往下執行。
釋放信號量函數:
void up(struct semaphore *sem);
所以,信號量一般這樣使用:
#include <asm/semaphore.h>
struct semaphore sem;
sema_init(&sem, 1);
if (down_interruptible(&sem)){
return – ERESTARTSYS;
}
臨界區代碼。。。。。
up(&sem);
這4th_mutex_5/2nd/test.c我寫了加上信號量的代碼,還是那一句,代碼不規範(在死循環加信號量無疑是自殺),我只是想告訴大家這幾條函數一般使用在什麼地方。在搶佔式內核的情況下,使用信號量和使用自旋鎖保護代碼會不一樣。
[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& //先後臺運行app_read
<kernel>[test_open]
<app_read> pid[400] //注意進程號400
<kernel>[test_read]task pid[400], context [app_read]
[root: app]# <kernel>[test_read]task pid[400], context [app_read]
<kernel>[test_read]task pid[400], context [app_read]
<kernel>[test_read]task pid[400], context [app_read]
[root: app]# ./app_read& //再後臺運行一個app_read
<kernel>[test_open]
<app_read> pid[401] //注意進程號401,後面的打印沒有一個是401!!!
[root: app]# <kernel>[test_read]task pid[400], context [app_read]
<kernel>[test_read]task pid[400], context [app_read]
<kernel>[test_read]task pid[400], context [app_read]
<kernel>[test_read]task pid[400], context [app_read]
<kernel>[test_read]task pid[400], context [app_read]
<kernel>[test_read]task pid[400], context [app_read]
<kernel>[test_read]task pid[400], context [app_read]
[root: app]# ./app //後臺運行app
<app> runing
<kernel>[test_read
key down
key down
key down
<kernel>[test_read]task pid[400], context [app_read]
<app> runing
<kernel>[test_read]task pid[400], context [app_read]
<app> runing
]task pid[400], context [app_read]
<app> runing //app能打印!!!!!
<kernel>[test_read]task pid[400], context [app_read]
<kernel>[test_read]task pid[400], context [app_read]
<app> runing
key down //中斷也能執行!!!
key down
key down
key down
key down
<kernel>[test_read]task pid[400], context [app_read]
<app> runing
<kernel>[test_read]task pid[400], context [app_read]
<app> runing
不知道各位注意到上面的現象與自旋鎖的有什麼區別。
第一:信號量沒有關搶佔,如果別的進程沒有訪問上鎖的臨界區(如app),這個進程照樣可以運行。
第二:訪問了上鎖臨界區的進程,就不能執行了(如第二次運行的app_read)。
第三:臨界區還是可以被中斷打斷的,因爲信號量根本沒關中斷,如果臨界區的資源不能被中斷訪問,那就像之前說的處理,要不在中斷處理函數在進入臨界區前獲得鎖,要不就把中斷也關了。
所以,簡單的說,信號量就是一個數,你獲得這個數了,你就可以進去臨界區。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
四、信號量與自旋鎖的區別:
既然上面介紹了兩種鎖的機制和使用的方法,接下來就到對比一下兩種鎖的區別,應該在哪裏使用。
區別一:實現方式
自旋鎖是自旋等待,進程狀態始終處於TASK_RUNNING。
信號量是睡眠等待,進程在等待是處於TASK_INTERRUPTIBLE。
區別二:睡眠死鎖陷阱:
在自旋鎖的臨界區中,進程是不能陷入睡眠的。
而信號量可以睡眠。
同時,基於上面的原因,中斷上下文中只能使用自旋鎖(中斷裏不能休眠),在有睡眠代碼的臨界區只能使用信號量
區別三:CPU的使用情況:
明顯的,信號量對系統的負載小,因爲它睡眠了。
區別四:執行的效率方面:
自旋鎖的效率比較高,因爲它少了進程狀態切換的消耗。
相對的信號量的效率比較低,因爲進程的等待需要切換進程狀態。
區別五:上鎖的時間長短:
因爲自旋鎖是忙等待,所以臨界區的代碼不能太長。
而信號量可以使用在運行時間較長的臨界區代碼。
區別六:是否關搶佔:
自旋鎖是關搶佔的,所以在單處理器非搶佔的內核下,自旋鎖是沒用的。是空操作。
信號量並沒有關搶佔,所以,只有需要獲得鎖的進程纔會睡眠,其他進程還可以繼續運行,如上面的例子。
居於上面的區別,有這樣的一個表:
需求 |
建議的加鎖方法 |
低開銷的加鎖 |
優先考慮自旋鎖 |
短時間的加鎖 |
優先考慮自旋鎖 |
長時間的加鎖 |
優先是使用信號量 |
中斷上下文中加鎖 |
必須使用自旋鎖 |
上鎖後會有睡眠 |
必須使用信號量 |
還是那一句,個人喜好與需求,像我這種小白一般是不需要用到內核同步的機制的,因爲我的開發板是單處理器非搶佔內核。
接下來介紹一下其他的內核同步方法,但是我全都沒用過。。。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
五、互斥量
這是2.6內核新加的,是互斥信號量的升級版。
其實上面介紹了兩種鎖使用的情況,其實,可以睡眠的臨界區,都可以使用信號量,這就是信號量強大的地方。然而,越強大的功能,內核實現起來就越是困難。所以。內核開發者實現了輕量級的睡眠鎖——互斥量。
使用互斥量使用結構體struct mutex來表示:
一、定義並初始化,兩種方法:
靜態定義:
DEFINE_MUTEX(name)
動態定義並初始化:
struct mutex mutex;
mutex_init(&mutex);
二、互斥量的操作:
獲得互斥裏
void inline __sched mutex_lock(struct mutex *lock) //不能獲得鎖是進入不可中斷睡眠
int __sched mutex_lock_interruptible(struct mutex *lock) //進入可中斷睡眠
int __sched mutex_trylock(struct mutex *lock) //嘗試獲得鎖
這三個函數的用法的信號量的三個完全一樣,返回值也是,所以我就不細講了。
釋放信號量:
void __sched mutex_unlock(struct mutex *lock)
當然,互斥量是升級版的輕量級信號量,它必然會有限制:
1)同一時間只能有一個進程獲得鎖,這是互斥的概念。
2)只能在同一進程上鎖和解鎖,而信號量不一樣,可以在這個進程上鎖,另外的進程解鎖。
3)同一個進程獲得鎖後這段期間在獲得這個鎖,也就是說不能遞歸使用,原因很簡單,因爲是互斥,上鎖的只有一次,只能解鎖有在上鎖。
4)進程持有鎖是不能退出。
5)中斷上下文不能使用鎖,即使是mutex_trylock()。
6)互斥鎖只能通過內核提供的API接口來操作。
內核推薦,在能使用互斥鎖的情況下優先考慮,而不是使用信號量。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
六、原子操作(atomic_t)
所謂的原子操作,就是這段代碼不會被其它進程打斷,所以,加上自旋鎖等鎖之後的操作也算是原子操作。
而這裏要介紹的原子操作和上面的不一樣。考慮一下,如果你加鎖只是爲了保護一個整數,你有必要大費周章的使用自旋鎖了,只要你把操作這個數的代碼濃縮成一條指令,不就可以了嗎?
內核提供了兩種的原子操作:原子整數操作和原子位操作。顧名思義,就是在操作這個整數或者設置一個數的位數時,是不會被打斷的。
具體的函數操作我就不講了,我也沒用過,書上也講得很詳細:《linux內核設計與實現(第三版)》P175頁。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
七、總結:
這節講了在多處理器的情況下如何實現內核同步,避免臨界區併發訪問,當然,上面介紹的方法需要用在真正需要的地方,因爲我使用的是單處理器非搶佔式內核,所以也沒有太多的例子和代碼,只能粗略的描述各種鎖機制的優缺點和實現的機制。可能講得不好,如果有疑問可以提出,我儘量改善。
同時,內核同步的機制還有很多,譬如讀寫鎖等,都在書上有詳細的描述。
當我還是個小小白的時候,我一直在納悶自旋鎖信號量究竟使用在什麼地方,現在才發現,在我開發板如此低級的內核上,只要防中斷就可以了。
xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
源代碼: 4th_mutex_5.rar