竟態,阻塞

 產生的情況:

  1. SMP系統中任何時刻都可能出現
  2. 內核的代碼是可搶佔的 

設計驅動時儘可能減少資源共享

保護數據併發訪問的一般方法

  1. 使用緩衝區並且避免共享變量
  2. 使用自旋鎖實現互斥訪問
  3. 使用原子地遞增或遞減鎖變量

   //. . . . . . . 以上保持不變

static ssize_t ctest_write(struct file *file,char __user *buf,
size_t count,loff_t *offst)
{
static char ctestbuf[256]; //僅僅是把ctestbuf變成了static的緩衝區
int cnt;
memset(ctestbuf,0,256);
if(count<256)
cnt = count;
else
cnt = 255;
if(!copy_from_user(ctestbuf,buf,cnt)){
printk(“%s\n”,ctestbuf); //考慮一下,如果在這一行執行前發生進程間切換,如何?
return cnt;
}else{
return -1;
}
}
// . . . . . . 以下保持不變

-----------------------------------------------------------------------

大家仔細分析上面的代碼,我們只是將char ctestbuf[256]改稱了static char ctestbuf[256], 這樣這個空間將不是存儲在函數的棧上,而是存儲在了靜態全局區,
這樣ctestbuf就變成了一個共享的內存資源。

當有多個進程對這個設備進行write操作的時候,比如P1向/dev/ctest設備寫”hello,kernel”,P2向/dev/ctest寫”hello,ctest”,請問最後的結果,printk是輸出什麼呢?



大家可以寫下面的一段程序來測試:

-----------------------------------------------------------------------
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
int main()
{
int fd = fopen(“/dev/ctest”,O_RDWR);
if(fd<0){
perror(“open /dev/ctest failed\n”)
exit(1);
}
if(fork()==0){ //子進程
int cnt = 0xfffff;
while(--cnt){
write(fd,”hello,kernel”,13);
}
close(fd)
}else{ //父進程
int cnt = 0xfffff;
while(--cnt){
write(fd,”hello,ctest”,12);
}
close(fd);
wait(NULL);
return 0;
}
}

-----------------------------------------------------------------------

這裏的答案實際上是不確定的,如果內核不支持搶佔的話,P1進程將是讓printk輸出”hello,kernel”, P2進程將是讓printk()輸出”hello,ctest”.

但是如果支持搶佔的話,那麼結果就很難確定了,考慮一下極端情況,如果P1進程剛剛完成了數據從user buffer 到ctestbuf的拷貝工作,此時,進行了一次進程調度(如代碼中
指示的位置),調度的結果如果是讓P2進程執行,並且完成了write的操作,那麼ctestbuf裏面的數據將會是”hello,ctest”.這樣當控制權再次回到P1的時候,printk的輸出結
果將是”hello,ctest”.同理P2進程也可能輸出”hello,kernel”.這就是我們一直在討論的競爭條件的情況。

 要使用一個信號量,首先應該#include <asm/semaphore.h> 或者是include <linux/semaphore.h>, 不同的體系結構使用的頭文件的位置可能是不同的。我們使用struct semphore這個類型來表示信號量,有幾種聲明和初始化的方式,如下:

struct semaphore sem;

void sema_init(struct
semaphore *sem,int val)

操作步驟:

1. 定義一個信號量

2. 使用sema_init函數初始化它,val的值代表初始值

DECLARE_MUTEX(name);
DECLARE_MUTEX_LOCKED(name)
這裏定義的就是一個互斥體,這兩個宏是由內核提供的,在內核裏面常見這樣的方式,不光定義了struct semaphore name;還給出了初始值,他們的初始值是多少?

void init_MUTEX(struct
semaphore *sem);

void init_MUTEX_LOCKED(structsemaphore *sem)
這裏很明顯是對於前面定義的sem進行初始化,而且是將之當成互斥體初始化,也就是說信號量的初始值非0既1。



在Linux的世界中,P操作對應的函數爲down(),意思很明顯,將信號量的值下降一點.在Linux裏面,獲得一個信號量的方法有三種,其意義差別很大:

void down(struct semaphore *)
int down_interruptible(struct semaphore *)

都執行P操作,將信號量的值減1,如果信號量的值小於0,它將進入blocked 狀態,只是這裏需要注意:第二個函數比第一個函數多了interruptible,很明顯,如果使用第二個函數進入的blocked狀態,那麼將是一個可中斷的blocked狀態,那麼down()進入的就是一個不可中斷的狀態。可中斷狀態與不可中斷狀態的區別在於對信號的處理方式不同。

如果進程是因爲down()進入的blocked狀態,那麼將不能被殺死,因爲殺死進程就是向其發送一個信號,比如kill -9 PID.

