5.3. 旗標(biāo)和互斥體

2018-02-24 15:49 更新

5.3.?旗標(biāo)和互斥體

讓我們看看我們?nèi)绾谓o scull 加鎖. 我們的目標(biāo)是使我們對(duì) scull 數(shù)據(jù)結(jié)構(gòu)的操作原子化, 就是在有其他執(zhí)行線程的情況下這個(gè)操作一次發(fā)生. 對(duì)于我們的內(nèi)存泄漏例子, 我們需要保證, 如果一個(gè)線程發(fā)現(xiàn)必須分配一個(gè)特殊的內(nèi)存塊, 它有機(jī)會(huì)進(jìn)行這個(gè)分配在其他線程可做測(cè)試之前. 為此, 我們必須建立臨界區(qū): 在任何給定時(shí)間只有一個(gè)線程可以執(zhí)行的代碼.

不是所有的臨界區(qū)是同樣的, 因此內(nèi)核提供了不同的原語(yǔ)適用不同的需求. 在這個(gè)例子中, 每個(gè)對(duì) scull 數(shù)據(jù)結(jié)構(gòu)的存取都發(fā)生在由一個(gè)直接用戶請(qǐng)求所產(chǎn)生的進(jìn)程上下文中; 沒(méi)有從中斷處理或者其他異步上下文中的存取. 沒(méi)有特別的周期(響應(yīng)時(shí)間)要求; 應(yīng)用程序程序員理解 I/O 請(qǐng)求常常不是馬上就滿足的. 進(jìn)一步講, scull 沒(méi)有持有任何其他關(guān)鍵系統(tǒng)資源, 在它存取它自己的數(shù)據(jù)結(jié)構(gòu)時(shí). 所有這些意味著如果 scull 驅(qū)動(dòng)在等待輪到它存取數(shù)據(jù)結(jié)構(gòu)時(shí)進(jìn)入睡眠, 沒(méi)人介意.

"去睡眠" 在這個(gè)上下文中是一個(gè)明確定義的術(shù)語(yǔ). 當(dāng)一個(gè) Linux 進(jìn)程到了一個(gè)它無(wú)法做進(jìn)一步處理的地方時(shí), 它去睡眠(或者 "阻塞"), 讓出處理器給別人直到以后某個(gè)時(shí)間它能夠再做事情. 進(jìn)程常常在等待 I/O 完成時(shí)睡眠. 隨著我們深入內(nèi)核, 我們會(huì)遇到很多情況我們不能睡眠. 然而 scull 中的 write 方法不是其中一個(gè)情況. 因此我們可使用一個(gè)加鎖機(jī)制使進(jìn)程在等待存取臨界區(qū)時(shí)睡眠.

正如重要地, 我們將進(jìn)行一個(gè)可能會(huì)睡眠的操作( 使用 kmalloc 分配內(nèi)存 ) -- 因此睡眠是一個(gè)在任何情況下的可能性. 如果我們的臨界區(qū)要正確工作, 我們必須使用一個(gè)加鎖原語(yǔ)在一個(gè)擁有鎖的進(jìn)程睡眠時(shí)起作用. 不是所有的加鎖機(jī)制都能夠在可能睡眠的地方使用( 我們?cè)诒菊潞竺鏁?huì)看到幾個(gè)不可以的 ). 然而, 對(duì)我們現(xiàn)在的需要, 最適合的機(jī)制時(shí)一個(gè)旗標(biāo).

旗標(biāo)在計(jì)算機(jī)科學(xué)中是一個(gè)被很好理解的概念. 在它的核心, 一個(gè)旗標(biāo)是一個(gè)單個(gè)整型值, 結(jié)合有一對(duì)函數(shù), 典型地稱為 P 和 V. 一個(gè)想進(jìn)入臨界區(qū)的進(jìn)程將在相關(guān)旗標(biāo)上調(diào)用 P; 如果旗標(biāo)的值大于零, 這個(gè)值遞減 1 并且進(jìn)程繼續(xù). 相反, 如果旗標(biāo)的值是 0 ( 或更小 ), 進(jìn)程必須等待直到別人釋放旗標(biāo). 解鎖一個(gè)旗標(biāo)通過(guò)調(diào)用 V 完成; 這個(gè)函數(shù)遞增旗標(biāo)的值, 并且, 如果需要, 喚醒等待的進(jìn)程.

