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