使用down_interruptible需要額外小心,如果操作被中斷,該函數會返回非零值,而調用者不會擁有該信號量。對down_interruptible的正確使用需要始終檢查返回值,並作出相應的響應,如果返回值爲非0,通常立即返回-ERESTARTSYS,如:

if(down_interruptible(&sem)){
return –ERESTARTSYS;
}


int down_trylock(struct semaphore *)

此函數一樣是執行P操作,只是這裏帶有try的意思,那也就是嘗試獲得,換句話說,它可能會獲得信號量,也可能信號量此時不可用。當信號量不可用的時候,調用此函數的進程將不會等待,也就是說不會進入blocked 狀態,而是恢復信號量的原有值,並繼續執行。

既然此函數不會導致進程等待,而是繼續執行,那麼它的返回值就應該告知此函數是否獲得了信號量。當返回爲0時,表明獲得信號量,所以在退出臨界區的時候需要釋放,否則將無須此操作。

if(!down_trylock(&sem)){
...
up(&sem);
}


當一個線程成功調用上述down的某個版本之後,就稱爲該線程“擁有”(或“拿到”、“獲取”)了該信號量。這樣,該線程就被賦予訪問由該信號量保護的臨界區的權利。

不管你用何種方式獲得了semaphore,釋放的操作都是一樣的,這裏釋放的操作也就對應sempahore的V操作。
void up(struct semaphore *sem);

調用up之後,調用者不再擁有該信號量。

如讀者所料,任何拿到信號量的線程都必須通過一次(只有一次)對up的調用而釋放該信號量。在出現錯誤的情況下,經常需要特別小心;如果在擁有一個信號量時發生錯誤,必須在將錯誤狀態返回給調用者之前釋放該信號量。我們很容易犯忘記釋放信號量的錯誤,而其結果(進程在某些無關位置處被掛起)很難復現和跟蹤。信號量一般這樣被使用,如下所示:

//定義信號量

DECLARE_MUTEX(sem);

down(&sem);//獲取信號量,保護臨界區



Critical section//臨界區



up(&sem);//釋放信號量

這樣,我們可以使用semaphore讓我們開篇的代碼,不管是否是搶佔式,還是非搶佔式內核下都可安全的運行:

-----------------------------------------------------------------------

DECLARE_MUTEX(sem);

//. . . . . . . 以上保持不變

static ssize_t ctest_write(struct file *file,char __user *buf,
size_t count,loff_t *offst)
{
static char ctestbuf[256]; //僅僅是把ctestbuf變成了static的緩衝區
int cnt;
if(down_interruptible(&sem)){
return –ERESTARTSYS;
}。
memset(ctestbuf,0,256);
if(count<256)
cnt = count;
else
cnt = 255;
if(!copy_from_user(ctestbuf,buf,cnt)){
printk(“%s\n”,ctestbuf); //考慮一下,如果在這一行執行前發生進程間切換,如何?
up(&sem);
return cnt;
}else{
up(&sem);
return -1;
}
}
// . . . . . . 以下保持不變

-----------------------------------------------------------------------

驅動程序不能使用信號量, 可以使用自旋鎖,不能等待時間太長,因爲其他cpu被強制等待 可以看到,使用信號量,如果有一個進程持有了信號量,另一個進程就會進入睡眠等待。而很多情況並不需要進程進入等待睡眠,例如中斷處理中不允許進入睡眠,或者一些情況下只是簡單測試公共數據是否被其它進程佔用,如果被佔用,就重新測試直到可以使用,這裏就只需要利用自旋鎖(spinlock)。當然使用自旋鎖時處理器被佔用,所以自旋鎖適用持有數據時間比較短的情況,而且絕對不能在持有鎖時進入睡眠。

  #include <linux/spinlock.h>
spinlock_t my_lock = SPIN_LOCK_UNLOCKED; 或者spin_lock_init(&my_lock); 申明/創建一個鎖
spin_lock(spinlock_t *my_lock); 獲得給定的鎖,如果鎖被佔用,就自旋直到鎖可用,當spin_lock返回時調用函數即持有了該鎖,直到釋放spin_unlock(spinlock_t *my_lock); 釋放鎖

 

