阻塞與非阻塞I/O
還記得上篇 我們講到的是linux中並發控制訪問的手段有哪些????原子、信號量、自旋鎖、互斥體。這是為了保護臨界區的資源,是多個進程對共享資源的並發訪問的一種處理手段。但是,在驅動程序中,我們常常為了支持用戶空間對設備的靈活訪問,引入了阻塞與非阻塞I/O兩種不同模式。
阻塞操作是指在執行設備操作時若不能獲得資源則掛起進程,直到滿足可操作的條件后再進行操作。
因為阻塞的進程會進入休眠狀態,因此,必須確保有一個地方能夠喚醒休眠的進程。喚醒進程的地方最大可能發生在中斷里面,因為硬件資源獲得的同時往往伴隨着一個中斷。
注意:驅動程序需要提供阻塞(等待隊列,中斷)和非阻塞方式(輪詢,異步通知)訪問設備。
休眠(被阻塞)的進程處於一個特殊的不可執行狀態。這點非常重要,否則,沒有這種特殊狀態的話,調度程序就可能選出一個本不願意被執行的進程,更糟糕的是,休眠就必須以輪詢的方式實現了。進程休眠有各種原因,但肯定都是為了等待一些事件。事件可能是一段時間、從文件I/O讀更多數據,或者是某個硬件事件。一個進程還有可能在嘗試獲得一個已經占用的內核信號量時被迫進入休眠。休眠的一個常見原因就是文件I/O -- 如進程對一個文件執行了read()操作,而這需要從磁盤里讀取。還有,進程在獲取鍵盤輸入的時候也需要等待。無論哪種情況,內核的操作都相同:進程把它自己標記成休眠狀態,把自己從可執行隊列移出,放入等待隊列,然后調用schedule()選擇和執行一個其他進程。喚醒的進程剛好相反:進程被設置為可執行狀態,然后再從等待隊列中移到可執行隊列。
休眠有兩種相關的進程狀態:TASK_INTERRUPTIBLE and TASK_UNINTERRUPTIBLE。它們的惟一區別是處於TASK_UNINTERRUPTIBLE狀態的進程會忽略信號,而處於TASK_INTERRUPTIBLE狀態的進程如果收到信號會被喚醒並處理信號(然后再次進入等待睡眠狀態)。兩種狀態的進程位於同一個等待隊列上,等待某些事件,不能夠運行。
休眠通過等待隊列進行處理。等待隊列是由等待某些事件發生的進程組成的簡單鏈表。內核用wake_queue_head_t來代表等待隊列。等待隊列可以通過DECLARE_WAITQUEUE()靜態創建,也可以有init_waitqueue_head()動態創建。進程把自己放入等待隊列中並設置成不可執行狀態。等與等待隊列相關的事件發生的時候,隊列上的進程會被喚醒。為了避免產生競爭條件,休眠和喚醒的實現不能有紕漏。
等待隊列
在Linux驅動程序中,可以使用等待隊列來實現阻塞進程的喚醒。
進程通過執行下面幾步將自己加入到一個等待隊列中:
當然,首先是定義等待隊列頭,並初始化:
wait_queue_head_t wait;
init_waitqueue_head(&wait);
1. 調用DECLARE_WAITQUEUE()創建一個等待隊列的項
|/* 'q' is the wait queue we wish to sleep on */ |
|DECLARE_WAITQUEUE(wait, current); |
2. 調用add_wait_queue()把自己加入到隊列中。該隊列在進程等待的條件滿足時喚醒它。當然我們必須在其他地方撰寫相關代碼,在事件發生時,對等待隊列執行wake_up()操作
|-----------------------------|
|add_wait_queue(q, &wait); |
|-----------------------------|
while (!condition) { /* condition is the event that we are waiting for */
3. 將進程的狀態變更為TASK_INTERRUPTIBLE or TASK_UNINTERRUPTIBLE
|----------------------------------------------|
| /* or TASK_UNINTERRUPTIBLE */ |
| __set_current_state(TASK_INTERRUPTIBLE); |
|----------------------------------------------|
4. 如果狀態被設置為TASK_INTERRUPTIBLE,則信號可以喚醒進程(信號和事件都可以喚醒該進程)。這就是所謂的偽喚醒(喚醒不是因為事件的發生,而是由信號喚醒的),因此檢查並處理信號。
注: 信號和等待事件都可以喚醒處於TASK_INTERRUPTIBLE狀態的進程,信號喚醒該進程為偽喚醒;該進程被喚醒后,如果(!condition)結果為真,則說明該進程不是由等待事件喚醒的,而是由信號喚醒的。所以該進程處理信號后將再次讓出CPU控制權
|----------------------------------------------|
| if (signal_pending(current)) |
| /* handle signal */ |
|----------------------------------------------|
5. Tests whether the condition is true. If it is, there is no need to sleep. If it is not true, the task calls schedule().
本進程在此處交出CPU控制權,如果該進程再次被喚醒,將從while循環結尾處繼續執行,因而將回到while循環的開始處while (!condition),進測等待事件是否真正發生.
|----------------------------------------------|
| schedule(); |
|----------------------------------------------|
}
6. Now that the condition is true, the task can set itself to TASK_RUNNING and remove itself from the wait queue via remove_wait_queue().
|----------------------------------------------|
|set_current_state(TASK_RUNNING); |
|remove_wait_queue(q, &wait); |
|----------------------------------------------|
另外,在程序中必須有喚醒等待隊列的機制:
Wake_up_interruptible(&q);
輪詢操作
輪詢的概念與作用
使用非阻塞I/O的應用程序通常會使用select()和poll()系統調用查詢是否可對設備進行無阻塞的訪問。select()和poll()系統調用最終會引發設備驅動中的poll()函數被執行。
select()和poll()系統調用的本質一樣,前者在BSD UNIX中引入,后者在System V中引入。
應用程序中的輪詢編程
int select(int numfds,fd_set *readfds, fd_set *writefds, fd_set *exceptfds,struct timeval *timeout);
文件描述符集合操作:FD_ZERO(fd_set *set) FD_SET(int fd, fd_set *set)
FD_CLR(int fd, fd_set *set) FD_ISSET(int fd, fd_set *set)
設備驅動中的輪詢編程
unsigned int (*poll)(struct file *filp, struct poll_table *wait);
void poll_wait(struct file *filp, wait_queue_head_t *queue, struct poll_table *wait);
poll()函數的典型模板:
static unsigned int xxx_poll(struct file *filp, poll_table *wait)
{
unsigned int mask = O;
struct xxx_dev *dev - filp->private_data;//獲取設備接構體指針
.....
poll_wait (filp, &dev->r_wait, wait);//加讀等待隊列頭
poll_wait (filp, &dev->w_ait, waitl);//加寫等待隊列頭
if(...) //可讀
{
mask |=POLLIN|POLLRDNORM; //標志數據可獲得
}
if(...) //可寫
{
mask |=POLLOUT|POLLRDNORM; //標志數據可寫入
}
...
return mask;
}