linux-epoll研究
linux-epoll研究 - Geek_Ma - 博客園
linux-epoll研究
? ?做linux網絡編程的同學都清楚,2.6版本以前的linux內核大多都是用select作為非阻塞的事件觸發模型,但是效率低,使用受限已經很明顯的暴露了select(包括poll)的缺陷了,為了解決這些缺陷,epoll作為linux新的事件觸發模型被創造出來。
一、epoll相對于select的優點:
1.支持一個進程socket描述符(FD)的最大數目
? ? select支持的單進程socket描述符最大數目只有幾千,而epoll支持的數目很大,等于系統最大打開的文件描述符數,這個文件描述符數跟內存有一定關系
2.IO效率不隨FD數目增加而線性下降
? ? select對事件的掃描是針對于所有創建的socket描述符進行的,也就是說,有多少個socket描述符,就需要遍歷多少個句柄,所以IO效率是隨描述符增加線性下降的;而epoll只遍歷活躍的socket描述符,這是因為在內核實現中epoll是根據每個fd上面的callback函數實現的。那么,只有"活躍"的socket才會主動的去調用 callback函數,其他idle狀態socket則不會。比如一個高速LAN環境,epoll并不比select/poll有什么效率,相 反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模擬WAN環境,epoll的效率就遠在select/poll之上了。
? ? 3.使用mmap加速內核與用戶空間的消息傳遞
? ? select事件觸發后會將信息從內核拷貝到用戶空間,這種拷貝就影響了效率。而mmap將內核與用戶空間的內存映射到一塊內存上,內核將消息捕獲后放入該內存空間,用戶無需拷貝直接可以訪問,減少了拷貝次數,提高了效率。
?二、epoll工作模型
epoll事件有兩種模型:
Edge Triggered (ET),邊緣觸發是高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變為就緒時,內核通過epoll告訴你。然后它會假設你知道文件描述符已經就緒,并且不會再為那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再為就緒狀態了(比如,你在發送,接收或者接收請求,或者發送接收的數據少于一定量時導致了一個EWOULDBLOCK錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once),不過在TCP協議中,ET模式的加速效用仍需要更多的benchmark確認。效率非常高,在并發,大流量的情況下,會比LT少很多epoll的系統調用,因此效率高。但是對編程要求高,需要細致的處理每個請求,否則容易發生丟失事件的情況。
Level Triggered (LT),水平觸發是缺省的工作方式,并且同時支持block和no-block socket.在這種做法中,內核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續通知你的,所以,這種模式編程出錯誤可能性要小一點。傳統的select/poll都是這種模型的代表。效率會低于ET觸發,尤其在大并發,大流量的情況下。但是LT對代碼編寫要求比較低,不容易出現問題。LT模式服務編寫上的表現是:只要有數據沒有被獲取,內核就不斷通知你,因此不用擔心事件丟失的情況。
?
三、值得注意的情況:
1.當使用epoll的ET模型來工作時,當產生了一個EPOLLIN事件后,讀數據的時候需要考慮的是當recv()返回的大小如果等于請求的大小,那么很有可能是緩沖區還有數據未讀完,也意味著該次事件還沒有處理完,所以還需要再次讀取:
while(rs) {buflen = recv(events[i].data.fd, buf, sizeof(buf), 0);if(buflen < 0){// 由于是非阻塞的模式,所以當errno為EAGAIN時,表示當前緩沖區已無數據可讀// 在這里就當作是該次事件已處理處.if(errno == EAGAIN)break;elsereturn;}else if(buflen == 0){// 這里表示對端的socket已正常關閉. }if(buflen == sizeof(buf)rs = 1; // 需要再次讀取elsers = 0; }2.如果發送端流量大于接收端的流量,也就是說,epoll所在的程序讀比轉發的socket要慢,由于是非阻塞的socket,那么send()函數雖然返回,但實際緩沖區的數據并未真正發給接收端,這樣不斷的讀和發,當緩沖區滿后會產生EAGAIN錯誤(參考mansend),同時,不理會這次請求發送的數據。所以,需要封裝socket_send()的函數用來處理這種情況,該函數會盡量將數據寫完再返回,返回-1表示出錯。在socket_send()內部,當寫緩沖已滿(send()返回-1,且errno為EAGAIN),那么會等待后再重試。這種方式并不很完美,在理論上可能會長時間的阻塞在socket_send()內部,但暫沒有更好的辦法。
ssize_t socket_send(int sockfd, const char* buffer, size_t buflen) {ssize_t tmp;size_t total = buflen;const char *p = buffer;while(1){tmp = send(sockfd, p, total, 0);if(tmp < 0){// 當send收到信號時,可以繼續寫,但這里返回-1.if(errno == EINTR)return -1;// 當socket是非阻塞時,如返回此錯誤,表示寫緩沖隊列已滿,// 在這里做延時后再重試.if(errno == EAGAIN){usleep(1000);continue;}return -1;}if((size_t)tmp == total)return buflen;total -= tmp;p += tmp;}return tmp; }四、實例
#include <iostream> #include <sys/socket.h> #include <sys/epoll.h> #include <netinet/in.h> #include <arpa/inet.h> #include <fcntl.h> #include <unistd.h> #include <stdio.h> #include <errno.h>using namespace std;#define MAXLINE 5 #define OPEN_MAX 100 #define LISTENQ 20 #define SERV_PORT 5000 #define INFTIM 1000 //設置非阻塞 void setnonblocking(int sock) {int opts;opts = fcntl(sock, F_GETFL);if(opts<0){perror("fcntl(sock,GETFL)");exit(1);}opts = opts|O_NONBLOCK;if(fcntl(sock,F_SETFL,opts)<0){perror("fcntl(sock,SETFL,opts)");exit(1);} }int main() {int i, maxi, listenfd,connfd, sockfd,epfd,nfds;ssize_t n;char line[MAXLINE];socklen_t clilen;//聲明epoll_event結構體的變量,ev用于注冊事件,數組用于回傳要處理的事件struct epoll_event ev, events[20];//生成用于處理accept的epoll專用的文件描述符epfd = epoll_create(256);struct sockaddr_in clientaddr;struct sockaddr_in serveraddr;listenfd = socket(AF_INET, SOCK_STREAM, 0);//把socket設置為非阻塞方式//setnonblocking(listenfd);//設置與要處理的事件相關的文件描述符ev.data.fd = listenfd;//設置要處理的事件類型ETev.events = EPOLLIN|EPOLLET;//ev.events=EPOLLIN;//注冊epoll事件epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev);bzero(&serveraddr, sizeof(serveraddr));serveraddr.sin_family = AF_INET;char *local_addr="127.0.0.1";inet_aton(local_addr,&(serveraddr.sin_addr));//htons(SERV_PORT);serveraddr.sin_port=htons(SERV_PORT);bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));listen(listenfd, LISTENQ);maxi = 0;for ( ; ; ) {//等待epoll事件的發生nfds = epoll_wait(epfd, events, 20, 500);//處理所發生的所有事件 for(i = 0; i < nfds;++i){if(events[i].data.fd == listenfd){connfd = accept(listenfd, (sockaddr *)&clientaddr, &clilen);if(connfd < 0){perror("connfd<0");exit(1);}//setnonblocking(connfd);char *str = inet_ntoa(clientaddr.sin_addr);cout << "accapt a connection from " << str << endl;//設置用于讀操作的文件描述符ev.data.fd = connfd;//設置用于注測的讀操作事件ev.events = EPOLLIN|EPOLLET;//ev.events=EPOLLIN;//注冊evepoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev);}else if(events[i].events&EPOLLIN){cout << "EPOLLIN" << endl;if ( (sockfd = events[i].data.fd) < 0)continue;if ( (n = read(sockfd, line, MAXLINE)) < 0){if (errno == ECONNRESET) {close(sockfd);events[i].data.fd = -1;} elsestd::cout<<"readline error"<<std::endl;} else if (n == 0) {close(sockfd);events[i].data.fd = -1;}line[n] = '\0';cout << "read " << line << endl;//設置用于寫操作的文件描述符ev.data.fd = sockfd;//設置用于注冊的寫操作事件ev.events = EPOLLOUT|EPOLLET;//修改sockfd上要處理的事件為EPOLLOUT//epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev); }else if(events[i].events&EPOLLOUT){ sockfd = events[i].data.fd;write(sockfd, line, n);//設置用于讀操作的文件描述符ev.data.fd = sockfd;//設置用于注測的讀操作事件ev.events = EPOLLIN|EPOLLET;//修改sockfd上要處理的事件為EPOLINepoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);}}}return 0; }上面的代碼是ET模式
測試腳本1:
#!/usr/bin/python import socket import timesock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('127.0.0.1', 5000))sock.send('1234567890') time.sleep(5)while(1):time.sleep(1)輸出1:
accapt a connection from 0.0.0.0 EPOLLIN read 12345說明1:
運行server和client發現,server僅僅讀取了5字節的數據,而client其實發送了10字節的數據,也就是說,server僅當第一次監聽到了EPOLLIN事件,由于沒有讀取完數據,而且采用的是ET模式,狀態在此之后不發生變化,因此server再也接收不到EPOLLIN事件了。當關閉客戶端時,會另外觸發一個事件,這個事件又觸發了一次讀操作,也就將后面的5個字節讀取出來。
測試腳本2:
#!/usr/bin/python import socket import timesock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(('127.0.0.1', 5000))sock.send('1234567890') time.sleep(5) sock.send('1234567890')while(1):time.sleep(1)輸出2:
accapt a connection from 0.0.0.0 EPOLLIN read 12345 (5 sec...) EPOLLIN read 67890說明2:
可以發現,在server接收完5字節的數據之后一直監聽不到client的事件,而當client休眠5秒之后重新發送數據,server再次監聽到了變化,只不過因為只是讀取了5個字節,仍然有10個字節的數據(client第二次發送的數據)沒有接收完。
如果上面的實驗中,對accept的socket都采用的是LT模式,那么只要還有數據留在buffer中,server就會繼續得到通知,可以將上面標黃的選項去掉則變為LT模式。
五、總結
? ? ET模式僅當狀態發生變化的時候才獲得通知,這里所謂的狀態的變化并不包括緩沖區中還有未處理的數據,也就是說,如果要采用ET模式,需要一直read/write直到出錯為止,很多人反映為什么采用ET模式只接收了一部分數據就再也得不到通知了,大多是這個原因造成的;而LT模式是只要有數據沒有處理就會一直通知下去的。
補充說明一下這里一直強調的"狀態變化"是什么:
1)對于監聽可讀事件時,如果是socket是監聽socket,那么當有新的主動連接到來為狀態發生變化;對一般的socket而言,協議棧中相應的緩沖區有新的數據為狀態發生變化。但是,如果在一個時間同時接收了N個連接(N>1),但是監聽socket只accept了一個連接,那么其它未 accept的連接將不會在ET模式下給監聽socket發出通知,此時狀態不發生變化;對于一般的socket,就如例子中而言,如果對應的緩沖區本身已經有了N字節的數據,而只取出了小于N字節的數據,那么殘存的數據不會造成狀態發生變化。
2)對于監聽可寫事件時,同理可推,不再詳述。
? ? ?不論是監聽可讀還是可寫,對方關閉socket連接都將造成狀態發生變化,比如在例子中,如果強行中斷client腳本,也就是主動中斷了socket連接,那么都將造成server端發生狀態的變化,從而server得到通知,將已經在本方緩沖區中的數據讀出。
? ? 把前面的描述可以總結如下:僅當對方的動作(發出數據,關閉連接等)造成的事件才能導致狀態發生變化,而本方協議棧中已經處理的事件(包括接收了對方的數據,接收了對方的主動連接請求)并不是造成狀態發生變化的必要條件,狀態變化一定是對方造成的。所以在ET模式下的,必須一直處理到出錯或者完全處理完畢,才能進行下一個動作,否則可能會發生錯誤。
部分轉自他處-沒有找到最終來源
總結
以上是生活随笔為你收集整理的linux-epoll研究的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Nginx 学习笔记(四) Nginx+
- 下一篇: MATLAB生成正态样本以及正态矩阵、从