Linux上可以使用不同的I/O模型,我們可以通過下圖瞭解常用的I/O模型:同步和異步模型,以及阻塞和非阻塞模型,本文主要分析其中的異步阻塞模型。
一、select使用
這個模型中配置的是非阻塞I/O,然後使用阻塞select系統調用來確定一個I/O描述符何時有操作。使用select調用可以爲多個描述符提供通知,對於每個提示符,我們可以請求描述符的可寫,可讀以及是否發生錯誤。異步阻塞I/O的系統流程如下圖所示:
使用select常用的幾個函數如下:
FD_ZERO(int fd, fd_set* fds)
FD_SET(int fd, fd_set* fds)
FD_ISSET(int fd, fd_set* fds)
FD_CLR(int fd, fd_set* fds)
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout)
fd_set類型可以簡單的理解爲按bit位標記句柄的隊列。具體的置位、驗證可以使用FD_SET,FD_ISSET等宏實現。在select函數中,readfds、writefds和exceptfds同時作爲輸入參數和輸出參數,如果readfds標記了一個位置,則,select將檢測到該標記位可讀。timeout爲設置的超時時間。
下面我們來看如何使用select:
SOCKADDR_IN addrSrv;
int reuse = 1;
SOCKET sockSrv,connsock;
SOCKADDR_IN addrClient;
pool pool;
int len=sizeof(SOCKADDR);
/*創建TCP*/
sockSrv=socket(AF_INET,SOCK_STREAM,0);
/*地址、端口的綁定*/
addrSrv.sin_addr.S_un.S_addr=htonl(INADDR_ANY);
addrSrv.sin_family=AF_INET;
addrSrv.sin_port=htons(port);
if(bind(sockSrv,(SOCKADDR*)&addrSrv,sizeof(SOCKADDR))<0)
{
fprintf(stderr,"Failed to bind");
return ;
}
if(listen(sockSrv,5)<0)
{
fprintf(stderr,"Failed to listen socket");
return ;
}
setsockopt(sockSrv,SOL_SOCKET,SO_REUSEADDR,(const char*)&reuse,sizeof(reuse));
init_pool(sockSrv,&pool);
while(1)
{
/*通過selete設置爲異步模式*/
pool.ready_set=pool.read_set;
pool.nready=select(pool.maxfd+1,&pool.ready_set,NULL,NULL,NULL);
if(FD_ISSET(sockSrv,&pool.ready_set))
{
connsock=accept(sockSrv,(SOCKADDR *)&addrClient,&len);
//loadDeal()/*連接處理*/
//printf("test\n");
add_client(connsock,&pool);//添加到連接池
}
/*檢查是否有事件發生*/
check_client(&pool);
}
上面是一個服務器代碼的關鍵部分,設置爲異步的模式,然後接受到連接將其添加到連接池中。監聽描述符上使用select,接受客戶端的連接請求,在check_client函數中,遍歷連接池中的描述符,檢查是否有事件發生。
二、poll使用
poll函數類似於select,但是其調用形式不同。poll不是爲每個條件構造一個描述符集,而是構造一個pollfd結構體數組,每個數組元素指定一個描述符標號及其所關心的條件。定義如下:
#include <sys/poll.h>
int poll (struct pollfd *fds, unsigned int nfds, int timeout);
struct pollfd {
int fd; /* file descriptor */
short events; /* requested events to watch */
short revents; /* returned events witnessed */
};
每個結構體的events域是由用戶來設置,告訴內核我們關注的是什麼,而revents域是返回時內核設置的,以說明對該描述符發生了什麼事件。這點與select不同,select修改其參數以指示哪一個描述符準備好了。在《unix環境高級編程》中有一張events取值的表,如下:
POLLIN :可讀除高優級外的數據,不阻塞
POLLRDNORM:可讀普通數據,不阻塞
POLLRDBAND:可讀O優先數據,不阻塞
POLLPRI:可讀高優先數據,不阻塞
POLLOUT :可寫普數據,不阻塞
POLLWRNORM:與POLLOUT相同
POLLWRBAND:寫非0優先數據,不阻塞
其次revents還有下面取值
POLLERR :已出錯
POLLHUP:已掛起,當以描述符被掛起後,就不能再寫向該描述符,但是仍可以從該描述符讀取到數據。
POLLNVAL:此描述符並不引用一打開文件
對poll函數,nfds表示fds中的元素數,timeout爲超時設置,單位爲毫秒若爲0,表示不等待,爲-1表示描述符中一個已經準備好或捕捉到一個信號返回,大於0表示描述符準備好,或超時返回。函數返回值返回值若爲0,表示沒有事件發生,-1表示錯誤,並設置errno,大於0表示有幾個描述符有事件。
poll的使用和select基本類似。在此不再介紹。poll相對於是select的優勢是監聽的描述符數量沒有限制。
三、epoll學習
epoll有兩種模式,Edge Triggered(簡稱ET) 和 Level Triggered(簡稱LT).在採用這兩種模式時要注意的是,如果採用ET模式,那麼僅當狀態發生變化時纔會通知,而採用LT模式類似於原來的select/poll操作,只要還有沒有處理的事件就會一直通知.
1)epoll數據結構介紹:
typedef union epoll_data
{
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
struct epoll_event
{
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
常見的事件如下:
EPOLLIN:表示對描述符的可以讀
EPOLLOUT:表示對描述符的可以寫
EPOLLPRI:表示對描述符的有緊急數據可以讀
EPOLLERR:發生錯誤
EPOLLHUP:掛起
EPOLLET:邊緣觸發
EPOLLONESHOT:一次性使用,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裏
2)函數介紹
epoll的三個函數
int epoll_creae(int size);
功能:該函數生成一個epoll專用的文件描述符
參數:size爲epoll上能關注的最大描述符數
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能:用於控制某個epoll文件描述符時間,可以註冊、修改、刪除
參數:epfd由epoll_create生成的epoll專用描述符
op操作:EPOLL_CTL_ADD 註冊 EPOLL_CTL_MOD修改 EPOLL_DEL刪除
fd:關聯的文件描述符
evnet告訴內核要監聽什麼事件
int epoll_wait(int epfd,struct epoll_event*events,int maxevents,int timeout);
功能:該函數等待i/o事件的發生。
參數:epfd要檢測的句柄
events:用於回傳待處理時間的數組
maxevents:告訴內核這個events有多大,不能超過之前的size
timeout:爲超時時間
使用方法參考:https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/epoll-example.c
epoll支持的FD上限是最大可以打開文件的數目(select面臨這樣的問題),IO效率不隨FD數目增加而線性下降(select、poll面臨的問題)使用mmap加速內核與用戶空間的消息傳遞。現在libevent封裝了幾種的實現,可以通過使用libevent來實現多路複用。
本文參考:https://banu.com/blog/2/how-to-use-epoll-a-complete-example-in-c/
http://www.ibm.com/developerworks/cn/linux/l-cn-edntwk/index.html?ca=drs-
http://www.ibm.com/developerworks/cn/linux/l-async/