阻塞與非阻塞是設備訪問的兩種方式。在寫阻塞與非阻塞的驅動程序時,經常用到等待隊列。
阻塞調用是指調用結果返回之前,當前線程會被掛起,函數只有在得到結果之後才會返回。
非阻塞指不能立刻得到結果之前,該函數不會阻塞當前進程,而會立刻返回。
函數是否處於阻塞模式和驅動對應函數中的實現機制是直接相關的,但並不是一一對應的,例如我們在應用層設置為阻塞模式,如果驅動中沒有實現阻塞,函數仍然沒有阻塞功能。
在linux設備驅動程序中,阻塞進程可以使用等待隊列來實現。
在內核中,等待隊列是有很多用處的,尤其是在中斷處理,進程同步,定時等場合,可以使用等待隊列實現阻塞進程的喚醒。它以隊列為基礎數據結構,與進程調度機制緊密結合,能夠用於實現內核中的異步事件通知機制,同步對系統資源的訪問。
等待隊列的使用
(1)定義和初始化等待隊列:
wait_queue_head_t wait;//定義等待隊列 init_waitqueue_head(&wait);//初始化等待隊列
定義並初始化等待隊列:
#define DECLARE_WAIT_QUEUE_HEAD(name) wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
(2)添加或移除等待隊列:
void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);//將等待隊列元素wait添加到等待隊列頭q所指向的等待隊列鏈表中。 void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
(3)等待事件:
wait_event(wq, condition);//在等待隊列中睡眠直到condition為真。 wait_event_timeout(wq, condition, timeout); wait_event_interruptible(wq, condition) ; wait_event_interruptible_timeout(wq, condition, timeout) ; /* *queue:作為等待隊列頭的等待隊列被喚醒 * conditon:必須滿足,否則阻塞 * timeout和conditon相比,有更高優先級 */
(4)睡眠:
sleep_on(wait_queue_head_t *q); interruptible_sleep_on(wait_queue_head_t *q); /* sleep_on作用是把目前進程的狀態置成TASK_UNINTERRUPTIBLE,直到資源可用,q引導的等待隊列被喚醒。 interruptible_sleep_on作用是一樣的, 只不過它把進程狀態置為TASK_INTERRUPTIBLE */
(5)喚醒等待隊列:
//可喚醒處於TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE狀態的進程; #define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL)
//只能喚醒處於TASK_INTERRUPTIBLE狀態的進程 #define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL)
掛起線程的意思是在線程中主動使用調度函數,將自己掛起,何時在執行需要等待系統調度實現,無法預知,可能在執行完調度函數後,內核馬上又調度回來,繼續運行。
使線程睡眠的意思是在線程中使用能夠睡眠的語句,然後讓線程進入睡眠狀態,讓出cpu,直到滿足要求的事情發生了再喚醒線程繼續執行,比如睡眠定時時間到達,或者睡眠被打斷,或者睡眠等待的資源得到時。
線程阻塞有點是不一定會發生的意思,比如在運行函數的時候執行到摸一個位置需要一個資源,如果這個資源可用就繼續執行,如果這個資源不可用程序會進行睡眠並等待資源可用時在喚醒。這與線程主動睡眠的區別就是是否阻塞睡眠需要根據一定的條件進行。
阻塞操作是指在執行設備操作時若不能獲得資源則掛起進程,直到滿足可操作的條件後在進行操作。
非阻塞操作的進程在不能進行設備操作時並不掛起,它或者被放棄,或者不停的查詢,直到可以進行操作為止。
在簡單字符設備驅動, 我們看到如何實現read和write方法。但是我們僅僅實現了阻塞的方式,也就是我們的應用程序不論如何設置,我們的驅動只支持阻塞方式。
在驅動中如何知道應用程序的設置呢?驅動中使用的是read或者write函數的參數struct file中的f_flags標志判斷應用程序是否設置了非阻塞。
判斷代碼片段如下:
if (file->f_flags & O_NONBLOCK) /* 非 阻塞操作 */ { if (!ev_press) /* ev_press 為 1 表示有按鍵按下,為 0 if 成立 ,沒有按鍵按下, */ return -EAGAIN; /* 返回 -EAGAIN 讓再次來執行 */ } else /* 阻塞操作 */ { /* 如果沒有按鍵動作, 休眠 */ wait_event_interruptible(button_waitq, ev_press); }
函數原型如下
static unsigned int poll(struct file *file, struct socket *sock, poll_table *wait)
第一個參數是file結構體指針,第三個參數是輪詢表指針
這個函數應該進行兩項工作
(1)對可能引起設備文件狀態變化的等待隊列調用poll_wait()函數,將對應的等待隊列頭添加到poll_table
(2)返回表示是否能對設備進行無阻塞讀,寫訪問的掩碼
poll_wait()函數原型如下
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
從中可以看出是將等待隊列頭wait_address添加到p所指向的結構體中(poll_table)
驅動函數中的poll()函數典型模板如下
static unsigned int xxx_poll(struct file *filp,struct socket *sock, poll_table *wait) { unsigned int mask = 0; struct xxx_dev *dev = filp->private_data;//獲得設備結構體指針 ... poll_wait(filp,&dev->r_wait,wait);//加讀等待隊列頭到poll_table poll_wait(filp,&dev->w_wait,wait);//加寫等待隊列頭到poll_table ... if(...)//可讀 mask |= POLLIN | POLLRDNORM; if(...)//可寫 mask |= POLLOUT | POLLRDNORM; ... return mask; }
驅動程序中的poll函數,在應用程序中對應著select、poll、epoll函數。
1)將要監控的文件添加到文件描述符集
2)調用select開始監控
3)判斷文件是否發生變化
系統提供了4個宏對描述符集進行操作:
#includeVoid FD_SET(int fd, fd_set *fdset) Void FD_CLR(int fd, fd_set *fdset) Void FD_ZERO(fd_set *fdset) Void FD_ISSET(int fd, fd_set *fdset)
宏FD_SET將文件描述符fd添加到文件描述符fdset中
宏FD_CLR從文件描述符集fdset中清除文件描述符fd
宏FD_ZERO清空文件描述符集fdset
在調用select後使用FD_ISSET來檢測文件描述符集fdset中的文件fd發生了變化
使用例子(對兩個文件進行讀監控):
FD_ZERO(&fds);//清空集合 FD_SET(fd1,&fds);//設置描述符 FD_SET(fd2,&fds);//設置描述符 Maxfdp = fd1+1;//描述符最大值加1,假設fd1>fd2 Switch(select(maxfdp,&fds,NULL,NULL,&timeout))//讀監控 Case -1: exit(-1);break;//select錯誤,退出程序 Case 0:break; Default: If(FD_ISSET(fd1,&fds)) //測試fd1是否可讀
select的幾大缺點:
(1)每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大
(2)同時每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
(3)select支持的文件描述符數量太小了,默認是1024
poll的實現和select非常相似,只是描述fd集合的方式不同,poll使用pollfd結構而不是select的fd_set結構,其他的都差不多。
epoll既然是對select和poll的改進,就應該能避免上述的三個缺點。那epoll都是怎麼解決的呢?在此之前,我們先看一下epoll和select和poll的調用接口上的不同,select和poll都只提供了一個函數——select或者poll函數。而epoll提供了三個函數,epoll_create,epoll_ctl和epoll_wait,epoll_create是創建一個epoll句柄;epoll_ctl是注冊要監聽的事件類型;epoll_wait則是等待事件的產生。
對於第一個缺點,epoll的解決方案在epoll_ctl函數中。每次注冊新的事件到epoll句柄中時(在epoll_ctl中指定EPOLL_CTL_ADD),會把所有的fd拷貝進內核,而不是在epoll_wait的時候重復拷貝。epoll保證了每個fd在整個過程中只會拷貝一次。
對於第二個缺點,epoll的解決方案不像select或poll一樣每次都把current輪流加入fd對應的設備等待隊列中,而只在epoll_ctl時把current掛一遍(這一遍必不可少)並為每個fd指定一個回調函數,當設備就緒,喚醒等待隊列上的等待者時,就會調用這個回調函數,而這個回調函數會把就緒的fd加入一個就緒鏈表)。epoll_wait的工作實際上就是在這個就緒鏈表中查看有沒有就緒的fd(利用schedule_timeout()實現睡一會,判斷一會的效果,和select實現中的第7步是類似的)。
對於第三個缺點,epoll沒有這個限制,它所支持的FD上限是最大可以打開文件的數目,這個數字一般遠大於2048,舉個例子,在1GB內存的機器上大約是10萬左右,具體數目可以cat /proc/sys/fs/file-max察看,一般來說這個數目和系統內存關系很大。
(1)select,poll實現需要自己不斷輪詢所有fd集合,直到設備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設備就緒時,調用回調函數,把就緒fd放入就緒鏈表中,並喚醒在epoll_wait中進入睡眠的進程。雖然都要睡眠和交替,但是select和poll在“醒著”的時候要遍歷整個fd集合,而epoll在“醒著”的時候只要判斷一下就緒鏈表是否為空就行了,這節省了大量的CPU時間。這就是回調機制帶來的性能提升。
(2)select,poll每次調用都要把fd集合從用戶態往內核態拷貝一次,並且要把current往設備等待隊列中掛一次,而epoll只要一次拷貝,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這裡的等待隊列並不是設備等待隊列,只是一個epoll內部定義的等待隊列)。這也能節省不少的開銷。
參考資料:
http://www.cnblogs.com/apprentice89/archive/2013/05/09/3070051.html
Linux/2012-05/59873p3.htm" target="_blank">http://www.linuxidc.com/Linux/2012-05/59873p3.htm
http://xingyunbaijunwei.blog.163.com/blog/static/76538067201241685556302/
http://blog.csdn.net/kkxgx/article/details/7717125
https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/epoll-example.c
異步通知是,一旦設備就緒,則主動向應用程序發送信號,應用程序根本就不需要查詢設備狀態,類似於中斷的概念,一個進程收到一個信號與處理器收到一個中斷請求可以說是一樣的。信號是異步的,一個進程不必通過任何操作來等待信號的到達。
在linux中,異步通知是使用信號來實現的,而在linux,大概有30種信號,比如大家熟悉的ctrl+c的SIGINT信號,進程能夠忽略或者捕獲除過SIGSTOP和SIGKILL的全部信號,當信號背捕獲以後,有相應的函數來處理它。
在驅動程序可以將特定信號發送到特定應用進程中。因此,應該在合適的時候讓設備驅動發送信號,驅動中應實現fasync()函數。並在設備資源可獲得時,調用kill_fasync()函數激發相應的信號。
驅動中實現異步通知機制很簡單。首先說fasync()函數的實現。
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); }
這個就是標准模板。
然後在需要發送信號的地方調用 kill_fasync()函數,釋放信號。
釋放信號的函數:
void kill_fasync(struct fasync_struct **fp, int sig, int band)
下面我們來看下支持異步通知的模板。
設備結構體:
struct xxx_dev{ struct cdev cdev; ...... struct fasync_struct *async_queue; }; fasync()函數: 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); }
在設備資源可以獲得時,應該調用kill_fasync()釋放SIGIO信號,可讀時第三個參數是POLL_IN,可寫時為POLL_OUT.
static ssize_t xxx_write(struct file *filp,const char __user *buf,size_t count,loff_t *ppos) { struct xxx_dev *dev = filp->private_data; ...... if(dev->async_queue) kill_fasync(&dev->async_queue,SIGIO,POLL_IN); ...... }
在release函數中,應調用fasync()函數將文件從異步通知的列表中刪除。
int xxx_release(struct inode *inode,struct file *filp) { xxx_fasync(-1,filp,0); return 0; }
在應用程序中需要3步將信號與驅動綁定。
1、注冊 SIGIO信號
signal(SIGIO, handler);
2、設置進程為文件的屬主
fcntl(fd, F_SETOWN, getpid());
3、設置異步屬性
int flags;
flags = fcntl(fd, F_GETFL);
flags |= FASYNC;
fcntl(fd, F_SETFL, flags);
然後當驅動發送信號的時候就會自動調用應用程序的信號處理函數。
應用程序模板:
void input_handler(int num) { …… } main() { 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); }
阻塞與非阻塞操作
(1)定義並初始化等待對列頭;
(2)定義並初始化等待隊列;
(3)把等待隊列添加到等待隊列頭
(4)設置進程狀態(TASK_INTERRUPTIBLE(可以被信號打斷)和TASK_UNINTERRUPTIBLE(不能被信號打斷))
(5)調用其它進程
poll機制
(1)把等待隊列頭加到poll_table
(2)返回表示是否能對設備進行無阻塞讀,寫訪問的掩碼
異步通知機制
(1)當發出 F_SETOWN,什麼都沒發生,除了一個值被賦值給filp->f_owner.
(2)當 F_SETFL被執行來打開FASYNC,驅動的fasync方法被調用.這個方法被調用無論何時FASYNC的值在filp->f_flags中被改變來通知驅動這個變化,因此它可正確地響應.這個標志在文件被打開時缺省地被清除.我們將看這個驅動方法的標准實現,在本節.
(3)當數據到達,所有的注冊異步通知的進程必須被發出一個SIGIO信號.