今天學習並發與競態
一 並發及其管理
競態通常是作為對資源的共用訪問結果而產生的。在設計自己的驅動程式時,第一個要記住的規則是:只要可能,就應該避免資源的共用。若沒有並發訪問,就不會有競態。這種思想的最明顯的應用是避免使用全域變數。但是,資源的共用是不可避免的 ,如硬體資源本質上就是共用、指標傳遞等等。資源共用的硬性規則:(1)在單個執行線程之外共用硬體或軟體資源的任何時候,因為另外一個線程可能產生對該資源的不一致觀察,因此必須顯示地管理對該資源的訪問。--訪問管理的常見技術成為“鎖定”或者“互斥”:確保一次只有一個執行線程可操作共用資源。(2)當核心代碼建立了一個可能和其他核心部分共用的對象時,該對象必須在還有其他組件引用自己時保持存在(並正確工作)。對象尚不能正確工作時,不能將其對核心可用。二 訊號量與互斥體
在書上有這樣一段話,說明了訊號量作為一種互斥機制是可以休眠的:當一個linux進程到達某個時間點,此時它不能進行任何處理時,將進入休眠或阻塞狀態,這將把處理器讓給其他執行線程直到將來它能夠繼續完成自己的處理為止。在等待I/O完成時,進程進場會進入休眠狀態。當然核心中夜存在大量不能進行休眠的情況。因此我們可以使用一種鎖定機制,進程在等待對臨界區的訪問時,此機制可以讓進程進入休眠狀態--訊號量。注意在可能出現休眠的情況下,並不是所有的鎖定機制都適用。
訊號量初始化API:
(1)void sema_init(struct semaphore *sem,int val);
只是建立個訊號量,為初始化
(2)DECLEAR_MUTEX(name); 初始為1
DECLEAR_MUTEX_LOCKED(name); 初始為0
建立並進行初始化,也即在建立是已經初始化了。用LOCKED表示的互斥體初始狀態時鎖定的,在允許任何現成訪問之前,必須顯示的解除該鎖。
(3)如果互斥體在運行時才初始化,則用以下:
void init_MUTEX(struct semaphore *sem);
void init_MUTEX_LOCKED(struct semaphore *sem);
為什麼要在運行時才初始化?正確使用說定機制的關鍵在於明確指定需要保護的對象,將訊號的建立和初始化分開,可以更直接的用來保護所要訪問的資源,清楚的表現我們的意圖.
for(i=0;i<scull_ne_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_quantum,scull_qset是匯出的符號變數。可以看出訊號量必須在裝置對系統其它部分可用之前初始化,也即裝置向核心註冊之前,否則會發生競態。
(4)訊號量down的三個版本
void down(struct semaphore *sem);
int down_interruptible(struct semaphore *sem);
int down_trylock(struct semaphore *sem);
down將減小訊號量的值,並在必要時一直等待,會建立不可殺進程。down_interruptible完成相同的工作,但操作是可以中斷的,允許等待在訊號量上的使用者進程被使用者中斷。使用down_interruptible需要小心,若操作被中斷,該函數會返回非零值,而調用者不會擁有該訊號量。
最後down_trylock永遠不會休眠,如果訊號量在調用時不可獲得,down_tyrlock會立即返回一個非零值。
void up(struct semaphore *sem)調用up以後,調用者不再擁有該訊號量。注意任何拿到訊號量的線程都必須通過(只有一次)對up調用而釋放該訊號量。
三 訊號量的使用
在每個scull_dev結構中定義一個訊號量:
struct scull_dev {
...aphore
struct semaphmoe *sem;
....
};
為什麼不是用一個全域訊號量:由於不同的scull_dev並不共用資源,如果定義一個全域訊號量,那麼在各個裝置之間就會形成競爭。因此沒有理由讓一個進程在其他進程訪問不同的scull裝置時等待。為每個裝置使用單獨訊號量允許不同裝置上的操作可以平行處理。
這樣,我們以後在對scull_dev結構中的所有成員變數進行操作前,都需要用sem保護起來。然後通過goto out來完成訊號量的釋放,具體可以參看scull_write.
四 讀寫訊號量
讀寫訊號量主要是對讀寫操作進行區分,允許讀操作並發的訪問緩衝區而寫操作則必須在持有訊號量的情況對緩衝區進行訪問。
include <linux/rwsem.h>
初始化:
void init_rwsem(struct rw_semaphore *sem);
唯讀介面:
void down_read(struct rw_semaphore *sem); int down_read_trylock(struct rw_semaphore *sem); void up_read(struct rw_semaphore *sem);
|
寫入介面:
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);/*該函數用於把寫者降級為讀者,這有時是必要的。因為寫者是排他性的,因此在寫者保持讀寫訊號量期間,任何讀者或寫者都將無法訪問該讀寫訊號量保護的共用資源,對於那些當前條件下不需要寫訪問的寫者,降級為讀者將,使得等待訪問的讀者能夠立刻訪問,從而增加了並發性,提高了效率。*/
|
一個 rwsem 允許一個寫者或無限多個讀者來擁有該訊號量. 寫者有優先權; 當某個寫者試圖進入臨界區, 就不會允許讀者進入直到寫者完成了它的工作. 如果有大量的寫者競爭該訊號量,則這個實現可能導致讀者“餓死”,即可能會長期拒絕讀者訪問。因此, rwsem 最好用在很少請求寫的時候, 並且寫者只佔用短時間.
五 completion
核心編程中常見的一種方式是:在當前線程之外初始化某個活動,然後等待該活動終止。我們可以用訊號量同步這個進程。struct semaphore sem; 定義個訊號量
init_MUTEX_LOCKED(&sem); 初始化訊號量
start_exteral_task(&sem);
down(&sem);
當外部完成其工作時調用up(&sem);
對於上述情況如果針對該訊號量存在嚴重競爭,效能將受到影響,並且如果像上面那樣使用訊號量在任務完成時進行通訊,則調用down的進程將常時間處於等待狀態。為此2.4實現了completion,completion是一種輕量級的機制,它允許一個線程告訴另一個線程某個工作已經完成。代碼必須包含<linux/completion.h>。
API實現如下:
(1)DECLEARE_COMPLETION(my_completion); 建立同時初始化
(2)分開進行:struct completion my_completion;
init_completion(&my_completion);
要對定義的completion進行等待,用如下調用:
void wait_for_completion(struct completion *c);
該函數將進行一個非中斷等待,如果代碼調用了wait_for_completion且沒有人會完成改任務,則將產生一個不可殺進程。
可以通過以下調用來喚醒等待進程:
void struct(struct completion *c); 喚醒一個等待進程
void struct_all(struct completion *c); 喚醒多個等待進程
樣本程式:
DECLARE_COMPLETION(comp);
sszie_t read(struct file *filp,char __user *buf,size_t count,loff_t *pos)
{
printk(KERN_DEBUG "process %i (%s) going to sleep/n",current->pid,current->comm);
wait_for_completion(&comp); //等待寫進程完成
printk(KERN_DEBUG "awoken &i (&s) /n",current->pid,current->comm);
return 0; //EOF
}
sszie_t write(struct file *filp,const char __user *buf,ssize_t count,loff_t *pos)
{
printk(KERN_DEBUG "process %i (%s) awakening the reader/n",current->pid,current->comm);
complete(&comp); //喚醒讀進程
return count;
}