阻塞型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;
}