當(dāng)旗標(biāo)用作互斥 -- 阻止多個(gè)進(jìn)程同時(shí)在同一個(gè)臨界區(qū)內(nèi)運(yùn)行 -- 它們的值將初始化為 1. 這樣的旗標(biāo)在任何給定時(shí)間只能由一個(gè)單個(gè)進(jìn)程或者線程持有. 以這種模式使用的旗標(biāo)有時(shí)稱為一個(gè)互斥鎖, 就是, 當(dāng)然, "互斥"的縮寫. 幾乎所有在 Linux 內(nèi)核中發(fā)現(xiàn)的旗標(biāo)都是用作互斥.

5.3.1.?Linux 旗標(biāo)實(shí)現(xiàn)

Linux 內(nèi)核提供了一個(gè)遵守上面語(yǔ)義的旗標(biāo)實(shí)現(xiàn), 盡管術(shù)語(yǔ)有些不同. 為使用旗標(biāo), 內(nèi)核代碼必須包含 <asm/semaphore.h>. 相關(guān)的類型是 struct semaphore; 實(shí)際旗標(biāo)可以用幾種方法來(lái)聲明和初始化. 一種是直接創(chuàng)建一個(gè)旗標(biāo), 接著使用 sema_init 來(lái)設(shè)定它:


void sema_init(struct semaphore *sem, int val);

這里 val 是安排給旗標(biāo)的初始值.

然而, 通常旗標(biāo)以互斥鎖的模式使用. 為使這個(gè)通用的例子更容易些, 內(nèi)核提供了一套幫助函數(shù)和宏定義. 因此, 一個(gè)互斥鎖可以聲明和初始化, 使用下面的一種:


DECLARE_MUTEX(name); 
DECLARE_MUTEX_LOCKED(name);

這里, 結(jié)果是一個(gè)旗標(biāo)變量( 稱為 name ), 初始化為 1 ( 使用 DECLARE_MUTEX ) 或者 0 (使用 DECLARE_MUTEX_LOCKED ). 在后一種情況, 互斥鎖開始于上鎖的狀態(tài); 在允許任何線程存取之前將不得不顯式解鎖它.

如果互斥鎖必須在運(yùn)行時(shí)間初始化( 這是如果動(dòng)態(tài)分配它的情況, 舉例來(lái)說(shuō)), 使用下列中的一個(gè):


void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);

在 Linux 世界中, P 函數(shù)稱為 down -- 或者這個(gè)名子的某個(gè)變體. 這里, "down" 指的是這樣的事實(shí), 這個(gè)函數(shù)遞減旗標(biāo)的值, 并且, 也許在使調(diào)用者睡眠一會(huì)兒來(lái)等待旗標(biāo)變可用之后, 給予對(duì)被保護(hù)資源的存取. 有 3 個(gè)版本的 down:


void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);

down 遞減旗標(biāo)值并且等待需要的時(shí)間. down_interruptible 同樣, 但是操作是可中斷的. 這個(gè)可中斷的版本幾乎一直是你要的那個(gè); 它允許一個(gè)在等待一個(gè)旗標(biāo)的用戶空間進(jìn)程被用戶中斷. 作為一個(gè)通用的規(guī)則, 你不想使用不可中斷的操作, 除非實(shí)在是沒(méi)有選擇. 不可中斷操作是一個(gè)創(chuàng)建不可殺死的進(jìn)程( 在 ps 中見(jiàn)到的可怕的 "D 狀態(tài)" )和惹惱你的用戶的好方法, 使用 down_interruptible 需要一些格外的小心, 但是, 如果操作是可中斷的, 函數(shù)返回一個(gè)非零值, 并且調(diào)用者不持有旗標(biāo). 正確的使用 down_interruptible 需要一直檢查返回值并且針對(duì)性地響應(yīng).

最后的版本 ( down_trylock ) 從不睡眠; 如果旗標(biāo)在調(diào)用時(shí)不可用, down_trylock 立刻返回一個(gè)非零值.

一旦一個(gè)線程已經(jīng)成功調(diào)用 down 各個(gè)版本中的一個(gè), 就說(shuō)它持有著旗標(biāo)(或者已經(jīng)"取得"或者"獲得"旗標(biāo)). 這個(gè)線程現(xiàn)在有權(quán)力存取這個(gè)旗標(biāo)保護(hù)的臨界區(qū). 當(dāng)這個(gè)需要互斥的操作完成時(shí), 旗標(biāo)必須被返回. V 的 Linux 對(duì)應(yīng)物是 up:


void up(struct semaphore *sem); 

一旦 up 被調(diào)用, 調(diào)用者就不再擁有旗標(biāo).

如你所愿, 要求獲取一個(gè)旗標(biāo)的任何線程, 使用一個(gè)(且只能一個(gè))對(duì) up 的調(diào)用釋放它. 在錯(cuò)誤路徑中常常需要特別的小心; 如果在持有一個(gè)旗標(biāo)時(shí)遇到一個(gè)錯(cuò)誤, 旗標(biāo)必須在返回錯(cuò)誤狀態(tài)給調(diào)用者之前釋放旗標(biāo). 沒(méi)有釋放旗標(biāo)是容易犯的一個(gè)錯(cuò)誤; 這個(gè)結(jié)果( 進(jìn)程掛在看來(lái)無(wú)關(guān)的地方 )可能是難于重現(xiàn)和跟蹤的.

5.3.2.?在 scull 中使用旗標(biāo)

旗標(biāo)機(jī)制給予 scull 一個(gè)工具, 可以在存取 scull_dev 數(shù)據(jù)結(jié)構(gòu)時(shí)用來(lái)避免競(jìng)爭(zhēng)情況. 但是正確使用這個(gè)工具是我們的責(zé)任. 正確使用加鎖原語(yǔ)的關(guān)鍵是嚴(yán)密地指定要保護(hù)哪個(gè)資源并且確認(rèn)每個(gè)對(duì)這些資源的存取都使用了正確的加鎖方法. 在我們的例子驅(qū)動(dòng)中, 感興趣的所有東西都包含在 scull_dev 結(jié)構(gòu)里面, 因此它是我們的加鎖體制的邏輯范圍.

讓我們?cè)诳纯催@個(gè)結(jié)構(gòu):


struct scull_dev {
    struct scull_qset *data; /* Pointer to first quantum set */
    int quantum; /* the current quantum size */
    int qset; /* the current array size */
    unsigned long size; /* amount of data stored here */
    unsigned int access_key; /* used by sculluid and scullpriv */
    struct semaphore sem; /* mutual exclusion semaphore */
    struct cdev cdev; /* Char device structure */
};

到結(jié)構(gòu)的底部是一個(gè)稱為 sem 的成員, 當(dāng)然, 它是我們的旗標(biāo). 我們已經(jīng)選擇為每個(gè)虛擬 scull 設(shè)備使用單獨(dú)的旗標(biāo). 使用一個(gè)單個(gè)的全局的旗標(biāo)也可能會(huì)是同樣正確. 通常各種 scull 設(shè)備不共享資源, 然而, 并且沒(méi)有理由使一個(gè)進(jìn)程等待, 而另一個(gè)進(jìn)程在使用不同 scull 設(shè)備. 不同設(shè)備使用單獨(dú)的旗標(biāo)允許并行進(jìn)行對(duì)不同設(shè)備的操作, 因此, 提高了性能.

旗標(biāo)在使用前必須初始化. scull 在加載時(shí)進(jìn)行這個(gè)初始化, 在這個(gè)循環(huán)中:


for (i = 0; i < scull_nr_devs; i++) {
    scull_devices[i].quantum = scull_quantum;
    scull_devices[i].qset = scull_qset;
    init_MUTEX(&scull_devices[i].sem);
    scull_setup_cdev(&scull_devices[i], i);
}

注意, 旗標(biāo)必須在 scull 設(shè)備對(duì)系統(tǒng)其他部分可用前初始化. 因此, init_MUTEX 在 scull_setup_cdev 前被調(diào)用. 以相反的次序進(jìn)行這個(gè)操作可能產(chǎn)生一個(gè)競(jìng)爭(zhēng)情況, 旗標(biāo)可能在它準(zhǔn)備好之前被存取.

下一步, 我們必須瀏覽代碼, 并且確認(rèn)在沒(méi)有持有旗標(biāo)時(shí)沒(méi)有對(duì) scull_dev 數(shù)據(jù)結(jié)構(gòu)的存取. 因此, 例如, scull_write 以這個(gè)代碼開始:


if (down_interruptible(&dev->sem))
    return -ERESTARTSYS;

注意對(duì) down_interruptible 返回值的檢查; 如果它返回非零, 操作被打斷了. 在這個(gè)情況下通常要做的是返回 -ERESTARTSYS. 看到這個(gè)返回值后, 內(nèi)核的高層要么從頭重啟這個(gè)調(diào)用要么返回這個(gè)錯(cuò)誤給用戶. 如果你返回 -ERESTARTSYS, 你必須首先恢復(fù)任何用戶可見(jiàn)的已經(jīng)做了的改變, 以保證當(dāng)重試系統(tǒng)調(diào)用時(shí)正確的事情發(fā)生. 如果你不能以這個(gè)方式恢復(fù), 你應(yīng)當(dāng)替之返回 -EINTR.

