阻塞與非阻塞訪問是 I/O 操作的兩種不同模式,前者在 I/O 操作暫時不可進行時會讓進程睡眠,後者則不然。在設備驅動中使用非同步通知可以使得對設備的訪問可進行時,由驅動主動通知應用程式進行訪問。這樣,使用無阻塞 I/O 的應用程式無需輪詢設備是否可訪問,而阻塞訪問也可以被類似“中斷”的非同步通知所取代... ...
阻塞與非阻塞訪問是 I/O 操作的兩種不同模式,前者在 I/O 操作暫時不可進行時會讓進程睡眠,後者則不然。在設備驅動中阻塞 I/O一般基於等待隊列來實現,等待隊列可用於同步驅動中事件發生的先後順序。使用非阻塞 I/O 的應用程式也可藉助輪詢函數來查詢設備是否能立即被訪問,用戶空間調用 select()和 poll()介面,設備驅動提供 poll()函數。設備驅動的 poll()本身不會阻塞,但是 poll()和 select()系統調用則會阻塞地等待文件描述符集合中的至少一個可訪問或超時。
在設備驅動中使用非同步通知可以使得對設備的訪問可進行時,由驅動主動通知應用程式進行訪問。這樣,使用無阻塞 I/O 的應用程式無需輪詢設備是否可訪問,而阻塞訪問也可以被類似“中斷”的非同步通知所取代。
等待隊列
進程阻塞會進入睡眠,進入睡眠時應確保有其他進程或者中斷將其喚醒,而用於喚醒的進程必須知道正在睡眠的進程在哪兒,於是就有了等待隊列——睡眠進程的隊列,都等待一個特定的喚醒信號。
- 等待隊列是被等待隊列頭管理的
include <linux/wait.h>
/* defined and initialized statically */
DECLARE_WAIT_QUEUE_HEAD(name);
/* dynamicly */
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
- 進入睡眠
/*
condition為等待條件,睡眠前後都要判斷,如果condition == false則繼續睡眠;
interruptible版被中斷時會返回非0值,驅動應據此返回-ERESTARTSYS;
timeout版等待超時後返回0,單位jiffies。
*/
wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)
- 喚醒
/*
對應的常見的喚醒方法會喚醒等待隊列頭所管理的等待隊列中所有進程;
interruptible版只能喚醒interruptible版的wait_event。
*/
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);
輪詢
使用非阻塞 I/O的應用程式通常會使用 select()和 poll()系統調用查詢是否可對設備進行無阻塞的訪問。 select()和poll()系統調用最終會引發設備驅動中的 poll()函數被執行。
設備驅動中 poll()函數的原型是:
include <linux/poll.h>
/*
wait: 輸入的輪詢表指針
返回是否能對設備進行無阻塞讀、寫訪問的掩碼:
POLLIN | POLLRDNORM -> 可讀
POLLOUT | POLLWRNORM -> 可寫
POLLHUP -> 讀到文件尾
POLLERR -> 設備錯誤
read more: poll.h
*/
unsigned int(*poll)(struct file * filp, struct poll_table* wait);
關鍵的用於向 poll_table 註冊等待隊列的 poll_wait()函數的原型如下:
/* 把管理當前進程的隊列頭queue添加到wait參數指定的等待列表(poll_table)中 */
void poll_wait(struct file *filp, wait_queue_heat_t *queue, poll_table * wait);
非同步通知(Asynchronous Notification)
非同步通知的意思是:一旦設備就緒,則主動通知應用程式,這樣應用程式根本就不需要查詢設備狀態,這一點非常類似於硬體上“中斷”的概念,比較準確的稱謂是“信號驅動的非同步 I/O”。
阻塞 I/O 意味著一直等待設備可訪問後再訪問,非阻塞 I/O 中使用 poll()意味著查詢設備是否可訪問,而非同步通知則意味著設備通知自身可訪問,實現了非同步 I/O。
為了使設備支持非同步通知機制,驅動程式中涉及 3 項工作:
- 支持 F_SETOWN 命令,能在這個控制命令處理中設置 filp->f_owner 為對應進程 ID。不過此項工作已由內核完成,設備驅動無需處理。
- 支持 F_SETFL 命令的處理,每當 FASYNC 標誌改變時,驅動程式中的 fasync()函數將得以執行。因此,驅動中應該實現 fasync()函數。
- 在設備資源可獲得時,調用 kill_fasync()函數激發相應的信號。
驅動中的上述 3 項工作和應用程式中的 3 項工作是一一對應的,如圖所示為非同步通知處理過程中用戶空間和設備驅動的交互:
設備驅動中非同步通知編程比較簡單,主要用到一項數據結構和兩個函數。數據結構是fasync_struct 結構體, 兩個函數分別是:
/* 處理 FASYNC 標誌變更 */
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
/* 釋放信號給用戶 */
void kill_fasync(struct fasync_struct **fa, int sig, int band);
使用方法:
static int xxx_fasync(int fd, struct file *filp, int mode)
{
struct xxx_dev *dev = filp->private_data;
return fasync_helper(fd, filp, mode, &dev->async_queue);
}
static ssize_t xxx_write(struct file *filp, const char __user *buf, size_t count, loff_t *f_pos)
{
struct xxx_dev *dev = filp->private_data;
...
/* 產生非同步讀信號 */
if (dev->async_queue)
kill_fasync(&dev->async_queue, SIGIO, POLL_IN); /* POLL_OUT可寫 */
...
}
static int xxx_release(struct inode *inode, struct file *filp)
{
/* 將文件從非同步通知列表中刪除 */
xxx_fasync(-1, filp, 0);
...
return 0;
}
References
1. Linux Device Drivers
2. Linux設備驅動開發詳解(宋寶華第二版)
Copyright (C) 2016 archiexie@cnblogs. All Rights Reserved.