@[TOC]
阻塞/非阻塞簡介阻塞操作是指在執行裝置操作時,若不能獲得資源,則掛起程序直到滿足可操作的條件後再進行操作。被掛起的程序進入睡眠狀態,被從排程器的執行佇列移走,直到等待的條件被滿足。而非阻塞操作的程序在不能進行裝置操作時,並不掛起,它要麼放棄,要麼不停地查詢,直至可以進行操作為止。
阻塞/非阻塞例程阻塞方式
int fd;int data = 0;fd = open("/dev/xxx_dev", O_RDWR); /* 阻塞方式開啟 */ret = read(fd, &data, sizeof(data)); /* 讀取資料 */
非阻塞方式
int fd;int data = 0; fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式開啟 */ ret = read(fd, &data, sizeof(data)); /* 讀取資料 */
等待佇列簡介
等待佇列是核心中一個重要的資料結構。阻塞方式訪問裝置時,如果裝置不可操作,那麼程序就會進入休眠狀態。等待佇列就是來完成程序休眠操作的一種資料結構。
等待佇列相關函式定義等待佇列wait_queue_head_t my_queue;
wait_queue_head_t是__wait_queue_head結構體的一個typedef。
初始化等待佇列頭void init_waitqueue_head(wait_queue_head_t *q)
引數q就是要初始化的等待佇列頭,也可以使用宏 DECLARE_WAIT_QUEUE_HEAD (name)來一次性完成等待佇列頭的定義的初始化。
定義並初始化一個等待佇列項DECLARE_WAITQUEUE(name, tsk)
name就是等待佇列項的名字,tsk表示這個等待佇列項屬於哪個任務程序,一般設定為current,在 Linux核心中 current相當於一個全域性變數,表示當前程序。因此宏DECLARE_WAITQUEUE就是給當前正在執行的程序建立並初始化了一個等待佇列項。
將佇列項新增到等待佇列頭void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
q:等待佇列項要加入的等待佇列頭 wait:要加入的等待佇列項 返回值:無
將佇列項從等待佇列頭移除void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait)
q:要刪除的等待佇列項所處的等待佇列頭 wait:要刪除的等待佇列項。 返回值:無
等待喚醒void wake_up(wait_queue_head_t *q) void wake_up_interruptible(wait_queue_head_t *q)
q:就是要喚醒的等待佇列頭,這兩個函式會將這個等待佇列頭中的所有程序都喚醒 wake_up函式可以喚醒處於 TASK_INTERRUPTIBLE和 TASK_UNINTERRUPTIBLE狀態的程序,而wake_ up_ interruptible函式只能喚醒處於 TASK_INTERRUPTIBLE狀態的程序
等待事件wait_event(wq, condition)
等待以wq為等待佇列頭的等待佇列被喚醒,前提是 condition條件必須滿足(為真),否則一直阻塞。此函式會將程序設定為TASK _UNINTERRUPTIBLE狀態
wait_event_timeout(wq, condition, timeout)
功能和 wait_event類似,但是此函式可以新增超時時間,以 jiffies為單位。此函式有返回值,如果返回0的話表示超時時間到,而且 condition為假。為1的話表示 condition為真,也就是條件滿足了。
wait_event_interruptible(wq, condition)
與 wait event函式類似,但是此函式將程序設定為 TASK_INTERRUPTIBLE,就是可以被訊號打斷。
wait_event_interruptible_timeout(wq, condition, timeout)
與 wait event timeout函式類似,此函式也將程序設定為 TASK_INTERRUPTIBLE,可以被訊號打斷。
輪詢當應用程式以非阻塞的方式訪問裝置時,會一遍一遍的去查詢我們的裝置是否可以訪問,這個查詢操作就叫做輪詢。核心中提供了poll,epoll,select函式來處理輪詢操作。當應用程式在上層透過poll,epoll,select函式來查詢裝置時,驅動程式中的poll,epoll,select函式就要在底層實現查詢,如果可以操作的話,就會從讀取裝置的資料或者向裝置寫入資料。
select函式原型
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
nfds:要操作的檔案描述符個數。 readifds、 writefds和 exceptfds:這三個指標指向描述符集合,這三個引數指明瞭關心哪些描述符、需要滿足哪些條件等等,這三個引數都是fd_set型別的, fd_set型別變數的每一個位都代表了一個檔案描述符。 readfds用於監視指定描述符集的讀變化,也就是監視這些檔案是否可以讀取,只要這些集合裡面有一個檔案可以讀取,那麼 seclect就會返回一個大於0的值表示檔案可以讀取。如果沒有檔案可以讀取,那麼就會根據 timeout引數來判斷是否超時。可以將 reads設定為NULL,表示不關心任何檔案的讀變化。 writefds和 reads類似,只是 writers用於監視這些檔案是否可以進行寫操作。 exceptfds用於監視這些檔案的異常 timeout:超時時間,當我們呼叫 select函式等待某些檔案描述符可以設定超時時間,超時時間使用結構體 timeval表示,結構體定義如下所示:
struct timeval { long tv_sec; /* 秒 */long tv_usec; /* 微妙 */ };
當 timeout為NULL的時候就表示無限期的等待返回值。0,表示的話就表示超時發生,但是沒有任何檔案描述符可以進行操作;-1,發生錯誤;其他值,可以進行操作的檔案描述符個數。 操作fd_set變數的函式
void FD_ZERO(fd_set *set) void FD_SET(int fd, fd_set *set) void FD_CLR(int fd, fd_set *set) int FD_ISSET(int fd, fd_set *set)
FD_ZERO用於將 fd set變數的所有位都清零, FD_SET用於將 fd_set變數的某個位置1,也就是向 fd_set新增一個檔案描述符,引數fd就是要加入的檔案描述符。 FD_CLR使用者將 fd_set變數的某個位清零,也就是將一個檔案描述符從 fd_set中刪除,引數fd就是要刪除的檔案描述符。 FD_ISSET用於測試 fd_set的某個位是否置1,也就是判斷某個檔案是否可以進行操作,引數fd就是要判斷的檔案描述符。
void main(void) { int ret, fd; /* 要監視的檔案描述符 */ fd_set readfds; /* 讀操作檔案描述符集 */ struct timeval timeout; /* 超時結構體 */ fd = open("dev_xxx", O_RDWR | O_NONBLOCK); /* 非阻塞式訪問 */ FD_ZERO(&readfds); /* 清除readfds */ FD_SET(fd, &readfds); /* 將fd新增到readfds裡面 */ /* 構造超時時間 */ timeout.tv_sec = 0; timeout.tv_usec = 500000; /* 500ms */ ret = select(fd + 1, &readfds, NULL, NULL, &timeout); switch (ret) { case 0: /* 超時 */ printf("timeout!\r\n"); break; case -1: /* 錯誤 */ printf("error!\r\n"); break; default: /* 可以讀取資料 */ if(FD_ISSET(fd, &readfds)) /* 判斷是否為fd檔案描述符 */ { /* 使用read函式讀取資料 */ } break; } }
poll在單個執行緒中, select函式能夠監視的檔案描述符數量有最大的限制,一般為1024,可以修改核心將監視的檔案描述符數量改大,但是這樣會降低效率!這個時候就可以使用poll函式, poll函式本質上和 select沒有太大的差別,但是poll函式沒有最大檔案描述符限制,Linx應用程式中poll函式原型如下所示:
int poll(struct pollfd *fds, nfds_t nfds, int timeout)
函式引數和返回值含義如下 fds:要監視的檔案描述符集合以及要監視的事件,為一個數組,陣列元素都是結構體 polled型別的, pollfd結構體如下所示
struct pollfd { int fd; /* 檔案描述符 檔案描述符 檔案描述符 */ short events; /* 請求的事件 請求的事件 請求的事件 */ short revents; /* 返回的事件 返回的事件 返回的事件 */ };
fd是要監視的檔案描述符,如果f無效的話那麼 events監視事件也就無效,並且 revents返回0。 events是要監視的事件,可監視的事件型別如下所示
POLLIN //有資料可以讀取。POLLPRI //有緊急的資料需要讀取。POLLOUT //可以寫資料POLLERR指定的檔案描述符發生錯誤POLLHUP指定的檔案描述符掛起POLLNVAL無效的請求POLLRDNORM等同於 POLLIN
revents:返回引數,也就是返回的事件,有Linux核心設定具體的返回事件。 nfds:poll函式要監視的檔案描述符數量 timeout:超時時間,單位為ms 返回值:返回 revents域中不為0的 polled結構體個數,也就是發生事件或錯誤的檔案描述符數量;0,超時;-1,發生錯誤,並且設定errno為錯誤型別
void main(void){ int ret; int fd; /* 要監視的檔案描述符 */ struct pollfd fds; fd = open(filename, O_RDWR | O_NONBLOCK); /* 非阻塞式訪問 */ /* 構造結構體 */ fds.fd = fd; fds.events = POLLIN; /* 監視資料是否可以讀取 */ ret = poll(&fds, 1, 500); /* 輪詢檔案是否可操作,超時500ms */ if (ret) { /* 資料有效 */ /* 讀取資料 */ } else if (ret == 0) { /* 超時 */ } else if (ret < 0) { /* 錯誤 */ } }
epoll傳統的 selcet和poll函式都會隨著所監聽的fd數量的增加,出現效率低下的問題,而且poll函式每次必須遍歷所有的描述符來檢查就緒的描述符,這個過程很浪費時間。為此,epoll因運而生,epoll就是為處理大併發而準備的,一般常常在網路程式設計中使用epoll函式。應用程式需要先使用 epoll_create函式建立一個 epoll控制代碼, epoll create函式原至如下.
int epoll_create(int size)
函式引數和返回值含義如下: size;從 Linux2.6.8開始此引數已經沒有意義了,隨便填寫一個大於0的值就可以 返回值:epoll控制代碼,如果為-1的話表示建立失敗,epoll控制代碼建立成功以後使用,epoll ctl函式向其中新增要監視的檔案描述符以及監視的事ct函式原型如下所示
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
函式引數和返回值含義如下 epfd;要操作的epoll控制代碼,也就是使用 epoll_create函式建立的epoll控制代碼。 p:表示要對epfd( epoll控制代碼)進行的操作,可以設定為
EPOLL CTL ADD //向印fd新增檔案引數d表示的描述符EPOLL CTL MOD修改引數fd的 event事件。EPOLL CTL DEL //從f中刪除過l描述符
fd:要監視的檔案描述 event:要監視的事件型別,為 epoll_event結構體型別指標, epoll_event結構體型別如下所
struct epoll_event { uint32_t events; /* epoll事件 */ epoll_data_t data; /* 使用者資料 使用者資料 */ };
結構體 epoll_event的 events成員變量表示要監視的事件,可選的事件如下所示
EPOLLIN //有資料可以讀取EPOLLOUT可以寫資料EPOLLPRI //有緊急的資料需要讀取EPOLLERI指定的檔案描述符發生錯誤。EPOLLHUP //指定的檔案描述符掛起POLLET設定epo為邊沿觸發,預設觸發模式為水平觸發王POLLONESHOT //一次性的監視,當監視完成以後還需要再次監視某個fd,那麼就需要將fd重新新增到 epoll 裡面
上面這些事件可以進行“或”操作,也就是說可以設定監視多個事件返回值:0,成功;-1,失敗,並且設定errno的值為相應的錯誤碼。一切都設定好以後應用程式就可以透過 epoll_wait函式來等待事件的發生,類似 select函式。 epoll_wait函式原型如下所示
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout)
函式引數和返回值含義如下 epfd:要等待的 epoll events:指向 epoll_event結構體的陣列,當有事件發生的時候Iimx核心會填寫 events,呼叫者可以根據 events判斷髮生了哪些事件。 prevents:events陣列大小,必須大於0 timeout:超時時間,單位為ms返回值:0,超時;-1,錯誤;其他值,準備就緒的檔案描述符數量。 epoll更多的是用在大規模的併發伺服器上,因為在這種場合下 select和poll並不適合。當設計到的檔案描述符(fd比較少的時候就適合用 selcet和pl本章我們就使用 sellect和poll這兩個函式
非同步通知概念阻塞與非阻塞訪問、poll函式提供了較好的解決裝置訪問的機制,但是如果有了非同步通知,整套機制則更加完整了。 非同步通知的意思是:一旦裝置就緒,則主動通知應用程式,這樣應用程式根本就不需要查詢裝置狀態,這一點非常類似於硬體上“中斷”的概念,比較準確的稱謂是“訊號驅動的非同步I/O”。訊號是在軟體層次上對中斷機制的一種模擬,在原理上,一個程序收到一個訊號與處理器收到一箇中斷請求可以說是一樣的。訊號是非同步的,一個程序不必透過任何操作來等待訊號的到達,事實上,程序也不知道訊號到底什麼時候到達。 阻塞I/O意味著一直等待裝置可訪問後再訪問,非阻塞I/O中使用poll()意味著查詢裝置是否可訪問,而非同步通知則意味著裝置通知使用者自身可訪問,之後使用者再進行I/O處理。由此可見,這幾種I/O方式可以相互補充。
Linux訊號非同步通知的核心就是訊號,在 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 /* 未使用訊號 */
非同步通知程式碼我們使用中斷的時候需要設定中斷處理函式,同樣的,如果要在應用程式中使用訊號,那麼就必須設定訊號所使用的訊號處理函式,在應用程式中使用 signal函式來設定指定訊號的處理函式, signal函式原型如下所示
void (*signal(int signum, void (*handler))(int)))(int);
該函式原型較難理解,它可以分解為:
typedef void (*sighandler_t)(int);sighandler_t signal(int signum, sighandler_t handler));
第一個引數指定訊號的值,第二個引數指定針對前面訊號值的處理函式,若為SIG_IGN,表示忽略該訊號;若為SIG_DFL,表示採用系統預設方式處理訊號;若為使用者自定義的函式,則訊號被捕獲到後,該函式將被執行。 如果signal呼叫成功,它返回最後一次為訊號signum繫結的處理函式的handler值,失敗則返回SIG_ERR。
驅動中的訊號處理fasync_struct結構體首先我們需要在驅動程式中定義個 fasync_struct結構體指標變數, 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; };
一般將 fasync_struct結構體指標變數定義到裝置結構體中,比如在xxx_dev結構體中新增一個 fasync_struct結構體指標變數,結果如下所示
struct xxx_dev { struct device *dev; struct class *cls; struct cdev cdev; ...... struct fasync_struct *async_queue; /* 非同步相關結構體 */ };
fasync函式
如果要使用非同步通知,需要在裝置驅動中實現file_ operations操作集中的 fasync函式,此函式格式如下所示:
int (*fasync) (int fd, struct file *filp, int on)
fasync函數里面一般透過呼叫 fasync_helper函式來初始化前面定義的 fasync_struct結構體指標, fasync_helper函式原型如下
int fasync_helper(int fd, struct file * filp, int on, struct fasync_struct **fapp)
fasync_helper函式的前三個引數就是 fasync函式的那三個引數,第四個引數就是要初始化的 fasync_ struct結構體指標變數。當應用程式透過結構體指標變數。當應用程式透過“ fcntl(fd, F_SETFL, flags | FASYNC)”改變fasync標記的時候,驅動程式 file_operations操作集中的 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, ...... };
在關閉驅動檔案的時候需要在file_ operations操作集中的 release函式中釋放 fasyn_fasync struct的釋放函式同樣為 fasync_helper, release函式引數參考例項如下
static int xxx_release(struct inode *inode, struct file *filp) { return xxx_fasync(-1, filp, 0); /* 刪除非同步通知 */ }
static struct file_operations xxx_ops = { ...... .release = xxx_release, };
第3行透過呼叫示例程式碼 xxx_fasync函式來完成 fasync_struct的釋放工作,但是,其最終還是透過 fasync_helper函式完成釋放工作。
kill_fasync函式當裝置可以訪問的時候,驅動程式需要嚮應用程式發出訊號,相當於產生“中斷” kill_fasync函式負責傳送指定的訊號, kill_fasync函式原型如下所示
void kill_fasync(struct fasync_struct **fp, int sig, int band)
函式引數和返回值含義如下: fasync struct 要操作的檔案指標 sig:要傳送的訊號 band:可讀時設定為 POLL IN,可寫時設定為 POLL OUT。 返回值:無。
應用程式對非同步通知的處理應用程式對非同步通知的處理包括以下三步 1、註冊訊號處理函式應用程式根據驅動程式所使用的訊號來設定訊號的處理函式,應用程式使用 signal函式來設定訊號的處理函式。前面已經詳細的講過了,這裡就不細講了。 2、將本應用程式的程序號告訴給核心使用fcntl(fd, F_SETOWN, getpid)將本應用程式的程序號告訴給核心 3、開啟非同步通知使用如下兩行程式開啟非同步通知:
flags = fcntl(fd, F_GETFL); /* 獲取當前的程序狀態*/ fcntl(fd, F_SETFL, flags | FASYNC); /* 開啟當前程序非同步通知功能 */
重點就是透過 fcntl函式設定程序狀態為 FASYNC,經過這一步,驅動程式中的 fasync函式就會執行。