基于epoll的Linux并發服務器
時間:2018-09-26 來源:未知
基于網絡的不同主機之間通信、我們經常使用socket(套接字)來實現,socket就是用于通信的endpoint(端點)。當我們基于socket發送或接受數據時,由于各種原因可能無法將數據發送出去,或者無法接收到數據。這時我們是選擇等待、還是立即返回。如果一直等待,我們稱之為阻塞I/O;立即返回,則稱之為非阻塞I/O。對于Linux系統,socket默認是阻塞的。一個典型的阻塞I/O如下圖:

當進程調用read系統調用時,由于沒有數據可讀,內核需要準備數據。當準備好數據、內核又需要將數據從內核空間拷貝到戶用空間。在此期間,應用程序一直在read函數上阻塞等待,直到數據到達。當有多個I/O同時讀寫時,阻塞I/O模式經常因為當前I/O不可讀寫而影響其他I/O的數據傳輸,使效率大大降低。如果使用非阻塞I/O就可以避免這種情況出現。但是如果采用非阻塞模式實現多I/O通信、就必須輪詢嘗試讀寫每一個I/O,而一些I/O并不可讀寫,白白浪費了時間,效率也受到影響。如圖:

當用戶進程調用read函數時,如果內核中的數據還沒有準備好,那么進程并不會阻塞,而是立刻返回錯誤,錯誤碼可能是EWOULDBLOCK,表示數據沒有準備好,于是可以選擇重新read。一旦內核中的數據準備好,并且用戶此時發起read操作,那么它馬上就將數據拷貝到了用戶空間內存,然后返回。
不停的檢測I/O是否可讀寫降低了系統效率,做了大量的無用功。幸運的是,有這樣一種機制,可以監視多個I/O,一旦某個I/O就緒(可讀或可寫),就能夠通知程序進行相應的讀寫操作,這種機制稱為I/O多路復用。實現這種機制Linux提供了select,poll和epoll函數。
select函數將I/O事件劃分成可讀,可寫和異常,而有時即使通知用戶可讀寫,但實際卻沒有數據,相當于誤報。另外,select監視的多路I/O文件描述符集合、同時也用來保存返回結果,也就是保存哪些描述符可讀、可寫或發生異常。這就導致我們總要不停的構造監視的I/O文件描述符集合。所以select監視多路I/O的效率不高,也不能監視太多的I/O,一般不應該超過FD_SETSIZE(默認為1024)個I/O。當返回結果后,還要檢查哪些描述符在返回的可讀,可寫或發生異常集中。
poll函數針對select函數的缺陷做了一些改進,將監視的文件描述符與監視的返回結果(哪些可讀寫)分開,所以poll不需要重新構造監視的I/O文件描述符集合。并且使用位圖表示更多的事件。但是poll對select的改進是有限的,都需要向內核拷貝需要監視的I/O文件描述符集合,也需要查看全部描述符,檢查哪些描述符已經可讀,可寫或發生異常等。雖然poll沒有描述符個數限制,但文件描述符太多會導致效率下降。
當客戶端連接大量增加時,select和poll的效率下降比較多。而且很多時候我們并不僅僅關心哪些描述符可讀寫,很可能是和該描述符關聯的其他任務要處理,而我們只通過文件描述符很難找到關聯的其他任務。epoll函數解決了select/poll函數的上述缺陷。它不需要向內核拷貝監視的文件描述符集合,當某些文件描述符就緒時、也不需要輪詢檢查哪些描述符可讀寫。當文件描述符可讀寫時,內核直接返回可讀寫的文件描述符,以及其相關聯的對象指針。這在實際程序中非常有意義。但epoll是Linux內核2.5.44引入,只在Linux系統上才可以使用。
epoll提供三個系統調用,分別如下:
1.int epoll_create(int size)
創建一個epoll的句柄。size用來告訴內核需要監聽的數目一共有多大,自從linux2.6.8之后,size參數是被忽略的,內核動態覺得size大小。需要注意的是,當創建好epoll句柄后,它就是會占用一個fd值,在使用完epoll后,必須調用close()關閉。
2.int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event)
epoll的事件注冊函數,它不同于select()是在監聽事件時告訴內核要監聽什么類型的事件,而是在這里先注冊要監聽的事件類型。
epfd參數是文件描述符,用來引用關聯的epoll實例。op表示要完成的操作,分別為:
EPOLL_CTL_ADD,添加新的文件描述符到epoll實例;
EPOLL_CTL_MOD,修改已經注冊的文件描述符的監聽事件;
EPOLL_CTL_DEL,刪除一個文件描述符;
fd就是要操作的文件描述符,event參數告訴內核需要監聽什么事,epoll_event結構如下:
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 */
};
Events是一個32位整數,每一位表示一種事件。用一下宏表示:
EPOLLIN ,表示對應的文件描述符可讀(包括對端SOCKET正常關閉);
EPOLLOUT,表示對應的文件描述符可寫;
EPOLLPRI,表示對應的文件描述符有緊急的數據可讀;
EPOLLERR,表示對應的文件描述符發生錯誤;
EPOLLHUP,表示對應的文件描述符被掛斷;
EPOLLET,將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對于水平觸發(Level Triggered)來說的。
EPOLLONESHOT,只監聽一次事件,當監聽完這次事件之后,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到epoll實例中。
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待epfd指向的epoll實例上發生關心的事件。參數events是分配好的epoll_event結構體數組,epoll將會把發生的事件賦值到events數組中。maxevents告之內核這個events有多大,這個maxevents的值不能大于創建epoll_create()時的size,參數timeout是超時時間,單位是毫秒,如果該值為0,即使沒有事件發生也會立即返回,-1表示一直阻塞到有事件發生。如果函數調用成功,返回對應I/O上已準備好的文件描述符數目,如返回0表示已超時。
epoll的man手冊上有一個比較好的例子可以參考,代碼如下:
#define MAX_EVENTS 10
structepoll_eventev, events[MAX_EVENTS];
intlisten_sock, conn_sock, nfds, epollfd;
/* Set up listening socket, 'listen_sock' (socket(),
* bind(), listen()) */
/* 創建epoll實例 */
epollfd = epoll_create(10);
if (epollfd == -1) {
perror("epoll_create");
exit(EXIT_FAILURE);
}
/* 將socket創建的監聽描述符listen_sock添加到epoll實例,以便監聽EPOLLIN事件 */
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) {
perror("epoll_ctl: listen_sock");
exit(EXIT_FAILURE);
}
for (;;) {
/* 等待epollfd指向的epoll實例有關心的事件發生,并將其對應的epoll_event結構存入events指向的內存 */
nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1);
if (nfds == -1) {
perror("epoll_pwait");
exit(EXIT_FAILURE);
}
for (n = 0; n
/* 如果有客戶端發起連接、就accept建立連接 */
if (events[n].data.fd == listen_sock) {
conn_sock = accept(listen_sock, (structsockaddr *) &local, &addrlen);
if (conn_sock == -1) {
perror("accept");
exit(EXIT_FAILURE);
}
/* 將新連接設置為非阻塞模式 */
setnonblocking(conn_sock);
/* 將新連接添加到epoll實例 */
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) {
perror("epoll_ctl: conn_sock");
exit(EXIT_FAILURE);
}
} else {
/* 如果不是有客戶端發起連接,而是有文件描述符發生關心的事件,這里就可以處理該事件 */
do_use_fd(events[n].data.fd);
}
}
}

