3、異步通知與異步I/O
3.1 Linux信號
阻塞與非阻塞訪問、poll()函數提供了較好的解決設備訪問的機制,但是如果有了異步通知,整套機制則更加完整了。
異步通知的意思是:一旦設備就緒,則主動通知應用程序,這樣應用程序根本就不需要查詢設備狀態,這一點非常類似于硬件上“中斷”的概念,比較準確的稱謂是“信號驅動的異步I/O”。信號是在軟件層次上對中斷機制的一種模擬,在原理上,一個進程收到一個信號與處理器收到一個中斷請求可以說是一樣的。信號是異步的,一個進程不必通過任何操作來等待信號的到達,事實上,進程也不知道信號到底什么時候到達。
阻塞I/O意味著一直等待設備可訪問后再訪問,非阻塞I/O中使用poll()意味著查詢設備是否可訪問,而異步通知則意味著設備通知用戶自身可訪問,之后用戶再進行I/O處理。由此可見,這幾種I/O方式可以相互補充。
圖9.1呈現了阻塞I/O、結合輪詢的非阻塞I/O及基于SIGIO的異步通知在時間先后順序上的不同。
這里要強調的是:阻塞、非阻塞I/O、異步通知本身沒有優劣,應該根據不同的應用場景合理選擇。
異步通知的核心就是信號,在 arch/xtensa/include/uapi/asm/signal.h
文件中定義了 Linux 所支持的所有信號,這些信號如下所示:
#define SIGHUP 1 /* 終端掛起或控制進程終止 */
#define SIGINT 2 /* 終端中斷(Ctrl+C 組合鍵) */
#define SIGQUIT 3 /* 終端退出(Ctrl+\組合鍵) */
#define SIGILL 4 /* 非法指令 */
#define SIGTRAP 5 /* debug 使用,有斷點指令產生 */
#define SIGABRT 6 /* 由 abort(3)發出的退出指令 */
#define SIGIOT 6 /* IOT 指令 */
#define SIGBUS 7 /* 總線錯誤 */
#define SIGFPE 8 /* 浮點運算錯誤 */
#define SIGKILL 9 /* 殺死、終止進程 ----不可忽略 */
#define SIGUSR1 10 /* 用戶自定義信號 1 */
#define SIGSEGV 11 /* 段違例(無效的內存段) */
#define SIGUSR2 12 /* 用戶自定義信號 2 */
#define SIGPIPE 13 /* 向非讀管道寫入數據 */
#define SIGALRM 14 /* 鬧鐘 */
#define SIGTERM 15 /* 軟件終止 */
#define SIGSTKFLT 16 /* 棧異常 */
#define SIGCHLD 17 /* 子進程結束 */
#define SIGCONT 18 /* 進程繼續 */
#define SIGSTOP 19 /* 停止進程的執行,只是暫停 ----不可忽略*/#define SIGTSTP 20 /* 停止進程的運行(Ctrl+Z 組合鍵) */
#define SIGTTIN 21 /* 后臺進程需要從終端讀取數據 */
#define SIGTTOU 22 /* 后臺進程需要向終端寫數據 */
#define SIGURG 23 /* 有"緊急"數據 */
#define SIGXCPU 24 /* 超過 CPU 資源限制 */
#define SIGXFSZ 25 /* 文件大小超額 */
#define SIGVTALRM 26 /* 虛擬時鐘信號 */
#define SIGPROF 27 /* 時鐘信號描述 */
#define SIGWINCH 28 /* 窗口大小改變 */
#define SIGIO 29 /* 可以進行輸入/輸出操作 */
#define SIGPOLL SIGIO
/* #define SIGLOS 29 */
#define SIGPWR 30 /* 斷點重啟 */
#define SIGSYS 31 /* 非法的系統調用 */
#define SIGUNUSED 31 /* 未使用信號 *//* These should not be considered constants from userland. */
#define SIGRTMIN 32
#define SIGRTMAX (_NSIG-1)
除了 SIGKILL(9)和 SIGSTOP(19)這兩個信號不能被忽略外,進程能夠忽略或捕獲其他的全部信號。一個信號被捕獲的意思是當一個信號到達時有相應的代碼處理它。如果一個信號沒有被這個進程所捕獲,內核將采用默認行為處理。
3.2 信號的接收–應用端
我們使用中斷的時候需要設置中斷處理函數,同樣的,如果要在應用程序中使用信號,那么就必須設置信號所使用的信號處理函數,在應用程序中使用 signal 函數來設置指定信號的處理函數, signal 函數原型如下所示:
sighandler_t signal(int signum, sighandler_t handler)-signum:要設置處理函數的信號。
-handler: 信號的處理函數。若為SIGIGN,表示忽略該信號;若為SIGDFL,表示采用系統默認方式處理信號;若為用戶自定義的函數,則信號被捕獲到后,該函數將被執行。
-返回值: 設置成功的話返回信號的前一個處理函數handler,設置失敗的話返回 SIG_ERR。
信號處理函數原型如下所示:
typedef void (*sighandler_t)(int)
先來看一個使用信號實現異步通知的例子,它通過signal(SIGIO,input_handler)對標準輸入文件描述符STDIN_FILENO啟動信號機制。用戶輸人后,應用程序將接收到SIGIO信號其處理函數input_handler()將被調用,如代碼清單 9.2所示。
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>
#define MAX_LEN 100void input_handler(int num)
{char data[MAX_LEN];int len;/* 讀取并輸出 STDIN_FILENO 上的輸入 */len = read(STDIN_FILENO, &data, MAX_LEN);data[len] = 0;printf("input available:%s\n", data);
}int main(void)
{int oflags;/* 啟動信號驅動機制 */signal(SIGIO, input_handler);fcntl(STDIN_FILENO, F_SETOWN, getpid());oflags = fcntl(STDIN_FILENO, F_GETFL);fcntl(STDIN_FILENO, F_SETFL, oflags | FASYNC);/* 最后進入一個死循環,僅為保持進程不終止。如果程序中沒有這個死循環會立即執行完畢 */while (1);
}
上述代碼 24 行為 SIGIO 信號安裝 input_handler() 作為處理函數,第 25 行設置本進程為 STDIN_FILENO 文件的擁有者,沒有這一步,內核不會知道應該將信號發給哪個進程。而為了啟用異步通知機制,還需對設備設置 FASYNC 標志,第 26 行、27 行代碼可實現此目的。
整個程序的執行效果如下:
[root@localhost driver_study1# ./signal_test
I am Chinese.
input available: I am Chinese.
-> signal_test 程序打印I love Linux driver.
input available: I love Linux driver.
-> signal_test 程序打印
從中可以看出,當用戶輸入一串字符串后,標準輸入設備釋放 SIGIO 信號,這個信號“中斷”與驅使對應的應用程序中的 input_handler() 得以執行,并將用戶輸入顯示出來。
**由此可見,為了能在用戶空間中處理一個設備釋放的信號,它必須完成 3 項工作。**應用程序對異步通知的處理包括以下三步:
1、注冊信號處理函數
應用程序根據驅動程序所使用的信號來設置信號的處理函數,應用程序使用 signal 函數來設置信號的處理函數。
2、將本應用程序的進程號告訴給內核
使用 fcntl(fd, F_SETOWN, getpid())
將本應用程序的進程號告訴給內核。
3、開啟異步通知
使用如下兩行程序開啟異步通知:
flags = fcntl(fd, F_GETFL); /* 獲取當前的進程狀態 */
fcntl(fd, F_SETFL, flags | FASYNC); /* 開啟當前進程異步通知功能 */
重點就是通過 fcntl
函數設置進程狀態為 FASYNC
,經過這一步,驅動程序中的 fasync
函數就會執行。
3.3 信號的釋放–設備驅動端
在設備驅動和應用程序的異步通知交互中,僅僅在應用程序端捕獲信號是不夠的,因為信號的源頭在設備驅動端。因此,應該在合適的時機讓設備驅動釋放信號,在設備驅動程序中增加信號釋放的相關代碼。
為了使設備支持異步通知機制,驅動程序中涉及3項工作。
1)支持 F_SETOWN 命令:
- 能在這個控制命令處理中設置
filp->f_owner
為對應進程 ID。不過此項工作已由內核完成,設備驅動無須處理。
2)支持 F_SETFL 命令的處理:
- 每當
FASYNC
標志改變時,驅動程序中的fasync()
函數將得以執行。因此,驅動中應該實現fasync()
函數。
3)在設備資源可獲得時,調用 kill_fasync()
函數激發相應的信號。
驅動中的上述3項工作和應用程序中的3項工作是一一對應的:
設備驅動中異步通知編程比較簡單,主要用到一項數據結構和兩個函數。
數據結構是 fasync_struct
結構體:
struct fasync_struct {spinlock_t fa_lock; // 保護結構體的自旋鎖int magic; // 用于驗證結構體的魔術數字int fa_fd; // 文件描述符struct fasync_struct *fa_next; // 指向下一個結構體的指針,形成單向鏈表struct file *fa_file; // 指向文件結構體的指針struct rcu_head fa_rcu; // RCU機制使用的頭結構
};
和其他的設備驅動一樣,將 fasync_struct
結構體指針放在設備結構體中仍然是最佳選擇,支持異步通知的設備結構體模板,如下所示:
struct xxx_dev {struct cdev cdev; /* cdev 結構體 */...struct fasync_struct *async_queue; /* 異步結構體指針 */
};
- 當一個進程對某個文件調用
fcntl()
系統調用并設置 FASYNC 標志時,內核會創建一個fasync_struct
實例,并將其加入到文件的異步通知隊列中。- 當文件狀態發生變化時(如數據可讀或可寫),內核會遍歷這個隊列----上圖中的
fasync_struct
列表,找到相關的fasync_struct
,并通過文件描述符通知對應的進程。引出了**“異步通知隊列”**:接下來再進一步分析
在 Linux 內核中,異步通知隊列(由
fasync_struct
結構體組成的鏈表)是通過設備驅動程序的私有數據結構來維護的(也就是前面的struct fasync_struct *async_queue; /* 異步結構體指針 */
)如何維護異步通知隊列
- 私有數據結構:設備驅動程序通常會在其私有數據結構中維護一個指向
fasync_struct
的指針。這個私有數據結構可能是struct inode
的一部分,或者是設備驅動程序定義的其他結構體。fasync_helper
函數:當需要處理異步通知時,內核會調用fasync_helper
函數。這個函數會檢查設備驅動程序的私有數據結構中是否有指向fasync_struct
的指針,并據此決定是否需要創建或修改異步通知隊列。kill_fasync
函數:當設備驅動程序需要通知應用程序文件狀態發生變化時(如數據可讀或可寫),它會調用kill_fasync
函數。這個函數會遍歷由fasync_struct
結構體組成的鏈表,并向每個注冊的進程發送信號。
兩個函數分別是:
1)處理 FASYNC 標志變更的函數。
int (*fasync) (int fd, struct file *filp, int on);
int fasync_helper(int fd, struct file *filp, int mode, struct fasync_struct **fa);
2)釋放信號用的函數。
void kill_fasync(struct fasync_struct **fa, int sig, int band);
如果要使用異步通知,需要在設備驅動中實現 file_operations
操作集中的 fasync
函數,fasync
函數里面一般通過調用 fasync_helper
函數來初始化前面定義的 fasync_struct
結構體指針。
當應用程序通過“fcntl(fd, F_SETFL, flags | FASYNC)”
改變fasync
標記的時候,驅動程序 file_operations
操作集中的 fasync
函數就會執行。 在設備驅動的 fasync()
函數中,只需要簡單地將該函數的3個參數以及 fasync_struct
結構體指針的指針作為第4個參數傳入 fasync_helper()
函數即可。下面給出了支持異步通知的設備驅動程序 fasync()
函數的模板。
struct xxx_dev {......struct fasync_struct *async_queue; /* 異步相關結構體 */
};static int xxx_fasync(int fd, struct file *filp, int on)
{struct xxx_dev *dev = (xxx_dev *)filp->private_data;if (fasync_helper(fd, filp, on, &dev->async_queue) < 0)return -EIO;return 0;
}static struct file_operations xxx_ops = {.......fasync = xxx_fasync,......
};
在設備資源可以獲得時,應該調用 kill_fasync()
釋放 SIGIO 信號。在可讀時,第3個參數設置為 POLL_IN,在可寫時,第3個參數設置為 POLL_OUT。下面給出了釋放信號的范例。
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);...
}
最后,在文件關閉時,即在設備驅動的 release() 函數中,應調用設備驅動的 fasync() 函數將文件從異步通知的列表中刪除。給出了支持異步通知的設備驅動 release() 函數的模板。
static int xxx_release(struct inode *inode, struct file *filp)
{/* 將文件從異步通知列表中刪除 */xxx_fasync(-1, filp, 0);...return 0;
}
調用前面代碼中的
xxx_fasync
函數來完成fasync_struct
的釋放工作,但是,其最終還是通過fasync_helper
函數完成釋放工作。