scull_write 必須釋放旗標(biāo), 不管它是否能夠成功進(jìn)行它的其他任務(wù). 如果事事都順利, 執(zhí)行落到這個(gè)函數(shù)的最后幾行:


out:
 up(&dev->sem);
 return retval; 

這個(gè)代碼釋放旗標(biāo)并且返回任何需要的狀態(tài). 在 scull_write 中有幾個(gè)地方可能會(huì)出錯(cuò); 這些地方包括內(nèi)存分配失敗或者在試圖從用戶空間拷貝數(shù)據(jù)時(shí)出錯(cuò). 在這些情況中, 代碼進(jìn)行了一個(gè) goto out, 以確保進(jìn)行正確的清理.

5.3.3.?讀者/寫者旗標(biāo)

旗標(biāo)為所有調(diào)用者進(jìn)行互斥, 不管每個(gè)線程可能想做什么. 然而, 很多任務(wù)分為 2 種清楚的類型: 只需要讀取被保護(hù)的數(shù)據(jù)結(jié)構(gòu)的類型, 和必須做改變的類型. 允許多個(gè)并發(fā)讀者常常是可能的, 只要沒(méi)有人試圖做任何改變. 這樣做能夠顯著提高性能; 只讀的任務(wù)可以并行進(jìn)行它們的工作而不必等待其他讀者退出臨界區(qū).

Linux 內(nèi)核為這種情況提供一個(gè)特殊的旗標(biāo)類型稱為 rwsem (或者" reader/writer semaphore"). rwsem 在驅(qū)動(dòng)中的使用相對(duì)較少, 但是有時(shí)它們有用.

使用 rwsem 的代碼必須包含 <linux/rwsem.h>. 讀者寫者旗標(biāo) 的相關(guān)數(shù)據(jù)類型是 struct rw_semaphore; 一個(gè) rwsem 必須在運(yùn)行時(shí)顯式初始化:


void init_rwsem(struct rw_semaphore *sem); 

一個(gè)新初始化的 rwsem 對(duì)出現(xiàn)的下一個(gè)任務(wù)( 讀者或者寫者 )是可用的. 對(duì)需要只讀存取的代碼的接口是:


void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);

對(duì) down_read 的調(diào)用提供了對(duì)被保護(hù)資源的只讀存取, 與其他讀者可能地并發(fā)地存取. 注意 down_read 可能將調(diào)用進(jìn)程置為不可中斷的睡眠. down_read_trylock 如果讀存取是不可用時(shí)不會(huì)等待; 如果被準(zhǔn)予存取它返回非零, 否則是 0. 注意 down_read_trylock 的慣例不同于大部分的內(nèi)核函數(shù), 返回值 0 指示成功. 一個(gè)使用 down_read 獲取的 rwsem 必須最終使用 up_read 釋放.

讀者的接口類似:


void down_write(struct rw_semaphore *sem);
int down_write_trylock(struct rw_semaphore *sem);
void up_write(struct rw_semaphore *sem);
void downgrade_write(struct rw_semaphore *sem);

down_write, down_write_trylock, 和 up_write 全部就像它們的讀者對(duì)應(yīng)部分, 除了, 當(dāng)然, 它們提供寫存取. 如果你處于這樣的情況, 需要一個(gè)寫者鎖來(lái)做一個(gè)快速改變, 接著一個(gè)長(zhǎng)時(shí)間的只讀存取, 你可以使用 downgrade_write 在一旦你已完成改變后允許其他讀者進(jìn)入.

一個(gè) rwsem 允許一個(gè)讀者或者不限數(shù)目的讀者來(lái)持有旗標(biāo). 寫者有優(yōu)先權(quán); 當(dāng)一個(gè)寫者試圖進(jìn)入臨界區(qū), 就不會(huì)允許讀者進(jìn)入直到所有的寫者完成了它們的工作. 這個(gè)實(shí)現(xiàn)可能導(dǎo)致讀者饑餓 -- 讀者被長(zhǎng)時(shí)間拒絕存取 -- 如果你有大量的寫者來(lái)競(jìng)爭(zhēng)旗標(biāo). 由于這個(gè)原因, rwsem 最好用在很少請(qǐng)求寫的時(shí)候, 并且寫者只占用短時(shí)間.

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)