對於 Linux 設備來說,設備是當作文件來處理的。所以,很多設備的 I/O 操作都是很重要的一個部分。這篇博客總結了 Linux 驅動開發中 I/O 相關的一些內容。包含阻塞和非阻塞 I/O 、I/O 輪詢、異步 I/O 等。
一、阻塞和非阻塞 I/O
阻塞是指設備操作時,如果不能獲取資源,則掛起相應的操作單元直到滿足可操作的條件後進行操作。而非阻塞是指在不能滿足操作需求的時候,相應的操作單元並不掛起,而是選擇放棄或者不停的查詢直到滿足條件。
我們可以顯式的設置一個設備文件爲非阻塞 I/O。我們可以在打開一個文件的時候通過設置 filp->f_flags
爲 O_NONBLOCK
來實現。如果設置爲阻塞模式,當使用 open()
函數打開一個文件的時候,I/O 操作會在操作條件不滿足的時候返回 EAGAIN
,此時必須嚴格檢查相應的 errno
。
`
而對於我們的驅動程序來講,我們更多的應該(默認)阻塞進程,將其置入休眠狀態。
休眠狀態:當一個進程被置入休眠狀態之後,他會被標記爲一種特殊的狀態並從 CPU 的運行隊列中移走,直到某些情況修改了這個狀態。對於休眠狀態,有兩個重要的規則:
- 永遠不要在原子操作中進入休眠。
- 我們對喚醒之後的狀態不能做任何假定,必須檢查以確保我們等待的條件爲真。
另外一個需要考慮的問題是,進入休眠的進程必須確保有其他進程會在其他地方喚醒休眠的進程,且休眠的進程能夠被找到。
1.1 等待隊列
如上文所說,我們需要能夠找到進入休眠的進程。我們使用一種叫做等待隊列的數據結構來實現這個功能。
等待隊列是以隊列爲基礎數據結構,與進程調度機制相結合,用於實現內核中的異步事件通知的機制。它實際上是一個進程鏈表,其中包含了等待某個特定事件的所有進程。
初始化等待隊列
等待隊列使用一個等待隊列頭來管理,他可以通過下面兩種方式來定義並初始化:
C
/*使用動態方法定義並初始化*/
wait_queue_head_t my_queue;
init_waitqueue_head(&my_queue);
/*使用靜態定義並初始化*/
DECLARE_WAIT_QUEUE_HEAD(name);
我們使用這個等待隊列頭來指代我們的等待隊列。進入休眠
當我們 I/O 遇到了阻塞條件,我們需要將其休眠的時候,我們可以使用
wait_event
宏來設置相應的進程進入休眠。
C
wait_event(queue, condition);
wait_event_interruptible(queue, condition);
wait_event_timeout(queue, condition, timeout);
wait_event_interruptible_timeout(queue, condition, timeout);
上面相應的宏中,需要注意的是queue
是相應的等待隊列頭,它是值傳遞的。condition
是一個布爾表達式,在表達式爲真之前,進程會持續休眠。該表達式有可能被多次求值。
wait_event
將相應的進程置於非中斷休眠,我們更多使用的是wait_event_interruptible
,他可以被信號中斷,當被信號中斷的時候,返回一個非零值;後兩個函數會設置相應的等待時間,超過等待時間之後,進程返回,返回值爲0。我們也可以手動設置一個進程進入休眠:
- 建立一個並初始化一個等待隊列入口。
我們可以使用宏DEFINE_WAIT(my_wait)
來靜態定義並初始化一個名爲my_wait
的等待隊列入口,也可以使用下面的動態方法:
C
wait_queue_t my_wait;
init_wait(&my_wait);
我們更多使用的是靜態方法。 - 將相應的等待隊列入口加入到隊列中:
C
void prepare_to_wait(wait_queue_head_t *queue,
wait_queue_t *wait,
int state);
其中queue
指我們的等待隊列頭,wait
指我們的進程等待隊列入口,state
是進程的新狀態,有兩種值TASK_INTERRUPTIBLE
(可中斷休眠,常用值)TASK_UNINTERRUPTIBLE
(不可中斷休眠)。 - 再次檢查條件,讓出 CPU:
C
if (condition)
schedule();
- 進程被喚醒後執行相應的清理工作:
C
void finish_wait(wait_queue_head_t *q, wait_queue_t *wait);
該函數將進程狀態更改爲TASK_RUNNING
,並從等待隊列中刪除該進程。 - 最後,我們需要測試我們是否是被信號喚醒的。
- 建立一個並初始化一個等待隊列入口。
對於阻塞的進程,我們需要能夠喚醒它,喚醒函數如下:
C
void wake_up(wait_queue_head_t *q);
void wake_up_interruptible(wait_queue_head_t *q);
第一個函數會喚醒等待隊列q
上面的所有進程,而第二個只能喚醒等待隊列上執行可中斷休眠的進程。通常我們是將對應版本的wait
和wake
搭配使用。
1.2 輪詢操作
在應用層有輪詢的概念(I/O 多路複用),指的是在讀取多個文件的時候,阻塞進程直到給定的文件描述符集合中任何一個可以進行相應的讀寫操作。相應的應用層函數有 select
、poll
、epoll
等,他們在底層調用的是 poll
函數。它的原型是:
C
unsigned int (*poll)(struct file *filp, poll_table *wait);
這個函數要完成兩個工作:
- 對可能引起設備文件狀態變化的等待隊列調用
poll_wait()
函數,將對應的等待隊列添加到poll_table
。 - 返回表示是否能對設備進行無阻塞的讀寫訪問的掩碼。
poll_wait()
函數的原型如下:
C
void poll_wait(struct file *filp, wait_queue_head_t *queue, poll_table *wait);
功能:將當前進程加入到函數指定的等待列表 wait
。
參數:filp
打開的文件指針,queue
需要加入的 wait
的等待隊列。
一個相應的 poll
函數實現代碼如下:
C
static unsigned int xxx_poll(struct file *filp, poll_table *wait) {
struct xxx_dev *dev = filp->private_data;
unsigned int mask = 0;
poll_wait(filp, &dev->r_wait, wait);
poll_wait(filp, &dev->w_wait, wait);
if (readable)
mask |= POLLIN|POLLRDNORM;
if (writable)
mask |= POLLOUT|POLLWRNORM;
return mask;
}
二、異步通知
異步通知的意思是:一旦設備就緒,就主動通知應用程序,這樣應用程序就不用查詢設備的狀態,類似於硬件上的中斷的概念。在 Linux 中,異步通知使用信號來實現。爲了使設備支持異步通知機制,驅動程序中涉及三項工作:
- 支持
F_SETOWN
命令,能在這個命令處理中設置filp->f_owner
爲對應的進程 ID,這部分工作由內核完成。 - 支持
F_SETFL
命令,每當FASYNC
標誌改變的時候,驅動程序中的fasync()
函數將得以執行。因此,應在設備驅動中實現fasync()
函數。 - 在設備資源可以獲得的時候,調用
kill_fasync()
函數激發相應的信號。
設備驅動的異步通知機制編程涉及到兩個函數和一個結構體:
fasync_struct
結構體FASYNC
標誌變更函數
C
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);- 釋放信號的函數
C
void kill_fasync(struct fasync_struct **fa, int sig, int band);
fasync_struct
同樣一般定義在相應的設備結構體 xxx_dev
中。
設備驅動的 fasync()
函數,一般參照下面的模板編寫:
C
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->fasync_queue);
}
在設備刪除的時候,還需要將文件從異步通知列表中刪除,調用xxx_fasync(-1, filp, 0);
。