訪問共享資源的代碼區域稱爲臨界區( critical sections),臨界區需要被以某種互斥機制加以保護,方法有中斷屏蔽、原子操作、自旋鎖和信號量等。
在驅動程序中,當多個線程同時訪問相同的資源時(驅動程序中的全局變量是一種典型的共享資源),可能會引發"競態",因此我們必須對共享資源進行併發控制。Linux內核中解決併發控制的最常用方法是自旋鎖與信號量(絕大多數時候作爲互斥鎖使用)。
1、中斷屏蔽
在單 CPU 範圍內避免競態的一種簡單而省事的方法是在進入臨界區之前屏蔽系統的中斷。
local_irq_disable() /* 屏蔽中斷 */
. . .
critical section /* 臨界區*/
. . .
local_irq_enable() /* 開中斷*/
如果只是想禁止中斷的底半部,應使用 local_bh_disable(),使能被 local_bh_disable()禁止的底半部應該調用local_bh_enable()。
由於 Linux 的異步 I/O、進程調度等很多重要操作都依賴於中斷,中斷對於內核的運行非常重要,在屏蔽中斷期間所有的中斷都無法得到處理,因此長時間屏蔽中斷是很危險的,有可能造成數據丟失乃至系統崩潰等後果。這就要求在屏蔽了中斷之後,當前的內核執行路徑應當儘快地執行完臨界區的代碼。
local_irq_disable()和 local_irq_enable()都只能禁止和使能本 CPU 內的中斷,因此,並不能解決 SMP 多 CPU 引發的競態。因此,單獨使用中斷屏蔽通常不是一種值得推薦的避免競態的方法, 它適宜與下文將要介紹的自旋鎖聯合使用。
2、自旋鎖
自旋鎖( spin lock)是一種典型的對臨界資源進行互斥訪問的手段,其名稱來源於它的工作方式。爲了獲得一個自旋鎖, 在某 CPU 上運行的代碼需先執行一個原子操作,該操作測試並設置( test-and-set) 某個內存變量,由於它是原子操作,所以在該操作完成之前其他執行單元不可能訪問這個內存變量。如果測試結果表明鎖已經空閒,則程序獲得這個自旋鎖並繼續執行; 如果測試結果表明鎖仍被佔用,程序將在一個小的循環內重複這個“ 測試並設置” 操作,即進行所謂的“ 自旋”,通俗地說就是“在原地打轉”。 當自旋鎖的持有者通過重置該變量釋放這個自旋鎖後,某個等待的“測試並設置” 操作向其調用者報告鎖已釋放。
自旋鎖一般這樣被使用:
/* 定義一個自旋鎖*/
spinlock_t lock; //定義自旋鎖
spin_lock_init(&lock); //初始化自旋鎖
spin_lock (&lock) ; /* 獲取自旋鎖,保護臨界區 */
. . ./* 臨界區*/
spin_unlock (&lock) ; /* 解鎖*/
自旋鎖主要針對 SMP 或單 CPU 但內核可搶佔的情況,對於單 CPU 和內核不支持搶佔的系統, 自旋鎖退化爲空操作。
儘管用了自旋鎖可以保證臨界區不受別的 CPU 和本 CPU 內的搶佔進程打擾,但是得到鎖的代碼路徑在執行臨界區的時候,還可能受到中斷和底半部( BH,稍後的章節會介紹)的影響。爲了防止這種影響,就需要用到自旋鎖的衍生。 spin_lock()/spin_unlock()是自旋鎖機制的基礎,它們和關中斷 local_irq_disable()/開中斷 local_irq_enable()、關底半部 local_bh_disable()/開底半部local_bh_enable()、關中斷並保存狀態字 local_irq_save()/開中斷並恢復狀態 local_irq_restore()結合
就形成了整套自旋鎖機制,關係如下:
spin_lock_irq() = spin_lock() + local_irq_disable()
spin_unlock_irq() = spin_unlock() + local_irq_enable()
spin_lock_irqsave() = spin_lock() + local_irq_save()
spin_unlock_irqrestore() = spin_unlock() + local_irq_restore()
spin_lock_bh() = spin_lock() + local_bh_disable()
spin_unlock_bh() = spin_unlock() + local_bh_enable()
驅動工程師應謹慎使用自旋鎖:
( 1)自旋鎖實際上是忙等鎖,因此,只有在佔用鎖的
時間極短的情況下,使用自旋鎖纔是合理的。 當臨界區很大,或有共享設備的時候,需要較長時間佔用鎖,使用自旋鎖會降低系統的性能。
( 2)自旋鎖可能導致系統死鎖。引發這個問題最常見的情況是遞歸使用一個自旋鎖,即如果一個已經擁有某個自旋鎖的 CPU 想第二次獲得這個自旋鎖,則該 CPU 將死鎖。
( 3)自旋鎖鎖定期間不能調用可能引起進程調度的函數。如果進程獲得自旋鎖之後再阻塞, 如調用 copy_from_user()、 copy_to_user()、 kmalloc()和 msleep()等函數,則可能導致內核的崩潰。
3、信號量
信號量( semaphore)是用於保護臨界區的一種常用方法,它的使用方式和自旋鎖類似。與自旋鎖相同,只有得到信號量的進程才能執行臨界區代碼。但是,與自旋鎖不同的是,當獲取不到信號量時,進程不會原地打轉而是進入休眠等待狀態。
1.定義信號量
下列代碼定義名稱爲 sem 的信號量:
struct semaphore sem;
2.初始化信號量
void sema_init(struct semaphore *sem, int val);
該函數初始化信號量,並設置信號量 sem 的值爲 val。儘管信號量可以被初始化爲大於 1 的值從而成爲一個計數信號量,但是它通常不被這樣使用。
#define init_MUTEX(sem) sema_init(sem, 1)
該宏用於初始化一個用於互斥的信號量,它把信號量 sem 的值設置爲 1;
#define init_MUTEX_LOCKED(sem) sema_init(sem, 0)
該宏也用於初始化一個信號量,但它把信號量 sem 的值設置爲 0;
此外,下面兩個宏是定義並初始化信號量的“快捷方式”:
DECLARE_MUTEX(name)
DECLARE_MUTEX_LOCKED(name)
前者定義一個名爲 name 的信號量並初始化爲 1;後者定義一個名爲 name 的信號量並初始化爲 0。
3.獲得信號量
void down(struct semaphore * sem);
該函數用於獲得信號量 sem,它會導致睡眠,因此不能在中斷上下文使用;
int down_interruptible(struct semaphore * sem);
該函數功能與 down 類似,不同之處爲,因爲 down()而進入睡眠狀態的進程不能被信號打斷,但因爲 down_interruptible()而進入睡眠狀態的進程能被信號打斷,信號也會導致該函數返回,這時候函數的返回值非 0;
int down_trylock(struct semaphore * sem);
該函數嘗試獲得信號量 sem,如果能夠立刻獲得,它就獲得該信號量並返回 0,否則,返回非 0 值。它不會導致調用者睡眠,可以在中斷上下文使用。
在使用 down_interruptible()獲取信號量時,對返回值一般會進行檢查,如果非 0,通常立即返回- ERESTARTSYS,如:
if (down_interruptible(&sem))
return - ERESTARTSYS;
4.釋放信號量
void up(struct semaphore * sem);
該函數釋放信號量 sem,喚醒等待者。
信號量一般這樣被使用:
/* 定義信號量
DECLARE_MUTEX(mount_sem);
down(&mount_sem);/* 獲取信號量,保護臨界區
. . .
critical section /* 臨界區
. . .
up(&mount_sem);/* 釋放信號量
1 /*增加併發控制後的 globalmem 讀函數*/
2 static ssize_t globalmem_read(struct file *filp, char _ _user *buf, size_t size,
3 loff_t *ppos)
4 {
5 unsigned long p = *ppos;
6 unsigned int count = size;
7 int ret = 0;
8 struct globalmem_dev *dev = filp->private_data; /*獲得設備結構體指針*/
9
10 /*分析和獲取有效的寫長度*/
11 if (p >= GLOBALMEM_SIZE)
12 return 0;
13 if (count > GLOBALMEM_SIZE - p)
14 count = GLOBALMEM_SIZE - p;
15
16 if (down_interruptible(&dev->sem)) /* 獲得信號量*/
17 return - ERESTARTSYS;
18
19 /*內核空間→用戶空間*/
20 if (copy_to_user(buf, (void*)(dev->mem + p), count)) {
21 ret = - EFAULT;
22 } else {
23 *ppos += count;
24 ret = count;
25
26 printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
27 }
28 up(&dev->sem); /* 釋放信號量*/
29
30 return ret;
31 }
32
33 /*增加併發控制後的 globalmem 寫函數*/
34 static ssize_t globalmem_write(struct file *filp, const char _ _user *buf,
35 size_t size, loff_t *ppos)
36 {
37 unsigned long p = *ppos;
38 unsigned int count = size;
39 int ret = 0;
40 struct globalmem_dev *dev = filp->private_data; /*獲得設備結構體指針*/
41
42 /*分析和獲取有效的寫長度*/
43 if (p >= GLOBALMEM_SIZE)
44 return 0;
45 if (count > GLOBALMEM_SIZE - p)
46 count = GLOBALMEM_SIZE - p;
47
48 if (down_interruptible(&dev->sem)) /* 獲得信號量 */
49 return - ERESTARTSYS;
50
51 /*用戶空間→內核空間*/
52 if (copy_from_user(dev->mem + p, buf, count))
53 ret = - EFAULT;
54 else {
55 *ppos += count;
56 ret = count;
57
58 printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
59 }
60 up(&dev->sem); /* 釋放信號量*/
61 return ret;
62 }
如果信號量被初始化爲 0,則它可以用於同步,同步意味着一個執行單元的繼續執行需等待另一執行單元完成某事,保證執行的先後順序。如圖 所示,執行單元 A 執行代碼區域 b 之前,必須等待執行單元 B 執行完代碼單元 c,信號量可輔助這一同步過程。
4、completion(完成量)
completion是比用上述信號量做同步更好的方法,用於一個執行單元等待另一個執行單元執行完某事。
1.定義完成量
下列代碼定義名爲 my_completion 的完成量:
struct completion my_completion;
2.初始化 completion
下列代碼初始化 my_completion 這個完成量:
init_completion(&my_completion);
對 my_completion 的定義和初始化可以通過如下快捷方式實現:
DECLARE_COMPLETION(my_completion);
3.等待完成量
下列函數用於等待一個 completion 被喚醒:
void wait_for_completion(struct completion *c);
4.喚醒完成量
下面兩個函數用於喚醒完成量:
void complete(struct completion *c);
void complete_all(struct completion *c);
前者只喚醒一個等待的執行單元,後者釋放所有等待同一完成量的執行單元。
自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環查看是否該自旋鎖的保持者已經釋放了鎖,"自旋"就是"在原地打轉"。而信號量則引起調用者睡眠,它把進程從運行隊列上拖出去,除非獲得鎖。這就是它們的"不類"。
sem就是一個睡眠鎖.如果有一個任務試圖獲得一個已被持有的信號量時,信號量會將其推入等待隊列,然後讓其睡眠。這時處理器獲得自由去執行其它代碼。當持有信號量的進程將信號量釋放後,在等待隊列中的一個任務將被喚醒,從而便可以獲得這個信號量。信號量一般在用進程上下文中.它是爲了防止多進程同時訪問一個共享資源(臨界區).
spin_lock叫自旋鎖.就是當試圖請求一個已經被持有的自旋鎖.這個任務就會一直進行 忙循環——旋轉——等待,直到鎖重新可用(它會一直這樣,不釋放CPU,它只能用在短時間加鎖).它是爲了防止多個CPU同時訪問一個共享資源(臨界區).它一般用在中斷上下文中,因爲中斷上下文不能被中斷,也不能被調度。
但是,無論是信號量,還是自旋鎖,在任何時刻,最多隻能有一個保持者,即在任何時刻最多隻能有一個執行單元獲得鎖。這就是它們的"類似"。
鑑於自旋鎖與信號量的上述特點,一般而言,自旋鎖適合於保持時間非常短的情況,它可以在任何上下文使用;信號量適合於保持時間較長的情況,會只能在進程上下文使用。如果被保護的共享資源只在進程上下文訪問,則可以以信號量來保護該共享資源,如果對共享資源的訪問時間非常短,自旋鎖也是好的選擇。但是,如 果被保護的共享資源需要在中斷上下文訪問(包括底半部即中斷處理句柄和頂半部即軟中斷),就必須使用自旋鎖。
區別總結如下:
1、由於爭用信號量的進程在等待鎖重新變爲可用時會睡眠,所以信號量適用於鎖會被長時間持有的情況。
2、相反,鎖被短時間持有時,使用信號量就不太適宜了,因爲睡眠引起的耗時可能比鎖被佔用的全部時間還要長。
3、由於執行線程在鎖被爭用時會睡眠,所以只能在進程上下文中才能獲取信號量鎖,因爲在中斷上下文中(使用自旋鎖)是不能進行調度的。
4、你可以在持有信號量時去睡眠(當然你也可能並不需要睡眠),因爲當其它進程試圖獲得同一信號量時不會因此而死鎖,(因爲該進程也只是去睡眠而已,而你最終會繼續執行的)。
5、在你佔用信號量的同時不能佔用自旋鎖,因爲在你等待信號量時可能會睡眠,而在持有自旋鎖時是不允許睡眠的。
6、信號量鎖保護的臨界區可包含可能引起阻塞的代碼,而自旋鎖則絕對要避免用來保護包含這樣代碼的臨界區,因爲阻塞意味着要進行進程的切換,如果進程被切換出去後,另一進程企圖獲取本自旋鎖,死鎖就會發生。
7、信號量不同於自旋鎖,它不會禁止內核搶佔(自旋鎖被持有時,內核不能被搶佔),所以持有信號量的代碼可以被搶佔,這意味着信號量不會對調度的等待時間帶來負面影響。
除了以上介紹的同步機制方法以外,還有BKL(大內核鎖),Seq鎖等。
BKL是一個全局自旋鎖,使用它主要是爲了方便實現從Linux最初的SMP過度到細粒度加鎖機制。
Seq鎖用於讀寫共享數據,實現這樣鎖只要依靠一個序列計數器。
自旋鎖和信號量選用的 3 項原則:
( 1)當鎖不能被獲取到時,使用信號量的開銷是進程上下文切換時間 Tsw,使用自旋鎖的開銷是等待獲取自旋鎖(由臨界區執行時間決定) Tcs,若 Tcs 比較小,宜使用自旋鎖,若 Tcs 很大,應使用信號量。
( 2)信號量所保護的臨界區可包含可能引起阻塞的代碼,而自旋鎖則絕對要避免用來保護包含這樣代碼的臨界區。因爲阻塞意味着要進行進程的切換,如果進程被切換出去後,另一個進程企圖獲取本自旋鎖,死鎖就會發生。
( 3)信號量存在於進程上下文,因此,如果被保護的共享資源需要在中斷或軟中斷情況下使用,則在信號量和自旋鎖之間只能選擇自旋鎖。當然,如果一定要使用信號量,則只能通過down_trylock()方式進行,不能獲取就立即返回以避免阻塞。
自旋鎖對信號量:
需求 | 建議的加鎖方法 |
低開銷加鎖 | 優先使用自旋鎖 |
短期鎖定 | 優先使用自旋鎖 |
長期加鎖 | 優先使用信號量 |
中斷上下文中加鎖 | 使用自旋鎖 |
持有鎖是需要睡眠、調度 | 使用信號量 |
進程間的sem.線程間的sem與內核中的sem的功能就很類似.
進程間的sem,線程間的sem功能是一樣的.只是線程的sem,它在同一個進程空間,他的初始化,使用更方便.
進程間的sem,就是進程間通信的一部分,使用semget,semop等系統調用來完成.
內核中的sem 被鎖定,就等於被調用的進程佔有了這個sem.其它進程就只能進行睡眠隊列.這與進程間的sem基本一致.
區別 |
Spin_lock |
semaphore |
保護的對象 |
一段代碼 |
一個設備(必要性不強), 一個變量, 一段代碼 |
保護區可被搶佔 |
不可以(會被中斷打斷) |
可以。 |
可允許在保護對象(代碼)中休眠 |
不可以 |
可以。但最好不這樣。 |
保護區能否被中斷打斷 |
可以,這樣容易引發死鎖。 最好是關了中斷再使用此鎖。 因爲有可能中斷處理例程也需要得到同一個鎖。 |
可以。 |
其它功能 |
可完成同步,有傳達信息的能力。 |
|
試圖佔用鎖不成功後,進程的表現 |
不放開CPU,自己自旋。 |
進入一個等待隊列。 |
釋放鎖後,還有其它進程等待時,內核如何處理 |
哪個進程得到運行的權力,它就得到了鎖。 |
從等待隊列中選一個出來佔用此sem. |
內核對使用者的要求 |
被保護的代碼執行時間要短,是原子的, 不能主動的休眠。 不能調用有可以休眠的內核函數。 |
|
風險 |
發生死鎖 |
|
不允許鎖的持有者二次請求同一個鎖。 |
不允許鎖的持有者二次請求同一個鎖。 |
信號量在生產者與消費者模式中可以進行同步。
當sem的down和UP分別出現在對立函數中(讀,寫函數),其實這就是在傳達一種信息。表示當前是否有數據可讀的信息。
read_somthing()
{
down(設備) 佔用了此設備 此時沒有其它人都使用此設備上的所有操作(函數)
if(有數據)
{
讀完它。
()
}
else
{
up(設備)
down(有數據的sem)sem=1表示有數據,爲0表示無數據。
}
}
write_somthing()
{
down(設備) 佔用了此設備 此時沒有其它人都使用此設備上的所有操作(函數)
if(有數據)
{
不寫。
up(設備)
return
}
else
{
寫入數據
up(有數據的sem)sem=1表示有數據,爲0表示無數據。
up(設備)
return;
}
}
總結:
信號量適用於長時間片段,可能會睡眠(掛起調度) 所以只能用在進程上下文,不能用在中斷上下文。
自旋鎖使用於短時間片段 不會睡眠(掛起調度)抱着cpu不放, 用在中斷上下文 但是必須先關閉本地中斷,否則很可能因爲
獲取不到自旋鎖又抱着cpu不讓別人持有而釋放自旋鎖從而陷入死鎖。
信號量可以用的前提下儘量用信號量,萬不得已(中斷中)使用自旋鎖,短時間片段比較適合自旋鎖,調度(進程之間的切換)本身
佔用時間,自旋等待的時間很短時就沒必要調度了,這時選用自旋鎖死抱cpu不放比較好。
5、互斥體
儘管信號量已經可以實現互斥的功能,但是“正宗”的 mutex 在 Linux 內核中還是真實地存在着。
1、定義名爲 my_mutex 的互斥體
struct mutex my_mutex;
2、初始化它
mutex_init(&my_mutex);
3、獲取互斥體的兩個函數
void inline _ _sched mutex_lock(struct mutex *lock);
int _ _sched mutex_lock_interruptible(struct mutex *lock);
int _ _sched mutex_trylock(struct mutex *lock);
mutex_lock()與 mutex_lock_interruptible()的區別和 down()與 down_trylock()的區別完全一致,前者引起的睡眠不能被信號打斷,而後者可以。 mutex_trylock()用於嘗試獲得 mutex,獲取不到mutex 時不會引起進程睡眠。
4、釋放互斥體
void __sched mutex_unlock(struct mutex *lock);
mutex 的使用方法和信號量用於互斥的場合完全一樣:
struct mutex my_mutex; /* 定義 mutex */
mutex_init(&my_mutex); /* 初始化 mutex */
mutex_lock(&my_mutex); /* 獲取 mutex */
.../* 臨界資源*/
mutex_unlock(&my_mutex); /* 釋放 mutex */
6、時間函數
忙等待(一直消耗cpu)
ndelay、udelay、mdelay
unsigned long delay = jiffies + 100;
while(time_before(jiffies,delay));
睡着等待(不會一直消耗cpu)
msleep()、msleep_interruptible()、ssleep()、interruptible_sleep_on_timeout();