2 關於阻塞和非阻塞
2.1 關於阻塞
對read調用存在一個問題,就是當設備無數據可讀時,解決的方法有兩種,一是不阻塞直接讀失敗跳出。 二就是阻塞讀操作,進程進入睡眠,等待有數據時喚醒。
這裏探討一下阻塞型IO,處理睡眠和喚醒。
睡眠就是當一個進程需要等待一個事件時,應該暫時掛起,讓出CPU,等事件到達後再喚醒執行。
處理睡眠的一種方法是把進程加入等待隊列:
1)首先需要申明和初始化一個等待隊列項.
#include <linux/sched.h>
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
如果是申明一個靜態全局的等待隊列,可以不用上面兩個定義,直接使用
DECLARE_WAIT_QUEUE_HEAD(my_queue); //靜態申明將在編譯時自動被初始化
2)使用已初始化的等待隊列項
在需要加入內核等待隊列時,調用 interruptible_sleep_on(&my_queue); 或者sleep_on(&my_queue)
在需要喚醒時,調用wake_up_interruptible(&my_queue); 或者wake_up(&my_queue)
3)interruptible_sleep_on()的缺陷
a.引起的競態:
要了解interruptible_sleep_on()等這些sleep_on函數可能引起的競態,就需要多interruptible_sleep_on()的實現有個認識。
等待隊列其實是一個隊列鏈表,鏈表中的數據是類型wait_queue_t. 簡化了的interruptible_sleep_on()內部大概是這樣:
#include <list.h>
wait_queue_t wait; //;定義一個等待隊列
init_wait_queue_entry(&wait, current); //;初始化
current->state = TASK_INTERRUPTILBE; //;設置爲休眠狀態,將要進入睡眠,系統會認爲進程已經睡眠,不會去調用該進程
add_wait_queue(&my_queue, &wait); //;把我們定義的等待隊列項加入到這個等待隊列中
schedule(); //;真正進入睡眠
remove_wait_queue(&my_queue, &wait); //;事件到達,schedule()返回
競態就發生在current->state = TASK_INTERRUPTIBLE之前,在一些情況下,當驅動準備進入睡眠,即已經設置了current->state之前,可能剛好有數據到達,這個時候wake_up是不會喚醒這個還沒有真正進入睡眠的進程,這樣就可能造成該進程因爲沒有響應喚醒一直處於睡眠,這樣就產生這個競態,這個競態也是很容易發生的。解決辦法就是不使用interruptible_sleep_on(),而是直接使用它的內部實現。
例如:
#include <list.h>
wait_queue_t wait; //;定義一個等待隊列
init_wait_queue_entry(&wait, current); //;初始化
add_wait_queue(&my_queue, &wait); //;把我們定義的等待隊列項加入到這個等待隊列中
while(1){
current->state = TASK_INTERRUPTILBE; //;設置爲休眠狀態,將要進入睡眠,系統會認爲該進程睡眠,而不對他進行調用
if (short_head != short_tail) break; //;測試是否有數據到達,如果有,跳出
schedule(); //;真正進入睡眠
}
set_current_state(TASK_RUNNING);
remove_wait_queue(&my_queue, &wait); //;事件到達,schedule()返回
事實上,可以不用我們做這些複雜的事情,內核定義了一個宏
wait_event_interruptible(wq, condition); 或者wait_event(wq, condition) condition就是測試的條件

b.關於排它睡眠:
存在這樣一種情況,幾個進程都在等待同一個事件,當事件到達調用wake_up時,等待在這個事件上的所有進程都被喚醒,但是假如該事件只需要被一個進程處理,其它進程只是被喚醒後接着又進入睡眠,這樣很多進程運行,導致上下文切換,造成系統變慢。解決辦法是通過直接對等待隊列的鏈表操作, 指定排它睡眠,內核把這個隊列放在其它非排它睡眠之前,當事件到達時如果遇到排它睡眠的隊列,喚醒它後即結束,其它睡眠下一次被喚醒處理。事實上sleep_on的一系列函數都是對等待隊列的鏈表操作。鏈表中數據項是類型爲wait_queue_t的數據。
直接操作鏈表設置排它睡眠方法大概如下:
#include <list.h>
wait_queue_t wait; //;定義一個等待隊列
init_wait_queue_entry(&wait, current); //;初始化
current->state = TASK_INTERRUPTALBE | TASK_EXCLUSIVE; //;設置爲排它
add_wait_queue_exclusive(queue, &wait); //;把我們定義的等待隊列項加入到這個等待隊列中
schedule(); //;進入睡眠
remove_wait_queue(queue, &wait); //;事件到達,schedule()返回
c.在多個隊列中睡眠
interrruptible_sleep_on等函數只能在一個隊列中睡眠,如果真的需要做到在多個等待隊列中睡眠,只能通過直接操作等待隊列鏈表。
這個技巧找到相關資料再看看。
2.2. 非阻塞
打開,讀和寫操作在設備沒有準備好或沒有數據時立即返回。
在LINUX的打開設備時,可以傳遞一個參數O_NONBLOCK, 系統的open調用如果使用了這個參數,filp->f_flags的O_NONBLOCK標記將被設置,
驅動檢查到這個標記,應該實現非阻塞的open, read, write方法.
#include <linux/fs.h>

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章