目錄html
Linux NIO 系列(04-3) epolllinux
Netty 系列目錄(http://www.javashuo.com/article/p-hskusway-em.html)編程
句柄限制:單個進程可以監視的文件描述符的數量存在最大限制,一般是 1024,固然能夠更改數量,但因爲select採用輪詢的方式掃描文件描述符,文件描述符數量越多,性能越差;(在 linux 內核頭文件中,有這樣的定義: #define __FD_SETSIZE 1024
)api
數據拷貝:內核 / 用戶空間內存拷貝問題,select 須要複製大量的句柄數據結構,產生巨大的開銷;數組
輪詢機制:select 返回的是含有整個句柄的數組,應用程序須要遍歷整個數組才能發現哪些句柄發生了事件;緩存
select 的觸發方式是水平觸發,應用程序若是沒有完成對一個已經就緒的文件描述符進行 IO 操做,那麼以後每次 select 調用仍是會將這些文件描述符通知進程。服務器
設想一下以下場景:有 100 萬個客戶端同時與一個服務器進程保持着 TCP 鏈接。而每一時刻,一般只有幾百上千個 TCP 鏈接是活躍的(事實上大部分場景都是這種狀況)。如何實現這樣的高併發?網絡
粗略計算一下,一個進程最多有 1024 個文件描述符,那麼咱們須要開 1000 個進程來處理 100 萬個客戶鏈接。若是咱們使用 select 模型,這 1000 個進程裏某一段時間內只有數個客戶鏈接須要數據的接收,那麼咱們就不得不輪詢 1024 個文件描述符以肯定到底是哪一個客戶有數據可讀,想一想若是 1000 個進程都有相似的行爲,那系統資源消耗可有多大啊!數據結構
針對 select 模型的缺點,epoll 模型被提出來了!併發
epoll 在 Linux2.6 內核正式提出,是基於事件驅動的 I/O 方式,相對於 select 來講,epoll 沒有描述符個數限制,使用一個文件描述符管理多個描述符,將用戶關心的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的 copy 只需一次。
Linux 中提供的 epoll 相關函數以下:
int epoll_create(int size); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
函數建立一個 epoll 句柄,參數 size 代表內核要監聽的描述符數量。調用成功時返回一個 epoll 句柄描述符,失敗時返回 -1。
函數註冊要監聽的事件類型。四個參數解釋以下:
epfd 表示 epoll 句柄
op 表示 fd 操做類型,有以下 3 種
EPOLL_CTL_ADD 註冊新的fd到epfd中 EPOLL_CTL_MOD 修改已註冊的fd的監聽事件 EPOLL_CTL_DEL 從epfd中刪除一個fd
fd 是要監聽的描述符
event 表示要監聽的事件。epoll_event 結構體定義以下:
struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t;
函數等待事件的就緒,成功時返回就緒的事件數目,調用失敗時返回 -1,等待超時返回 0。
epoll 是 Linux 內核爲處理大批量文件描述符而做了改進的 poll,是 Linux 下多路複用 IO 接口 select/poll 的加強版本,它能顯著提升程序在大量併發鏈接中只有少許活躍的狀況下的系統 CPU 利用率。緣由就是獲取事件的時候,它無須遍歷整個被偵聽的描述符集,只要遍歷那些被內核 IO 事件異步喚醒而加入 Ready 隊列的描述符集合就好了。
epoll 除了提供 select/poll 那種 IO 事件的水平觸發(Level Triggered)外,還提供了邊緣觸發(Edge Triggered),這就使得用戶空間程序有可能緩存 IO 狀態,減小 epoll_wait/epoll_pwait 的調用,提升應用程序效率。
水平觸發(LT):默認工做模式,即當 epoll_wait 檢測到某描述符事件就緒並通知應用程序時,應用程序能夠不當即處理該事件;下次調用 epoll_wait 時,會再次通知此事件。
水平觸發同時支持 block 和 non-block socket。在這種作法中,內核告訴你一個文件描述符是否就緒了,而後你能夠對這個就緒的 fd 進行 IO 操做。若是你不做任何操做,內核仍是會繼續通知你的,因此,這種模式編程出錯誤可能性要小一點。好比內核通知你其中一個fd能夠讀數據了,你趕忙去讀。你仍是懶懶散散,不去讀這個數據,下一次循環的時候內核發現你還沒讀剛纔的數據,就又通知你趕忙把剛纔的數據讀了。這種機制能夠比較好的保證每一個數據用戶都處理掉了。
邊緣觸發(ET): 當 epoll_wait 檢測到某描述符事件就緒並通知應用程序時,應用程序必須當即處理該事件。若是不處理,下次調用 epoll_wait 時,不會再次通知此事件。(直到你作了某些操做致使該描述符變成未就緒狀態了,也就是說邊緣觸發只在狀態由未就緒變爲就緒時只通知一次)。
邊緣觸發是高速工做方式,只支持 no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核經過 epoll 告訴你。而後它會假設你知道文件描述符已經就緒,而且不會再爲那個文件描述符發送更多的就緒通知,等到下次有新的數據進來的時候纔會再次出發就緒事件。簡而言之,就是內核通知過的事情不會再說第二遍,數據錯過沒讀,你本身負責。這種機制確實速度提升了,可是風險相伴而行。
LT 和 ET 本來應該是用於脈衝信號的,可能用它來解釋更加形象。Level 和 Edge 指的就是觸發點,Level 爲只要處於水平,那麼就一直觸發,而 Edge 則爲上升沿和降低沿的時候觸發。好比:0->1 就是 Edge,1->1 就是 Level。
ET 模式很大程度上減小了 epoll 事件的觸發次數,所以效率比 LT 模式下高。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #include <sys/epoll.h> #include <sys/socket.h> #include <arpa/inet.h> #include <netinet/in.h> #define SERVER_PORT 8888 #define OPEN_MAX 3000 #define BACKLOG 10 #define BUF_SIZE 1024 int main() { int ret, i; int listenfd, connfd, epollfd; int nready; int recvbytes, sendbytes; char* recv_buf; struct epoll_event ev; struct epoll_event* ep; ep = (struct epoll_event*) malloc(sizeof(struct epoll_event) * OPEN_MAX); recv_buf = (char*) malloc(sizeof(char) * BUF_SIZE); struct sockaddr_in seraddr; struct sockaddr_in cliaddr; int addr_len; memset(&seraddr, 0, sizeof(seraddr)); seraddr.sin_family = AF_INET; seraddr.sin_port = htons(SERVER_PORT); seraddr.sin_addr.s_addr = htonl(INADDR_ANY); listenfd = socket(AF_INET, SOCK_STREAM, 0); if(listenfd == -1) { perror("create socket failed.\n"); return 1; } ret = bind(listenfd, (struct sockaddr*)&seraddr, sizeof(seraddr)); if(ret == -1) { perror("bind failed.\n"); return 1; } ret = listen(listenfd, BACKLOG); if(ret == -1) { perror("listen failed.\n"); return 1; } epollfd = epoll_create(1); if(epollfd == -1) { perror("epoll_create failed.\n"); return 1; } ev.events = EPOLLIN; ev.data.fd = listenfd; ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &ev); if(ret == -1) { perror("epoll_ctl failed.\n"); return 1; } while(1) { nready = epoll_wait(epollfd, ep, OPEN_MAX, -1); if(nready == -1) { perror("epoll_wait failed.\n"); return 1; } for(i = 0; i < nready; i++) { if(ep[i].data.fd == listenfd) { addr_len = sizeof(cliaddr); connfd = accept(listenfd, (struct sockaddr*) &cliaddr, &addr_len); printf("client IP: %s\t PORT : %d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port)); if(connfd == -1) { perror("accept failed.\n"); return 1; } ev.events = EPOLLIN; ev.data.fd = connfd; ret = epoll_ctl(epollfd, EPOLL_CTL_ADD, connfd, &ev); if(ret == -1) { perror("epoll_ctl failed.\n"); return 1; } } else { recvbytes = recv(ep[i].data.fd, recv_buf, BUF_SIZE, 0); if(recvbytes <= 0) { close(ep[i].data.fd); epoll_ctl(epollfd, EPOLL_CTL_DEL, ep[i].data.fd, &ev); continue; } printf("receive %s\n", recv_buf); sendbytes = send(ep[i].data.fd, recv_buf, (size_t)recvbytes, 0); if(sendbytes == -1) { perror("send failed.\n"); } } } // for each ev } // while(1) close(epollfd); close(listenfd); free(ep); free(recv_buf); return 0; }
參考:
天天用心記錄一點點。內容也許不重要,但習慣很重要!