W3Cschool
恭喜您成為首批注冊用戶
獲得88經(jīng)驗(yàn)值獎勵
讓我們看看我們?nèi)绾谓o scull 加鎖. 我們的目標(biāo)是使我們對 scull 數(shù)據(jù)結(jié)構(gòu)的操作原子化, 就是在有其他執(zhí)行線程的情況下這個操作一次發(fā)生. 對于我們的內(nèi)存泄漏例子, 我們需要保證, 如果一個線程發(fā)現(xiàn)必須分配一個特殊的內(nèi)存塊, 它有機(jī)會進(jìn)行這個分配在其他線程可做測試之前. 為此, 我們必須建立臨界區(qū): 在任何給定時間只有一個線程可以執(zhí)行的代碼.
不是所有的臨界區(qū)是同樣的, 因此內(nèi)核提供了不同的原語適用不同的需求. 在這個例子中, 每個對 scull 數(shù)據(jù)結(jié)構(gòu)的存取都發(fā)生在由一個直接用戶請求所產(chǎn)生的進(jìn)程上下文中; 沒有從中斷處理或者其他異步上下文中的存取. 沒有特別的周期(響應(yīng)時間)要求; 應(yīng)用程序程序員理解 I/O 請求常常不是馬上就滿足的. 進(jìn)一步講, scull 沒有持有任何其他關(guān)鍵系統(tǒng)資源, 在它存取它自己的數(shù)據(jù)結(jié)構(gòu)時. 所有這些意味著如果 scull 驅(qū)動在等待輪到它存取數(shù)據(jù)結(jié)構(gòu)時進(jìn)入睡眠, 沒人介意.
"去睡眠" 在這個上下文中是一個明確定義的術(shù)語. 當(dāng)一個 Linux 進(jìn)程到了一個它無法做進(jìn)一步處理的地方時, 它去睡眠(或者 "阻塞"), 讓出處理器給別人直到以后某個時間它能夠再做事情. 進(jìn)程常常在等待 I/O 完成時睡眠. 隨著我們深入內(nèi)核, 我們會遇到很多情況我們不能睡眠. 然而 scull 中的 write 方法不是其中一個情況. 因此我們可使用一個加鎖機(jī)制使進(jìn)程在等待存取臨界區(qū)時睡眠.
正如重要地, 我們將進(jìn)行一個可能會睡眠的操作( 使用 kmalloc 分配內(nèi)存 ) -- 因此睡眠是一個在任何情況下的可能性. 如果我們的臨界區(qū)要正確工作, 我們必須使用一個加鎖原語在一個擁有鎖的進(jìn)程睡眠時起作用. 不是所有的加鎖機(jī)制都能夠在可能睡眠的地方使用( 我們在本章后面會看到幾個不可以的 ). 然而, 對我們現(xiàn)在的需要, 最適合的機(jī)制時一個旗標(biāo).
旗標(biāo)在計(jì)算機(jī)科學(xué)中是一個被很好理解的概念. 在它的核心, 一個旗標(biāo)是一個單個整型值, 結(jié)合有一對函數(shù), 典型地稱為 P 和 V. 一個想進(jìn)入臨界區(qū)的進(jìn)程將在相關(guān)旗標(biāo)上調(diào)用 P; 如果旗標(biāo)的值大于零, 這個值遞減 1 并且進(jìn)程繼續(xù). 相反, 如果旗標(biāo)的值是 0 ( 或更小 ), 進(jìn)程必須等待直到別人釋放旗標(biāo). 解鎖一個旗標(biāo)通過調(diào)用 V 完成; 這個函數(shù)遞增旗標(biāo)的值, 并且, 如果需要, 喚醒等待的進(jìn)程.
當(dāng)旗標(biāo)用作互斥 -- 阻止多個進(jìn)程同時在同一個臨界區(qū)內(nèi)運(yùn)行 -- 它們的值將初始化為 1. 這樣的旗標(biāo)在任何給定時間只能由一個單個進(jìn)程或者線程持有. 以這種模式使用的旗標(biāo)有時稱為一個互斥鎖, 就是, 當(dāng)然, "互斥"的縮寫. 幾乎所有在 Linux 內(nèi)核中發(fā)現(xiàn)的旗標(biāo)都是用作互斥.
Linux 內(nèi)核提供了一個遵守上面語義的旗標(biāo)實(shí)現(xiàn), 盡管術(shù)語有些不同. 為使用旗標(biāo), 內(nèi)核代碼必須包含 <asm/semaphore.h>. 相關(guān)的類型是 struct semaphore; 實(shí)際旗標(biāo)可以用幾種方法來聲明和初始化. 一種是直接創(chuàng)建一個旗標(biāo), 接著使用 sema_init 來設(shè)定它:
void sema_init(struct semaphore *sem, int val);
這里 val 是安排給旗標(biāo)的初始值.
然而, 通常旗標(biāo)以互斥鎖的模式使用. 為使這個通用的例子更容易些, 內(nèi)核提供了一套幫助函數(shù)和宏定義. 因此, 一個互斥鎖可以聲明和初始化, 使用下面的一種:
DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name);
這里, 結(jié)果是一個旗標(biāo)變量( 稱為 name ), 初始化為 1 ( 使用 DECLARE_MUTEX ) 或者 0 (使用 DECLARE_MUTEX_LOCKED ). 在后一種情況, 互斥鎖開始于上鎖的狀態(tài); 在允許任何線程存取之前將不得不顯式解鎖它.
如果互斥鎖必須在運(yùn)行時間初始化( 這是如果動態(tài)分配它的情況, 舉例來說), 使用下列中的一個:
void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);
在 Linux 世界中, P 函數(shù)稱為 down -- 或者這個名子的某個變體. 這里, "down" 指的是這樣的事實(shí), 這個函數(shù)遞減旗標(biāo)的值, 并且, 也許在使調(diào)用者睡眠一會兒來等待旗標(biāo)變可用之后, 給予對被保護(hù)資源的存取. 有 3 個版本的 down:
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
down 遞減旗標(biāo)值并且等待需要的時間. down_interruptible 同樣, 但是操作是可中斷的. 這個可中斷的版本幾乎一直是你要的那個; 它允許一個在等待一個旗標(biāo)的用戶空間進(jìn)程被用戶中斷. 作為一個通用的規(guī)則, 你不想使用不可中斷的操作, 除非實(shí)在是沒有選擇. 不可中斷操作是一個創(chuàng)建不可殺死的進(jìn)程( 在 ps 中見到的可怕的 "D 狀態(tài)" )和惹惱你的用戶的好方法, 使用 down_interruptible 需要一些格外的小心, 但是, 如果操作是可中斷的, 函數(shù)返回一個非零值, 并且調(diào)用者不持有旗標(biāo). 正確的使用 down_interruptible 需要一直檢查返回值并且針對性地響應(yīng).
最后的版本 ( down_trylock ) 從不睡眠; 如果旗標(biāo)在調(diào)用時不可用, down_trylock 立刻返回一個非零值.
一旦一個線程已經(jīng)成功調(diào)用 down 各個版本中的一個, 就說它持有著旗標(biāo)(或者已經(jīng)"取得"或者"獲得"旗標(biāo)). 這個線程現(xiàn)在有權(quán)力存取這個旗標(biāo)保護(hù)的臨界區(qū). 當(dāng)這個需要互斥的操作完成時, 旗標(biāo)必須被返回. V 的 Linux 對應(yīng)物是 up:
void up(struct semaphore *sem);
一旦 up 被調(diào)用, 調(diào)用者就不再擁有旗標(biāo).
如你所愿, 要求獲取一個旗標(biāo)的任何線程, 使用一個(且只能一個)對 up 的調(diào)用釋放它. 在錯誤路徑中常常需要特別的小心; 如果在持有一個旗標(biāo)時遇到一個錯誤, 旗標(biāo)必須在返回錯誤狀態(tài)給調(diào)用者之前釋放旗標(biāo). 沒有釋放旗標(biāo)是容易犯的一個錯誤; 這個結(jié)果( 進(jìn)程掛在看來無關(guān)的地方 )可能是難于重現(xiàn)和跟蹤的.
旗標(biāo)機(jī)制給予 scull 一個工具, 可以在存取 scull_dev 數(shù)據(jù)結(jié)構(gòu)時用來避免競爭情況. 但是正確使用這個工具是我們的責(zé)任. 正確使用加鎖原語的關(guān)鍵是嚴(yán)密地指定要保護(hù)哪個資源并且確認(rèn)每個對這些資源的存取都使用了正確的加鎖方法. 在我們的例子驅(qū)動中, 感興趣的所有東西都包含在 scull_dev 結(jié)構(gòu)里面, 因此它是我們的加鎖體制的邏輯范圍.
讓我們在看看這個結(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)的底部是一個稱為 sem 的成員, 當(dāng)然, 它是我們的旗標(biāo). 我們已經(jīng)選擇為每個虛擬 scull 設(shè)備使用單獨(dú)的旗標(biāo). 使用一個單個的全局的旗標(biāo)也可能會是同樣正確. 通常各種 scull 設(shè)備不共享資源, 然而, 并且沒有理由使一個進(jìn)程等待, 而另一個進(jìn)程在使用不同 scull 設(shè)備. 不同設(shè)備使用單獨(dú)的旗標(biāo)允許并行進(jìn)行對不同設(shè)備的操作, 因此, 提高了性能.
旗標(biāo)在使用前必須初始化. scull 在加載時進(jìn)行這個初始化, 在這個循環(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è)備對系統(tǒng)其他部分可用前初始化. 因此, init_MUTEX 在 scull_setup_cdev 前被調(diào)用. 以相反的次序進(jìn)行這個操作可能產(chǎn)生一個競爭情況, 旗標(biāo)可能在它準(zhǔn)備好之前被存取.
下一步, 我們必須瀏覽代碼, 并且確認(rèn)在沒有持有旗標(biāo)時沒有對 scull_dev 數(shù)據(jù)結(jié)構(gòu)的存取. 因此, 例如, scull_write 以這個代碼開始:
if (down_interruptible(&dev->sem))
return -ERESTARTSYS;
注意對 down_interruptible 返回值的檢查; 如果它返回非零, 操作被打斷了. 在這個情況下通常要做的是返回 -ERESTARTSYS. 看到這個返回值后, 內(nèi)核的高層要么從頭重啟這個調(diào)用要么返回這個錯誤給用戶. 如果你返回 -ERESTARTSYS, 你必須首先恢復(fù)任何用戶可見的已經(jīng)做了的改變, 以保證當(dāng)重試系統(tǒng)調(diào)用時正確的事情發(fā)生. 如果你不能以這個方式恢復(fù), 你應(yīng)當(dāng)替之返回 -EINTR.
scull_write 必須釋放旗標(biāo), 不管它是否能夠成功進(jìn)行它的其他任務(wù). 如果事事都順利, 執(zhí)行落到這個函數(shù)的最后幾行:
out:
up(&dev->sem);
return retval;
這個代碼釋放旗標(biāo)并且返回任何需要的狀態(tài). 在 scull_write 中有幾個地方可能會出錯; 這些地方包括內(nèi)存分配失敗或者在試圖從用戶空間拷貝數(shù)據(jù)時出錯. 在這些情況中, 代碼進(jìn)行了一個 goto out, 以確保進(jìn)行正確的清理.
旗標(biāo)為所有調(diào)用者進(jìn)行互斥, 不管每個線程可能想做什么. 然而, 很多任務(wù)分為 2 種清楚的類型: 只需要讀取被保護(hù)的數(shù)據(jù)結(jié)構(gòu)的類型, 和必須做改變的類型. 允許多個并發(fā)讀者常常是可能的, 只要沒有人試圖做任何改變. 這樣做能夠顯著提高性能; 只讀的任務(wù)可以并行進(jìn)行它們的工作而不必等待其他讀者退出臨界區(qū).
Linux 內(nèi)核為這種情況提供一個特殊的旗標(biāo)類型稱為 rwsem (或者" reader/writer semaphore"). rwsem 在驅(qū)動中的使用相對較少, 但是有時它們有用.
使用 rwsem 的代碼必須包含 <linux/rwsem.h>. 讀者寫者旗標(biāo) 的相關(guān)數(shù)據(jù)類型是 struct rw_semaphore; 一個 rwsem 必須在運(yùn)行時顯式初始化:
void init_rwsem(struct rw_semaphore *sem);
一個新初始化的 rwsem 對出現(xiàn)的下一個任務(wù)( 讀者或者寫者 )是可用的. 對需要只讀存取的代碼的接口是:
void down_read(struct rw_semaphore *sem);
int down_read_trylock(struct rw_semaphore *sem);
void up_read(struct rw_semaphore *sem);
對 down_read 的調(diào)用提供了對被保護(hù)資源的只讀存取, 與其他讀者可能地并發(fā)地存取. 注意 down_read 可能將調(diào)用進(jìn)程置為不可中斷的睡眠. down_read_trylock 如果讀存取是不可用時不會等待; 如果被準(zhǔn)予存取它返回非零, 否則是 0. 注意 down_read_trylock 的慣例不同于大部分的內(nèi)核函數(shù), 返回值 0 指示成功. 一個使用 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 全部就像它們的讀者對應(yīng)部分, 除了, 當(dāng)然, 它們提供寫存取. 如果你處于這樣的情況, 需要一個寫者鎖來做一個快速改變, 接著一個長時間的只讀存取, 你可以使用 downgrade_write 在一旦你已完成改變后允許其他讀者進(jìn)入.
一個 rwsem 允許一個讀者或者不限數(shù)目的讀者來持有旗標(biāo). 寫者有優(yōu)先權(quán); 當(dāng)一個寫者試圖進(jìn)入臨界區(qū), 就不會允許讀者進(jìn)入直到所有的寫者完成了它們的工作. 這個實(shí)現(xiàn)可能導(dǎo)致讀者饑餓 -- 讀者被長時間拒絕存取 -- 如果你有大量的寫者來競爭旗標(biāo). 由于這個原因, rwsem 最好用在很少請求寫的時候, 并且寫者只占用短時間.
Copyright©2021 w3cschool編程獅|閩ICP備15016281號-3|閩公網(wǎng)安備35020302033924號
違法和不良信息舉報電話:173-0602-2364|舉報郵箱:jubao@eeedong.com
掃描二維碼
下載編程獅App
編程獅公眾號
聯(lián)系方式:
更多建議: