linux裝置驅動學習(八)—阻塞式I/O與休眠

來源:互聯網
上載者:User

阻塞型I/O與休眠

當一個進程被置入休眠時,他會被標記為一種特殊的狀態並從調度器的運行隊列調走(將該進程加入到等待隊列中,等待喚醒)。休眠中的進程會被擱置在一邊,等待喚醒。

要想安全的進行休眠需要注意:

1.永遠不要在原子上下文中進入休眠.原子上下文是指:在執行多個步驟時,不能有任何的並發訪問。對休眠來說,我們的驅動程式不能擁有自旋鎖、seqlock或者RCU鎖時休眠。如果我們已經禁止了中斷,也不能休眠。但在擁有訊號量時允許休眠。如果代碼在擁有訊號量時休眠,任何其他等待該訊號量的線程也會休眠,一次擁有訊號量而休眠的代碼必須很短,並且還要確保擁有訊號量並不會阻塞最終會喚醒我們的進程。

2.每當進程被喚醒,都必須重新檢查等待條件來確保正確的響應。

為了確保喚醒發生,並清楚地知道對每個休眠而言哪些事件序列會結束休眠,為此需要維護一個等待隊列的資料結構

linux中一個等待隊列通過一個等待隊列頭來管理,等待隊列頭是一個wait_queue_head_t結構體,定義在<linux/wait.h>

struct __wait_queue_head{

         spinlock_t lock;

         struct list_head task_list;

}

typedef struct __wait_queue_head  wait_queue_head_t;

它包含一個自旋鎖和一個鏈表。這個鏈表是一個等待隊列入口,它被聲明做 wait_queue_t。wait_queue_head_t包含關於睡眠進程的資訊和它想怎樣被喚醒。

靜態初始化:DECLEAR_WAIT_QUEUE_HEAD(name);

動態初始化:

 wait_queue_head_t my_queue;

 init_waitqueue_head(&my_queue);

簡單休眠方法是wait_event的宏,又如下實現方式:

wait_event(queue,condition);

wait_event_interruptible(queue,condition);

wait_event_timeout(queue,condition,timeout);

wait_event_interruptible_timeout(queue,condition,timeout); //這裡注意休眠隊列是通過值傳遞完成的。

queue為等待隊列頭,注意,它是通過值進行傳遞的,而不是通過指標。condition是一個布爾值,上面的宏在休眠前後對該運算式求值,在條件為真前,進程會保持休眠。

與down_interruptible(&my_sem)一樣interruptible是我們常用的版本。

wait_event_interrupt(queue,condition)返回非零值,表示該休眠被某個訊號中斷。

最後兩個表示在給定的時間內休眠,timeout到期時,都返回零,而無論condition取何值。

void wake_up(wait_queue_head_t  *queue);//這裡注意喚醒進程是通過指標來傳遞的

void wake_up_interruptible(wait_queue_head_t *queue);

程式碼範例:

static DECLEAR_WAIT_QUEUE_HEAD(wq);

static int flag=0;

 

ssize_t sleepy_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_event_interuptible(wq,flag!=0);

     flag = 0;

     printk(KERN_DEBUG "awken process %i (%s)../n",current->pid,current->comm);

     return 0;    //EOF

}

 

ssize_t sleepy_write(struct file *filp,const char __user *buf,size_t count,loff_t *pos)

{

     printk(KERN_DEBUG "process %i (%s) going to awken the reader../n",current->pid,current->comm);

     flag =1;     // 先設定喚醒條件條件

     wake_up_interruptible(wq);

     return count;

這裡需要注意個問題,如果有兩個sleepy_read同時等待wake_up_interruptible,在第一個read被喚醒時會將flag置為零,因此可能會認為第二個read會立即進入休眠狀態。但是要注意wake_up_interruptible(wq)會喚醒所有等待的進程,而在重設flag之前,兩個read進程都完全有可能注意到這個標誌的變化。所以,如果要想確保只有一個進程看見這個費零值,則必須以原子的方式進行檢查。

 

阻塞和非阻塞操作:

全功能的 read 和 write 方法涉及到進程可以決定是進行非阻塞 I/O還是阻塞 I/O操作。明確的非阻塞 I/O 由 filp->f_flags 中的 O_NONBLOCK 標誌來指示(定義再 <linux/fcntl.h> ,被 <linux/fs.h>自動包含)。瀏覽源碼,會發現O_NONBLOCK 的另一個名字:O_NDELAY ,這是為了相容 System V 代碼。O_NONBLOCK 標誌預設地被清除,因為等待資料的進程的正常行為只是睡眠。

如果指定了O_NOBLOCK,read和write會有所變化,如果沒有資料就緒時調用read或者在緩衝區沒空間是調用write,則該調用簡單的返回-EAGAIN。

這裡說明在非阻塞操作會立即返回,從而使得應用程式可以查詢資料的變化。例如在應用程式中,select調用可以對檔案集進行偵測,底層調用poll來完成狀態的查詢。

另外很容易把非阻塞的返回誤認為是EOF,所以必須始終檢查errno的值。

 

O_NOBLOCK在open調用中也很有用,它應用於在open調用可能會阻塞很長時間的場合。例如有的裝置可能會初始化很長的時間,這是就可以選擇在open方法中O_NOBLOCK,如果該標誌被置位,則在裝置開始初始化後會立刻返回-EAGAIN。

只有read、open、open檔案操作受非阻塞操作的影響。

 

這裡列出write和read預設方向的問題:

scull_p_read(struct file *filp,char __user *buf,size_t count,loff_t *pos);

scull_p_write(struct file *filp,const char __user *buf,size_t  count,loff_t *pos);

兩者的角度都是從使用者程式上看的,read表示從緩衝區讀,write則相反。

這裡給出一段執行個體代碼:

static ssize_t  scull_p_read(struct file *filp,char __user *buf,size_t  count ,loff_t *pos)

{

     struct scull_pipe *dev = filp->private;

     iif(down_interruprible(&dev->sem))  //如果操作被中斷,會返回一個非零值,而調用者不會擁有該訊號。

          return -ERESTARTSYS;

     while(dev->rp == dev->wp){      //表示無資料可讀,對結構內成員進行操作在持有鎖的情況下,注意while始終在擁有訊號的前提下對緩衝區進行檢查。如果有資料可讀直接返回給使用者,迴圈被跳過。相反如果為空白則必須休眠。

          up(&dev->sem);

在對裝置內部的讀寫指標進行檢查後,釋放訊號量,注意訊號量必須在讀進程進入休眠之前釋放,否則任何寫入者都沒有機會來喚醒,原因在於讀寫是用訊號量來進行互斥的,如果讀進程在擁有訊號的量情況下休眠,則寫進程永遠得不到這個鎖。

          if(filp->f_flags & O_NONBLOCK)

                  return -EAGAIN;

          if(wait_event_interruptible(dev->inq,(dev->rp != dev->wp)))

                  return -ERESTARTSYS;

快速檢查使用者請求的是否是非阻塞I/O,如果是,則返回,否則將讀進程休眠。這裡要明確一點對於interruptible族的調用來說總是要時刻檢查其傳回值,如down_interuptible、wait_event_interruptible等。如果非零則表示操作被中斷,同時驅動程式返回ERESTARTSYS

          if(down_interruptible(&dev->sem))

                  return  -ERESTARTSYS;

     }

當wait_event_interruptible這個函數返回時,說明其他人喚醒了等待隊列,但不知道具體是哪個情況(進程接受到一個訊號或者緩衝區有資料可讀)。if(wait_event_interruptible(dev->inq,(dev->rp != dev->wp))這條IF語句確保了對訊號正確的響應,該訊號可能用來喚醒進程的。如果訊號沒有被阻塞,正確的動作時讓核心的上層去處理這個事件。為此驅動程式返回給調用者ERESTARTSYS,這個值由虛擬檔案系統層(VFS)內部使用。它或者重啟系統調用,或者給使用者空間返回-EINTR;

就算不是因為訊號而被喚醒,我們仍然無法確認是否有資料可獲得。其他人可能也在等待資料,而且可能贏得競爭並拿走了資料。因此我們必須重新獲得訊號量,來對緩衝區進行檢查。當從while迴圈推出時,我們持有訊號量並且緩衝區包含有可使用的資源。

     if(dev->wp > dev->rp)

           count = min(count,(size_t)(dev->wp - dev->rp));

     else

           count = min(count,(size_t)(dev->end - dev->rp));

      if(copy_to_user(buf,dev->rp,count)){

           up(&dev->sem)

           return -EFAULT;

      }

 

      dev->rp += count;

      if(dev->rp == dev->end)

           dev->rp = dev->buffer;

      up(&dev->sem);

      wake_up_interruptible(&dev->outq);

      PDEBUG("/"%s/" did read %li bytes/n",current->comm,(long)count);

      return count;

}    

 

 

 

 

 

 

 

 

 

 

 

 


 

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.