Linux設(shè)備驅(qū)動(dòng)
中必須解決的一個(gè)問題是多個(gè)進(jìn)程對(duì)共享資源的并發(fā)訪問,,并發(fā)訪問會(huì)導(dǎo)致競(jìng)態(tài),linux提供了多種解決競(jìng)態(tài)問題的方式,這些方式適合不同的應(yīng)用場(chǎng)景,。
Linux內(nèi)核是多進(jìn)程,、多線程的操作系統(tǒng),它提供了相當(dāng)完整的內(nèi)核同步方
法,。內(nèi)核同步方法列表如下:
中斷屏蔽
原子操作
自旋鎖
讀寫自旋鎖
順序鎖
信號(hào)量
讀寫信號(hào)量
BKL(大內(nèi)核鎖)
Seq鎖
一,、并發(fā)與競(jìng)態(tài):
定義:
并發(fā)(concurrency)指的是多個(gè)執(zhí)行單元同時(shí)、并行被執(zhí)行,,而并發(fā)的執(zhí)行單元對(duì)共
享資源(硬件資源和軟件上的全局變量,、靜態(tài)變量等)的訪問則很容易導(dǎo)致競(jìng)態(tài)(race conditions)。
在linux中,,主要的競(jìng)態(tài)發(fā)生在如下幾種情況:
1,、對(duì)稱多處理器(SMP)多個(gè)CPU
特點(diǎn)是多個(gè)CPU使用共同的系統(tǒng)總線,,因此可訪問共同的外設(shè)和存儲(chǔ)器,。
2、單CPU內(nèi)進(jìn)程與搶占它的進(jìn)程
3,、中斷(硬中斷,、軟中斷、Tasklet,、底半部)與進(jìn)程之間
只要并發(fā)的多個(gè)執(zhí)行單元存在對(duì)共享資源
的訪問,,競(jìng)態(tài)就有可能發(fā)生。
如果中斷處理程序訪問進(jìn)程正在訪問的資
源,,則競(jìng)態(tài)也會(huì)會(huì)發(fā)生,。
多個(gè)中斷之間本身也可能引起并發(fā)而導(dǎo)致
競(jìng)態(tài)(中斷被更高優(yōu)先級(jí)的中斷打斷)。
解決競(jìng)態(tài)問題的途徑是保證對(duì)共享資源的互斥訪問,,所謂互斥訪問就是指一個(gè)執(zhí)行單元
在訪問共享資源的時(shí)候,,其他的執(zhí)行單元都被禁止訪問。
訪問共享資源的代碼區(qū)域被稱為臨界區(qū),,臨界區(qū)需要以某種互斥機(jī)
制加以保護(hù),,中斷屏蔽,原子操作,,自旋鎖,,和信號(hào)量都是linux設(shè)備驅(qū)動(dòng)中可采用的互斥途徑。
臨界區(qū)和競(jìng)爭(zhēng)條件:
所謂臨界區(qū)(critical
regions)就
是訪問和操作共享數(shù)據(jù)的代碼段,,為了避免在臨界區(qū)中并發(fā)訪問,,編程者必須保證這些代碼原子地執(zhí)行——也就是說,代碼在執(zhí)行結(jié)束前不可被打斷,,就如同整個(gè)臨
界區(qū)是一個(gè)不可分割的指令一樣,,如果兩個(gè)執(zhí)行線程有可能處于同一個(gè)臨界區(qū)中,那么就是程序包含一個(gè)bug,如果這種情況發(fā)生了,,我們就稱之為競(jìng)爭(zhēng)條件(race
conditions),,
避免并發(fā)和防止競(jìng)爭(zhēng)條件被稱為同步。
死鎖:
死鎖的產(chǎn)生需要一定條件:要有一個(gè)或多個(gè)執(zhí)行線程和一個(gè)或多個(gè)資
源,,每個(gè)線程都在等待其中的一個(gè)資源,,但所有的資源都已經(jīng)被占用了,所有線程都在相互等待,,但它們永遠(yuǎn)不會(huì)釋放已經(jīng)占有的資源,,于是任何線程都無法繼續(xù),
這便意味著死鎖的發(fā)生,。
二,、中斷屏蔽
在單CPU范圍內(nèi)避免競(jìng)態(tài)的一種簡(jiǎn)單方法是在進(jìn)入臨界區(qū)之前屏蔽系統(tǒng)的中斷。
由于linux內(nèi)核的進(jìn)程調(diào)度等操作都依賴中斷來實(shí)現(xiàn),,內(nèi)核搶占進(jìn)程之間的并發(fā)也就得以避免了,。
中斷屏蔽的使用方法:
local_irq_disable()//屏蔽中斷
//臨界區(qū)
local_irq_enable()//開中斷
特點(diǎn):
由于linux系統(tǒng)的異步IO,進(jìn)程調(diào)度等很多重要操作都依賴于中斷,,在屏蔽中斷期間所有的
中斷都無法得到處理,,因此長(zhǎng)時(shí)間的屏蔽是很危險(xiǎn)的,有可能造成數(shù)據(jù)丟失甚至系統(tǒng)崩潰,,這就要求在屏蔽中斷之后,,當(dāng)前的內(nèi)核執(zhí)行路徑應(yīng)當(dāng)盡快地執(zhí)行完臨界區(qū)
的代碼。
中斷屏蔽只能禁止本CPU內(nèi)的中斷,,因此,,并不能解決多CPU引發(fā)的競(jìng)態(tài),所以單獨(dú)使用中斷屏蔽并不是一個(gè)值得推薦的避免競(jìng)
態(tài)的方法,,它一般和自旋鎖配合使用,。
三、原子操作
定義:原子操作指的是在執(zhí)行過程中不會(huì)被別的代碼路徑所中斷的操作,。
(原子原本指的是不可分割的微粒,,所以原子操作也就是不能夠被分
割的指令)
(它保證指令以“原子”的方式執(zhí)行而不能被打斷)
原子操作是不可分割的,在執(zhí)行完畢不會(huì)
被任何其它任務(wù)或事件中斷,。在單處理器系統(tǒng)(UniProcessor)中,,能夠在單條指令中完成的操作都
可以認(rèn)為是" 原子操作",因?yàn)橹袛嘀荒馨l(fā)生于指令之間,。這也是某些CPU指令系統(tǒng)中引入了test_and_set,、test_and_clear等指令用于臨界資源互斥的原因。但是,,在對(duì)稱多處理器(Symmetric Multi-Processor)結(jié)構(gòu)中就不同了,,由于系統(tǒng)中有多個(gè)處理
器在獨(dú)立地運(yùn)行,,即使能在單條指令中完成的操作也有可能受到干擾。我們以decl (遞減指令)為例,,這是一個(gè)典型的"讀-改-寫"過程,,涉及兩次內(nèi)存訪問。
通俗理解:
原子操作,,顧名思義,,就是說像原子一樣不可再細(xì)分。一個(gè)操作是原子操作,,意思就是說這個(gè)操作是以原子的方
式被執(zhí)行,,要一口氣執(zhí)行完,執(zhí)行過程不能夠被OS的其他行為打斷,,是一個(gè)整體的過程,,在
其執(zhí)行過程中,OS的其它行為是插不進(jìn)來的,。
分類:linux內(nèi)核提供了一系列函數(shù)來實(shí)現(xiàn)內(nèi)核中的原子操作,,分為整型原子操
作和位原子操作,共同點(diǎn)是:在任何情況下操作都是原子的,,內(nèi)核代碼可以安全的調(diào)用它
們而不被打斷,。
原子整數(shù)操作:
針對(duì)整數(shù)的原子操作只能對(duì)atomic_t類型的數(shù)據(jù)進(jìn)行處理,在這里之所以引入了一個(gè)特殊的數(shù)據(jù)類型,,而沒有直接使用C語言的int型,主要是出于兩個(gè)原因:
第一,、讓原子函數(shù)只接受atomic_t類型的操作數(shù),,可以確保原子操作只與這種特殊類型數(shù)據(jù)一起使用,
同時(shí),,這也確保了該類型的數(shù)據(jù)不會(huì)被傳遞給其它任何非原子函數(shù),;
第二、使用atomic_t類型確保編譯器不對(duì)相應(yīng)的值進(jìn)行訪問優(yōu)化——這點(diǎn)使得原子操作最終接收到正確的內(nèi)存地址,,而不是一個(gè)別名,,
最后就是在不同體系結(jié)構(gòu)上實(shí)現(xiàn)原子操作的時(shí)候,使用atomic_t可以屏蔽其間的差異,。
原子整數(shù)操作最常見的用途就是實(shí)現(xiàn)計(jì)數(shù)器,。
另一點(diǎn)需要說明原子操作只能保證操作是原子的,要么完成,,要么
不完成,,不會(huì)有操作一半的可能,但原子操作并不能保證操作的順序性,,即它不能保證兩個(gè)操作是按某個(gè)順序完成的,。如果要保證原子操作的順序性,請(qǐng)使用內(nèi)存屏
障指令。
atomic_t和ATOMIC_INIT(i)定義
typedef
struct { volatile int counter; } atomic_t;
#define
ATOMIC_INIT(i) { (i) }
在你編寫代碼的時(shí)候,,能使用原子操作的時(shí)候,,就盡量不要使用復(fù)雜
的加鎖機(jī)制,對(duì)多數(shù)體系結(jié)構(gòu)來講,,原子操作與更復(fù)雜的同步方法相比較,,給系統(tǒng)帶來的開銷小,對(duì)高速緩存行的影響也小,,但是,,對(duì)于那些有高性能要求的代碼,
對(duì)多種同步方法進(jìn)行測(cè)試比較,,不失為一種明智的作法,。
原子位操作:
針對(duì)位這一級(jí)數(shù)據(jù)進(jìn)行操作的函數(shù),是對(duì)普通的內(nèi)存地址進(jìn)行操作
的,。它的參數(shù)是一個(gè)指針和一個(gè)位號(hào),。
為方便其間,內(nèi)核還提供了一組與上述操作對(duì)應(yīng)的非原子位函數(shù),,
非原子位函數(shù)與原子位函數(shù)的操作完全相同,,但是,前者不保證原子性,,且其名字前綴多兩個(gè)下劃線,。例如,與test_bit()對(duì)應(yīng)的非原子形式是_test_bit(),,如果你不需要原子性操作(比如,,如果你已經(jīng)用鎖保護(hù)了自己的
數(shù)據(jù)),那么這些非原子的位函數(shù)相比原子的位函數(shù)可能會(huì)執(zhí)行得更快些,。
四,、自旋鎖
自旋鎖的引入:
如果每個(gè)臨界區(qū)都能像增加變量這樣簡(jiǎn)單就好了,可惜現(xiàn)實(shí)不是這
樣,,而是臨界區(qū)可以跨越多個(gè)函數(shù),,例如:先得從一個(gè)數(shù)據(jù)結(jié)果中移出數(shù)據(jù),對(duì)其進(jìn)行格式轉(zhuǎn)換和解析,,最后再把它加入到另一個(gè)數(shù)據(jù)結(jié)構(gòu)中,,整個(gè)執(zhí)行過程必須是
原子的,在數(shù)據(jù)被更新完畢之前,,不能有其他代碼讀取這些數(shù)據(jù),,顯然,簡(jiǎn)單的原子操作是無能為力的(在
單處理器系統(tǒng)(UniProcessor)中,,能夠在單條指令中完成的操作都可
以認(rèn)為是" 原子操作",,因?yàn)橹袛嘀荒馨l(fā)生于指令之間),,這就需要使用更為復(fù)雜的同步方法——鎖來提供保護(hù)。
自旋鎖的介紹:
Linux內(nèi)核中最常見的鎖是自旋鎖(spin
lock),,自旋鎖最多只能被一個(gè)可執(zhí)行線程持有,,如果一個(gè)執(zhí)行線程試圖獲得一個(gè)被爭(zhēng)用(已經(jīng)被持有)的自
旋鎖,那么該線程就會(huì)一直進(jìn)行忙循環(huán)—旋轉(zhuǎn)—等待鎖重新可用,,要是鎖未被爭(zhēng)用,,請(qǐng)求鎖的執(zhí)行線程便能立刻得到它,繼續(xù)執(zhí)行,,在任意時(shí)間,,自旋鎖都可以防止多于一
個(gè)的執(zhí)行線程同時(shí)進(jìn)入理解區(qū),注意同一個(gè)鎖可以用在多個(gè)位置—例如,,對(duì)于給定數(shù)據(jù)的所有訪問都可以得到保護(hù)和同步,。
一個(gè)被爭(zhēng)用的自旋鎖使得請(qǐng)求它的線程在等待鎖重新可用時(shí)自旋
(特別浪費(fèi)處理器時(shí)間),所以自旋鎖不應(yīng)該被長(zhǎng)時(shí)間持有,,事實(shí)上,,這點(diǎn)正是使用自旋鎖的初衷,在短期間內(nèi)進(jìn)行輕量級(jí)加鎖,,還可以采取另外的方式來處理對(duì)鎖
的爭(zhēng)用:讓請(qǐng)求線程睡眠,,直到鎖重新可用時(shí)再喚醒它,這樣處理器就不必循環(huán)等待,,可以去執(zhí)行其他代碼,,這也會(huì)帶來一定的開銷——這里有兩次明顯的上下文切換,被阻塞的線程要換出和換入,。因此,,持有自旋鎖的時(shí)間最好小于
完成兩次上下文切換的耗時(shí),當(dāng)然我們大多數(shù)人不會(huì)無聊到去測(cè)量上下文切換的耗時(shí),,所以我們讓持有自旋鎖的時(shí)間應(yīng)盡可能的短就可以了,信號(hào)量可以提供上述第
二種機(jī)制,,它使得在發(fā)生爭(zhēng)用時(shí),,等待的線程能投入睡眠,而不是旋轉(zhuǎn),。
自旋鎖可以使用在中斷處理程序中(此處不能使用信號(hào)量,,因?yàn)樗鼈儠?huì)導(dǎo)致睡眠),在中斷處理程序中
使用自旋鎖時(shí),,一定要在獲取鎖之前,,首先禁止本地中斷(在當(dāng)前處理器上的中斷請(qǐng)
求),否則,,中斷處理程序就會(huì)打斷正持有鎖的內(nèi)核代碼,,有可能會(huì)試圖去爭(zhēng)用這個(gè)已經(jīng)持有的自旋鎖,,這樣以來,中斷處理程序就會(huì)自旋,,等待該鎖重新可用,,但
是鎖的持有者在這個(gè)中斷處理程序執(zhí)行完畢前不可能運(yùn)行,這正是我們?cè)谇耙徽鹿?jié)中提到的雙重請(qǐng)求死鎖,,注意,,需要關(guān)閉的只是當(dāng)前處理器上的中斷,如果中斷發(fā)生在不同的處理器上,,即使中斷處理程序在同一鎖上自旋,,也不會(huì)妨礙鎖的持有者(在不同處理
器上)最終釋放鎖。
自旋鎖的簡(jiǎn)單理解:
理解自旋鎖最簡(jiǎn)單的方法是把它作為一個(gè)變量看待,,該變量把一個(gè)
臨界區(qū)或者標(biāo)記為“我當(dāng)前正在運(yùn)行,,請(qǐng)稍等一會(huì)”或者標(biāo)記為“我當(dāng)前不在運(yùn)行,可以被使用”,。如果A執(zhí)行單元首先進(jìn)入例程,,它將持有自旋鎖,當(dāng)B執(zhí)行單元試圖進(jìn)入同一個(gè)例程時(shí),,將獲知自旋鎖已被持有,,需等到A執(zhí)行單元釋放后才能進(jìn)入。
自旋鎖的API函數(shù):
其
實(shí)介紹的幾種信號(hào)量和互斥機(jī)制,,其底層源碼都是使用自旋鎖,可以理解為自旋鎖的再包裝,。所以從這里就可以理解為什么自旋鎖通常可以提供比信號(hào)量更高的
性能,。
自旋鎖是一個(gè)互斥設(shè)備,,他只能會(huì)兩個(gè)值:“鎖定”和“解鎖”。它通常實(shí)現(xiàn)為某個(gè)整數(shù)之中的單個(gè)位,。
“測(cè)試并設(shè)置”的操作必須以原子方式完成,。
任何時(shí)候,只要內(nèi)核代碼擁有自旋鎖,,在相關(guān)CPU上的搶占就會(huì)被禁止,。
適用于自旋鎖的核心規(guī)則:
(1)任何擁有自旋鎖的代碼都必須使原子
的,除服務(wù)中斷外(某些情況下也不能放棄CPU,如中斷服務(wù)也要獲得自旋鎖,。為了避免
這種鎖陷阱,,需要在擁有自旋鎖時(shí)禁止中斷),不能放棄CPU(如休眠,,休眠可發(fā)生在許多無法預(yù)期
的地方),。否則CPU將有可能永遠(yuǎn)自旋下去(死機(jī))。
(2)擁有自旋鎖的時(shí)間越短越好,。
需要強(qiáng)調(diào)的是,,自旋鎖別設(shè)計(jì)用于多處理器的同步機(jī)制,,對(duì)于單處理
器(對(duì)于單處理器并且不可搶占的內(nèi)核來說,自旋鎖什么也不作),,內(nèi)核在編譯時(shí)不會(huì)引入自旋鎖機(jī)制,,對(duì)于可搶占的內(nèi)核,它僅僅被用于設(shè)置內(nèi)核的搶占機(jī)制是否
開啟的一個(gè)開關(guān),,也就是說加鎖和解鎖實(shí)際變成了禁止或開啟內(nèi)核搶占功能,。如果內(nèi)核不支持搶占,那么自旋鎖根本就不會(huì)編譯到內(nèi)核中,。
內(nèi)核中使用spinlock_t類型來表示自旋鎖,,它定義在<linux/spinlock_types.h>:
typedef
struct { raw_spinlock_t raw_lock; #if defined(CONFIG_PREEMPT)
&& defined(CONFIG_SMP) unsigned int break_lock; #endif }
spinlock_t;
|
對(duì)于不支持SMP的內(nèi)核來說,struct raw_spinlock_t什么也沒有,,
是一個(gè)空結(jié)構(gòu),。對(duì)于支持多處理器的內(nèi)核來說,struct
raw_spinlock_t定義為
typedef
struct { unsigned int slock; } raw_spinlock_t;
|
slock表示了自旋鎖的狀態(tài),,“1”表示自旋鎖
處于解鎖狀態(tài)(UNLOCK),,“0”表示自旋鎖
處于上鎖狀態(tài)(LOCKED)。
break_lock表示當(dāng)前是否由進(jìn)程在等待自旋鎖,,顯然,,它只有在支
持搶占的SMP內(nèi)核上才起作
用。
自旋鎖的實(shí)
現(xiàn)是一個(gè)復(fù)雜的過程,,說它復(fù)雜不是因?yàn)樾枰嗌俅a或邏輯來實(shí)現(xiàn)它,,其實(shí)它的實(shí)現(xiàn)代碼很少。自旋鎖的實(shí)現(xiàn)跟體系結(jié)構(gòu)關(guān)系密切,,核心代碼基本也是由匯編語言
寫成,,與體協(xié)結(jié)構(gòu)相關(guān)的核心代碼都放在相關(guān)的<asm/>目錄下,比
如<asm/spinlock.h>,。對(duì)于我們
驅(qū)動(dòng)程序開發(fā)人員來說,,我們沒有必要了解這么spinlock的內(nèi)部細(xì)
節(jié),如果你對(duì)它感興趣,,請(qǐng)參考閱讀Linux內(nèi)核源代
碼,。對(duì)于我們驅(qū)動(dòng)的spinlock接口,我們只
需包括<linux/spinlock.h>頭文件,。在
我們?cè)敿?xì)的介紹spinlock的API之前,我們先來看看自旋鎖的一個(gè)基本使用格式:
#include
<linux/spinlock.h> spinlock_t lock = SPIN_LOCK_UNLOCKED;
spin_lock(&lock); .... spin_unlock(&lock);
|
從使用上來說,,spinlock的API還很簡(jiǎn)單
的,,一般我們會(huì)用的的API如下表,其實(shí)
它們都是定義在<linux/spinlock.h>中的宏接
口,,真正的實(shí)現(xiàn)在<asm/spinlock.h>中
#include
<linux/spinlock.h> SPIN_LOCK_UNLOCKED DEFINE_SPINLOCK spin_lock_init(
spinlock_t *) spin_lock(spinlock_t *) spin_unlock(spinlock_t *) spin_lock_irq(spinlock_t
*) spin_unlock_irq(spinlock_t *) spin_lock_irqsace(spinlock_t *,,unsigned long flags) spin_unlock_irqsace(spinlock_t
*, unsigned long flags) spin_trylock(spinlock_t *) spin_is_locked(spinlock_t
*)
|
·
初始化
spinlock有兩種初始
化形式,,一種是靜態(tài)初始化,一種是動(dòng)態(tài)初始化,。對(duì)于靜態(tài)的spinlock對(duì)象,,我們
用 SPIN_LOCK_UNLOCKED來初始化,
它是一個(gè)宏,。當(dāng)然,,我們也可以把聲明spinlock和初始化它
放在一起做,這就是 DEFINE_SPINLOCK宏的工作,,
因此,,下面的兩行代碼是等價(jià)的。
DEFINE_SPINLOCK
(lock); spinlock_t lock = SPIN_LOCK_UNLOCKED;
|
spin_lock_init 函數(shù)一般用
來初始化動(dòng)態(tài)創(chuàng)建的spinlock_t對(duì)象,,它的參
數(shù)是一個(gè)指向spinlock_t對(duì)象的指針,。
當(dāng)然,它也可以初始化一個(gè)靜態(tài)的沒有初始化的spinlock_t對(duì)象,。
spinlock_t
*lock ...... spin_lock_init(lock);
|
·
獲取鎖
內(nèi)核提供了三個(gè)函數(shù)用于獲取一個(gè)自旋鎖,。
spin_lock:獲取指定的自旋鎖。
spin_lock_irq:禁止本地中
斷并獲取自旋鎖,。
spin_lock_irqsace:保存本地
中斷狀態(tài),,禁止本地中斷并獲取自旋鎖,返回本地中斷狀態(tài),。
自旋鎖是可以使用在中斷處理程序中的,,這時(shí)需要使用
具有關(guān)閉本地中斷功能的函數(shù),我們推薦使用 spin_lock_irqsave,,因?yàn)樗鼤?huì)
保存加鎖前的中斷標(biāo)志,,這樣就會(huì)正確恢復(fù)解鎖時(shí)的中斷標(biāo)志。如果spin_lock_irq在加鎖時(shí)中
斷是關(guān)閉的,,那么在解鎖時(shí)就會(huì)錯(cuò)誤的開啟中斷,。
另外兩個(gè)同自旋鎖獲取相關(guān)的函數(shù)是:
spin_trylock():嘗試獲取自
旋鎖,如果獲取失敗則立即返回非0值,,否則返回0,。
spin_is_locked():判斷指定的
自旋鎖是否已經(jīng)被獲取了。如果是則返回非0,,否則,,返
回0。
·
釋放鎖
同獲取鎖相對(duì)應(yīng),,內(nèi)核提供了三個(gè)相對(duì)的函數(shù)來釋放自旋鎖,。
spin_unlock:釋放指定的自旋鎖。
spin_unlock_irq:釋放自旋鎖
并激活本地中斷,。
spin_unlock_irqsave:釋放自旋
鎖,,并恢復(fù)保存的本地中斷狀態(tài),。
十二、Linux驅(qū)動(dòng)程序開發(fā)(6) - Linux內(nèi)核同步介紹和方法(2)
五,、讀寫自旋鎖
如果臨界區(qū)保
護(hù)的數(shù)據(jù)是可讀可寫的,,那么只要沒有寫操作,對(duì)于讀是可以支持并發(fā)操作的,。對(duì)于這種只要求寫操作是互斥的需求,,如果還是使用自旋鎖顯然是無法滿足這個(gè)要求
(對(duì)于讀操作實(shí)在是太浪費(fèi)了)。為此內(nèi)核提供了另一種鎖-讀寫自旋鎖,,讀自旋鎖也叫共享自旋鎖,,寫自旋鎖也叫排他自旋鎖。
讀寫自旋鎖是一種比自旋鎖粒度更小的鎖
機(jī)制,,它保留了“自旋”的概念,,但是在寫操作方面,只能最多有一個(gè)寫進(jìn)程,,在讀操作方面,,同時(shí)可以有多個(gè)讀執(zhí)行單元,當(dāng)然,,讀和寫也不能同時(shí)進(jìn)行,。
讀寫自旋鎖的
使用也普通自旋鎖的使用很類似,首先要初始化讀寫自旋鎖對(duì)象:
// 靜態(tài)初始化 rwlock_t rwlock = RW_LOCK_UNLOCKED; //動(dòng)態(tài)初始化 rwlock_t *rwlock; ... rw_lock_init(rwlock);
|
在讀操作代碼里對(duì)共享數(shù)據(jù)獲取讀自旋鎖:
read_lock(&rwlock); ... read_unlock(&rwlock);
|
在寫操作代碼里為共享數(shù)據(jù)獲取寫自旋鎖:
write_lock(&rwlock); ... write_unlock(&rwlock);
|
需要注意的是,,如果有大量的寫操作,,會(huì)使寫操作自旋在寫自旋鎖上而處于寫?zhàn)囸I狀
態(tài)(等待讀自旋鎖的全部釋放),因?yàn)樽x自旋鎖會(huì)自由的獲取讀自旋鎖,。
讀寫自旋鎖的
函數(shù)類似于普通自旋鎖,,這里就不一一介紹了,我們把它列在下面的表中,。
RW_LOCK_UNLOCKED rw_lock_init(rwlock_t
*) read_lock(rwlock_t *) read_unlock(rwlock_t *) read_lock_irq(rwlock_t
*) read_unlock_irq(rwlock_t *) read_lock_irqsave(rwlock_t *,
unsigned long) read_unlock_irqsave(rwlock_t *, unsigned long) write_lock(rwlock_t
*) write_unlock(rwlock_t *) write_lock_irq(rwlock_t *) write_unlock_irq(rwlock_t
*) write_lock_irqsave(rwlock_t *, unsigned long) write_unlock_irqsave(rwlock_t
*, unsigned long) rw_is_locked(rwlock_t *)
|
六,、順序瑣
順序瑣(seqlock)是對(duì)讀寫
鎖的一種優(yōu)化,若使用順序瑣,,讀執(zhí)行單元絕不會(huì)被寫執(zhí)行單元阻塞,,也就是說,讀執(zhí)行單元可以在寫執(zhí)行單元對(duì)被順序
瑣保護(hù)的共享資源進(jìn)行寫操作時(shí)仍然可以繼續(xù)讀,,而不必等待寫執(zhí)行單元完成寫操作,,寫執(zhí)行單元也不需要等待所有讀執(zhí)行單元完成讀操作才去進(jìn)行寫操作。
但是,,寫執(zhí)行單元與寫執(zhí)行單元之間仍然是互斥的,,即如果有寫執(zhí)行單元在進(jìn)行寫操
作,其它寫執(zhí)行單元必須自旋在哪里,直到寫執(zhí)行單元釋放了順序瑣,。
如果讀執(zhí)行單元在讀操作期間,寫執(zhí)行單元已經(jīng)發(fā)生了寫操作,,那么,,讀執(zhí)行單
元必須重新讀取數(shù)據(jù),以便確保得到的數(shù)據(jù)是完整的,,這種鎖在讀寫同時(shí)進(jìn)行的概率比較小時(shí),,性能是非常好的,而且它允許讀寫同時(shí)進(jìn)
行,,因而更大的提高了并發(fā)性,,
注意,順序瑣由一個(gè)限制,,就是它必須被保護(hù)的共享資源不含有指針,,因?yàn)閷憟?zhí)行單
元可能使得指針失效,但讀執(zhí)行單元如果正要訪問該指針,,將導(dǎo)致Oops,。
七、信號(hào)量
Linux中
的信號(hào)量是一種睡眠鎖,,如果有一個(gè)任務(wù)試圖獲得一個(gè)已經(jīng)被占用的信號(hào)量時(shí),,信號(hào)量會(huì)將其推進(jìn)一個(gè)等待隊(duì)列,然后讓其睡眠,,這時(shí)處理器能重獲自由,,從而去執(zhí)
行其它代碼,當(dāng)持有信號(hào)量的進(jìn)程將信號(hào)量釋放后,,處于等待隊(duì)列中的哪個(gè)任務(wù)被喚醒,,并獲得該信號(hào)量。
信號(hào)量,,或旗標(biāo),,就是我們?cè)诓僮飨到y(tǒng)里學(xué)習(xí)的經(jīng)典的P/V原語操作。
P:如果信號(hào)量值大于0,,則遞減信
號(hào)量的值,,程序繼續(xù)執(zhí)行,否則,,睡眠等待信號(hào)量大于0,。
V:遞增信號(hào)量的值,如果遞增的信號(hào)量的值大于0,,則喚醒等待
的進(jìn)程,。
信號(hào)量的值確定了同時(shí)可以有多少個(gè)進(jìn)程可以同時(shí)進(jìn)入臨界區(qū),如果信號(hào)量的初
始值始1,這信號(hào)量就是互斥信號(hào)量(MUTEX),。對(duì)于大于1的非0值信號(hào)量,,也可稱為計(jì)數(shù)信號(hào)量(counting
semaphore)。對(duì)于一般的驅(qū)動(dòng)程序使用的信號(hào)量都是互斥信號(hào)量,。
類似于自旋鎖,,信號(hào)量的實(shí)現(xiàn)也與體系結(jié)構(gòu)密切相關(guān),具體的實(shí)現(xiàn)定義在<asm/semaphore.h>頭文件中,,對(duì)
于x86_32系統(tǒng)來說,,它
的定義如下:
struct
semaphore { atomic_t count; int sleepers; wait_queue_head_t wait; };
|
信號(hào)量的初始值count是atomic_t類型的,這
是一個(gè)原子操作類型,,它也是一個(gè)內(nèi)核同步技術(shù),,可見信號(hào)量是基于原子操作的。我們會(huì)在后面原子操作部分對(duì)原子操作做詳細(xì)介紹,。
信號(hào)量的使
用類似于自旋鎖,,包括創(chuàng)建、獲取和釋放,。我們還是來先展示信號(hào)量的基本使用形式:
static
DECLARE_MUTEX(my_sem); ......
if
(down_interruptible(&my_sem))
{ return -ERESTARTSYS; } ...... up(&my_sem)
|
Linux內(nèi)核中的信
號(hào)量函數(shù)接口如下:
static
DECLARE_SEMAPHORE_GENERIC(name, count); static DECLARE_MUTEX(name); seam_init(struct
semaphore *, int); init_MUTEX(struct semaphore *); init_MUTEX_LOCKED(struct
semaphore *) down_interruptible(struct semaphore *); down(struct
semaphore *) down_trylock(struct semaphore *) up(struct semaphore
*)
|
·
初始化信號(hào)量
信號(hào)量的初始化包括靜態(tài)初始化和動(dòng)態(tài)初始化,。靜態(tài)初始化用于靜態(tài)的聲明并初始化
信號(hào)量。
static
DECLARE_SEMAPHORE_GENERIC(name, count); static DECLARE_MUTEX(name);
|
對(duì)于動(dòng)態(tài)聲明或創(chuàng)建的信號(hào)量,,可以使用如下函數(shù)進(jìn)行初始化:
seam_init(sem,
count); init_MUTEX(sem); init_MUTEX_LOCKED(struct semaphore *)
|
顯然,,帶有MUTEX的函數(shù)始初
始化互斥信號(hào)量。LOCKED則初始化信號(hào)
量為鎖狀態(tài),。
·
使用信號(hào)量
信號(hào)量初始化完成后我們就可以使用它了
down_interruptible(struct
semaphore *); down(struct semaphore *) down_trylock(struct
semaphore *) up(struct semaphore *)
|
down函數(shù)會(huì)嘗試
獲取指定的信號(hào)量,,如果信號(hào)量已經(jīng)被使用了,則進(jìn)程進(jìn)入不可中斷的睡眠狀態(tài),。down_interruptible則會(huì)使進(jìn)程
進(jìn)入可中斷的睡眠狀態(tài),。關(guān)于進(jìn)程狀態(tài)的詳細(xì)細(xì)節(jié),我們?cè)趦?nèi)核的進(jìn)程管理里在做詳細(xì)介紹,。
down_trylock嘗試獲取信號(hào)量,, 如果獲取成功則返回0,失敗則會(huì)
立即返回非0,。
當(dāng)退出臨界
區(qū)時(shí)使用up函數(shù)釋放信號(hào)
量,,如果信號(hào)量上的睡眠隊(duì)列不為空,則喚醒其中一個(gè)等待進(jìn)程,。
八,、讀寫信號(hào)量
類似于自旋
鎖,信號(hào)量也有讀寫信號(hào)量,。讀寫信號(hào)量API定義在<linux/rwsem.h>頭文件中,,它
的定義其實(shí)也是體系結(jié)構(gòu)相關(guān)的,,因此具體實(shí)現(xiàn)定義在<asm/rwsem.h>頭文件中,
以下是x86的例子:
struct
rw_semaphore { signed long count; spinlock_t wait_lock; struct list_head wait_list; };
|
首先要說明的是所有的讀寫信號(hào)量都是
互斥信號(hào)量,。讀鎖是共享鎖,,就是同時(shí)允許多個(gè)讀進(jìn)程持有該信號(hào)量,但寫鎖是獨(dú)占鎖,,同時(shí)只能有一個(gè)寫鎖持有該互斥信號(hào)量,。顯然,
寫鎖是排他的,,包括排斥讀鎖。由于寫鎖是共享鎖,,它允許多個(gè)讀進(jìn)程持有該鎖,,只要沒有進(jìn)程持有寫鎖,它就始終會(huì)成功持有該鎖,,因此這會(huì)造成寫進(jìn)程寫?zhàn)囸I狀
態(tài),。
在使用讀寫信號(hào)量前先要初始化,就像你所想到的,,它在使用上幾乎與讀寫自旋鎖一致,。先來看看讀
寫信號(hào)量的創(chuàng)建和初始化:
// 靜態(tài)初始化 static DECLARE_RWSEM(rwsem_name);
// 動(dòng)態(tài)初始化 static struct rw_semaphore rw_sem; init_rwsem(&rw_sem);
|
讀進(jìn)程獲取信號(hào)量保護(hù)臨界區(qū)數(shù)據(jù):
down_read(&rw_sem); ... up_read(&rw_sem);
|
寫進(jìn)程獲取信號(hào)量保護(hù)臨界區(qū)數(shù)據(jù):
down_write(&rw_sem); ... up_write(&rw_sem);
|
更多的讀寫信號(hào)量API請(qǐng)參考下表:
#include
<linux/rwsem.h>
DECLARE_RWSET(name); init_rwsem(struct rw_semaphore *); void
down_read(struct rw_semaphore *sem); void down_write(struct
rw_semaphore *sem); void up_read(struct rw_semaphore *sem); int
down_read_trylock(struct rw_semaphore *sem); int
down_write_trylock(struct rw_semaphore *sem); void
downgrade_write(struct rw_semaphore *sem); void up_write(struct
rw_semaphore *sem);
|
同自旋鎖一樣,,down_read_trylock和down_write_trylock會(huì)嘗試著獲取
信號(hào)量,,如果獲取成功則返回1,否則返回0,。奇怪為什么返回值與信號(hào)量的對(duì)應(yīng)函數(shù)相反,,使用是一定要小心這
點(diǎn)。
九,、自旋鎖和信號(hào)量區(qū)別
在驅(qū)動(dòng)程序中,,當(dāng)多個(gè)線程同時(shí)訪問相同的資源時(shí)(驅(qū)動(dòng)程序中的全局變量是一種典
型的共享資源),可能會(huì)引發(fā)"競(jìng)態(tài)",,因此我們必須對(duì)共享資源進(jìn)行并發(fā)控制,。Linux內(nèi)核中解決并發(fā)控制的最常用方法是自旋鎖與信號(hào)量(絕大多數(shù)時(shí)候
作為互斥鎖使用)。
自旋鎖與信號(hào)量"類似而不類",,類似說的是它們功能上的相似性,,"不類"指代它們?cè)诒举|(zhì)和實(shí)現(xiàn)機(jī)理上完全不一樣,不屬于一類,。
自旋鎖
不會(huì)引起調(diào)用者睡眠,,如果自旋鎖已經(jīng)被別的執(zhí)行單元保持,調(diào)用者就一直循環(huán)查看是否該自旋鎖的保持者已經(jīng)釋放了鎖,,"自旋"就是"在原地打轉(zhuǎn)",。而信號(hào)量則引起調(diào)用者睡眠,,它把進(jìn)程從運(yùn)行隊(duì)列上拖出去,除非
獲得鎖,。這就是它們的"不類",。
但是,
無論是信號(hào)量,,還是自旋鎖,,在任何時(shí)刻,最多只能有一個(gè)保持者,,即在任何時(shí)刻最多只能有一個(gè)執(zhí)行單元獲得鎖,。這就是它們的"類似"。
鑒于自
旋鎖與信號(hào)量的上述特點(diǎn),,一般而言,,自旋鎖適合于保持時(shí)間非常短的情況,它可以在任何上下文使用,;信號(hào)量適合于保持時(shí)間較長(zhǎng)的情況,,會(huì)只能在進(jìn)程上下文使
用。如果被保護(hù)的共享資源只在進(jìn)程上下文訪問,,則可以以信號(hào)量來保護(hù)該共享資源,,如果對(duì)共享資源的訪問時(shí)間非常短,自旋鎖也是好的選擇,。但是,,如果被保護(hù)
的共享資源需要在中斷上下文訪問(包括底半部即中斷處理句柄和頂半部即軟中斷),就必須使用自旋鎖,。
區(qū)別總結(jié)如下:
1,、由于爭(zhēng)用信號(hào)量的進(jìn)程在等待鎖重新變?yōu)榭捎脮r(shí)會(huì)睡眠,所以信號(hào)量適用于鎖會(huì)被長(zhǎng)時(shí)間持有的情況,。
2,、相反,鎖被短時(shí)間持有時(shí),,使用信號(hào)量就不太適宜了,,因?yàn)樗咭鸬暮臅r(shí)可能比鎖被占用的全部時(shí)間還要長(zhǎng)。
3,、由于執(zhí)行線程在鎖被爭(zhēng)用時(shí)會(huì)睡眠,,所以只能在進(jìn)程上下文中才能獲取信號(hào)量鎖,因?yàn)樵谥袛嗌舷挛闹校ㄊ褂米孕i)是
不能進(jìn)行調(diào)度的,。
4,、你可以在持有信號(hào)量時(shí)去睡眠(當(dāng)然你也可能并不需要睡眠),因?yàn)楫?dāng)其它進(jìn)程試圖獲得同一信號(hào)量時(shí)不會(huì)因此而死鎖,,
(因?yàn)樵撨M(jìn)程也只是去睡眠而已,,而你最終會(huì)繼續(xù)執(zhí)行的),。
5、在你占用信號(hào)量的同時(shí)不能占用自旋鎖,,因?yàn)樵谀愕却盘?hào)量時(shí)可能會(huì)睡眠,,而在持有自旋鎖時(shí)是不允許睡眠的。
6,、信號(hào)量鎖保護(hù)的臨界區(qū)可包含可能引起阻塞的代碼,,而自旋鎖則絕對(duì)要避免用來保護(hù)包含這樣代碼的臨界區(qū),因?yàn)樽枞?
味著要進(jìn)行進(jìn)程的切換,,如果進(jìn)程被切換出去后,,另一進(jìn)程企圖獲取本自旋鎖,死鎖就會(huì)發(fā)生,。
7,、信號(hào)量不同于自旋鎖,它不會(huì)禁止內(nèi)核搶占(自旋鎖被持有時(shí),,內(nèi)核不能被搶占),所以持有信號(hào)量的代碼可以被搶占,,
這意味著信號(hào)量不會(huì)對(duì)調(diào)度的等待時(shí)間帶來負(fù)面影響,。
除了以上介紹的同步機(jī)制方法以外,還有BKL(大內(nèi)核
鎖),,Seq鎖等,。
BKL是一個(gè)全局自
旋鎖,使用它主要是為了方便實(shí)現(xiàn)從Linux最初的SMP過度到細(xì)粒度加鎖機(jī)制,。
Seq鎖用于讀寫共享數(shù)據(jù),,實(shí)現(xiàn)這樣鎖只要依靠一個(gè)序列計(jì)數(shù)器