非阻塞IO
非阻塞式IO則會輪詢等待知道設備資源可以使用或者直接放棄
如果用戶以非阻塞方式訪問設備
提供輪詢的處理方式
可以通過select、epoll、poll函數來查詢設備是否可以操作
到應用程序調用以上三個函數 驅動中的poll函數就會執行,我們在驅動中需要編寫poll函數
三種IO多路複用的機制比較
select,poll,epoll都是IO多路複用的機制。I/O多路複用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。但select,poll,epoll本質上都是同步I/O,因爲他們都需要在讀寫事件就緒後自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。
select
int select(int nfds,
fd_set *readfds,
fd_set *writefds,
fd_set *exceptfds,
struct timeval *timeout
)
- nfds要操作的文件描述符
- readfds、writefds、exceptfds三個指針指向描述符集合,都是fd_set類型的,fd_set每一位代表了一個文件描述符,readfds用於監視指定描述符的讀變化,只要可以讀取,select返回一個大於0的值,如果不可以根據timeout來判斷超時,可以設置爲0表示不關心任何文件的讀變化。
writefds、exceptfds類似
我們可以定義一個fd_set變量,這個變量要傳遞給readfds
可以用一些宏來進行操作
void FD_ZERO(fd_set *set) //清零
void FD_SET(int fd, fd_set *set) //某位置1,添加一個文件描述符
void FD_CLR(int fd, fd_set *set) //清除一個文件描述符
int FD_ISSET(int fd, fd_set *set)//判斷某位是否爲1
- timeout超時值
struct timeval { long tv_sec; /* 秒 */
long tv_usec; /* 微妙 */
};
select的實現過程如下圖
select的幾大缺點:
-
每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大
-
同時每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
-
select支持的文件描述符數量太小了,默認是1024
poll
poll函數沒有最大文件描述符限制
int poll(struct pollfd *fds,
nfds_t nfds,
int timeout
)
- fds要監視的文件描述符集合以及要監視的時間,爲一個數組,數組元素是結構體pollfd類型
struct pollfd { int fd; /* 文件描述符 */
short events; /* 請求的事件 */
short revents; /* 返回的事件 */
};
fd爲要監視的文件描述符,events是要監視的事件,可監視的事件類型如下:
revents爲返回的事件
- nfd是poll函數要監視的文件描述符數量
- timeout爲超時時間,單位爲ms
- 返回值:返回 revents域中不爲 0的 pollfd結構體個數,也就是發生事件或錯誤的文件描述符數量; 0,超時 ;-1,發生錯誤,並且設置 errno爲錯誤類型。
epoll
傳統的select和poll函數都會隨着監聽的fd的數量的增加出現效率低下的問題
poll函數每次必須遍歷所有的文件描述符來檢查就緒的描述符,浪費時間
epoll就是爲了處理大併發而準備的,一般常用於網絡編程中
應用程序需要先使用epoll_creat函數創建一個epoll句柄
int epoll_create(int size)
size從2.6.8開始就沒有意義了 隨便寫一個大於0的值就可以
返回值爲epoll句柄返回-1表示創建失敗
句柄創建成功以後使用 epoll_ctl函數向其中添加要監視的文件描述符以及監視的事件
int epoll_ctl(int epfd,
int op,
int fd,
struct epoll_event *event
)
- epfd 要操作的epoll句柄,也就是使用epoll函數創建的句柄
- op 表示要對epfd進行的操作,可以爲
- fd 要監視的文件描述符
- event 要監視的事件類型,爲epoll_event結構體類型指針
struct epoll_event { uint32_t events; /* epoll事件 */
epoll_data_t data; /* 用戶數據 */
};
events爲要監視的事件,可選
EPOLLIN 有數據可以讀取。
EPOLLOUT 可以寫數據。
EPOLLPRI 有緊急的數據需要讀取。
EPOLLERR 指定的文件描述符發生錯誤。
EPOLLHUP 指定的文件描述符掛起。
EPOLLET 設置 epoll爲邊沿觸發,默認觸發模式爲水平觸發。
EPOLLONESHOT 一次性的監視,當監視完成以後還需要再次監視某個 fd,那麼就需要將fd重新添加到 epoll裏面。
一切設置好後可以通過epoll_wait函數來等待事件的發生,類似於select函數
int epoll_wait(int epfd,
struct epoll_event *events,
int maxevents,
int timeout
)
- epfd 要等待的epoll
- events 指向epoll_event結構體數組,當有事件發生的時候,Linux內核會填寫events數組,調用者可以根據events判斷髮生了哪些事件
- maxevents events結構體數組大小
- timeout 超時時間,單位ms
三者總結
一般來說當涉及的fd數量較少的時候,使用select是合適的;如果涉及的fd很多,如在大規模併發服務器中偵聽許多socket的時候,則不太適合選用select,而時候選用epoll
驅動中的poll操作函數
函數原型
unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait);
- filp 要打開的設備文件(文件描述符)
- wait 結構體 poll_table_struct類型指針,又應用程序傳遞進來的。一般將次參數傳遞給poll_wait函數
- 返回值:
POLLIN 有數據可以讀取。
POLLPRI 有緊急的數據需要讀取。
POLLOUT 可以寫數據。
POLLERR 指定的文件描述符發生錯誤。
POLLHUP 指定的文件描述符掛起。
POLLNVAL 無效的請求。
POLLRDNORM 等同於 POLLIN,普通數據可讀
我們需要在驅動程序的poll函數中調用poll_wait函數,poll_wait函數不會引起阻塞,只是將應用程序添加到poll_table中
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p);
參數 wait_address是要添加到 poll_table中的等待隊列頭
參數 p就是poll_table,就是file_operations中 poll函數的 wait參數。
實驗代碼與分析
實驗代碼
驅動代碼
unsigned int key_poll(struct file *filp, struct poll_table_struct *wait)
{
unsigned int mask = 0;
struct irqkey_dev *dev = (struct irqkey_dev *)filp->private_data;
poll_wait(filp, &dev->r_wait, wait);
if(atomic_read(&dev->releasekey))//按鍵按下
{
mask = POLLIN | POLLRDNORM; //返回PLLIN
}
return mask;
}
static struct file_operations key_fops = {
.owner = THIS_MODULE,
.open = key_open,
.read = key_read,
.write = key_write,
.release = key_release,
.poll = key_poll,
};
應用程序代碼
while (1)
{
FD_ZERO(&readfds);
FD_SET(fd, &readfds);
/* 構造超時時間 */
timeout.tv_sec = 0;
timeout.tv_usec = 500000; /* 500ms */
retvalue = select(fd + 1, &readfds, NULL, NULL, &timeout);
switch (retvalue) {
case 0: /* 超時 */
/* 用戶自定義超時處理 */
break;
case -1: /* 錯誤 */
/* 用戶自定義錯誤處理 */
break;
default: /* 可以讀取數據 */
if(FD_ISSET(fd, &readfds)) {
retvalue = read(fd, &keyvalue, sizeof(keyvalue));
if (retvalue < 0) {
/* 讀取錯誤 */
} else {
if (keyvalue == KEYVALUE)
printf("key value=%d\r\n", keyvalue);
}
}
break;
}
}
代碼分析
驅動部分
驅動中的工作很簡單,在file_operations的poll函數中調用poll_wait函數,把當前進程添加到wait參數指定的等待列表(poll_table)中,實際作用是讓喚醒參數queue對應的等待隊列可以喚醒因select而睡眠的進程
同時poll函數還要返回設備資源的可獲取狀態,這裏當按鍵被按下時會返回數據可讀的標誌
應用程序部分
在應用程序的while(1)循環中調用select函數來監控按鍵的狀態,然後就會阻塞等待文件描述符集合超時或者可訪問
當返回的值表示可以讀取數據時,我們開始讀取鍵值,首先使用FD_ISSET宏來判斷是否確實可讀,接着調用read函數從驅動中讀取鍵值
通過以上的操作我們就實現了使用輪詢操作來讀取鍵值
總結
阻塞與非阻塞訪問是IO操作的兩種不同的模式,前者在暫時不可以進行IO操作時會讓進程睡眠,後者則不然
在設備驅動中阻塞IO一般基於等待隊列或者基於等待隊列的其他Linux內核API來實現,等待隊列可用於同步驅動中事件發生的先後順序。使用非阻塞IO的應用程序可以藉助輪詢函數來查詢設備是否能立即被訪問,用戶空間調用select()、poll()或者epoll()接口,設備驅動提供poll()函數,設備驅動的poll()函數不會阻塞,但是與select()、poll()、epoll()相關的系統調用則會阻塞的等待至少一個文件描述符集合可訪問或超時。