linux-epoll研究
linux-epoll研究 - Geek_Ma - 博客園
linux-epoll研究
? ?做linux網(wǎng)絡(luò)編程的同學都清楚,2.6版本以前的linux內(nèi)核大多都是用select作為非阻塞的事件觸發(fā)模型,但是效率低,使用受限已經(jīng)很明顯的暴露了select(包括poll)的缺陷了,為了解決這些缺陷,epoll作為linux新的事件觸發(fā)模型被創(chuàng)造出來。
一、epoll相對于select的優(yōu)點:
1.支持一個進程socket描述符(FD)的最大數(shù)目
? ? select支持的單進程socket描述符最大數(shù)目只有幾千,而epoll支持的數(shù)目很大,等于系統(tǒng)最大打開的文件描述符數(shù),這個文件描述符數(shù)跟內(nèi)存有一定關(guān)系
2.IO效率不隨FD數(shù)目增加而線性下降
? ? select對事件的掃描是針對于所有創(chuàng)建的socket描述符進行的,也就是說,有多少個socket描述符,就需要遍歷多少個句柄,所以IO效率是隨描述符增加線性下降的;而epoll只遍歷活躍的socket描述符,這是因為在內(nèi)核實現(xiàn)中epoll是根據(jù)每個fd上面的callback函數(shù)實現(xiàn)的。那么,只有"活躍"的socket才會主動的去調(diào)用 callback函數(shù),其他idle狀態(tài)socket則不會。比如一個高速LAN環(huán)境,epoll并不比select/poll有什么效率,相 反,如果過多使用epoll_ctl,效率相比還有稍微的下降。但是一旦使用idle connections模擬WAN環(huán)境,epoll的效率就遠在select/poll之上了。
? ? 3.使用mmap加速內(nèi)核與用戶空間的消息傳遞
? ? select事件觸發(fā)后會將信息從內(nèi)核拷貝到用戶空間,這種拷貝就影響了效率。而mmap將內(nèi)核與用戶空間的內(nèi)存映射到一塊內(nèi)存上,內(nèi)核將消息捕獲后放入該內(nèi)存空間,用戶無需拷貝直接可以訪問,減少了拷貝次數(shù),提高了效率。
?二、epoll工作模型
epoll事件有兩種模型:
Edge Triggered (ET),邊緣觸發(fā)是高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變?yōu)榫途w時,內(nèi)核通過epoll告訴你。然后它會假設(shè)你知道文件描述符已經(jīng)就緒,并且不會再為那個文件描述符發(fā)送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再為就緒狀態(tài)了(比如,你在發(fā)送,接收或者接收請求,或者發(fā)送接收的數(shù)據(jù)少于一定量時導致了一個EWOULDBLOCK錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內(nèi)核不會發(fā)送更多的通知(only once),不過在TCP協(xié)議中,ET模式的加速效用仍需要更多的benchmark確認。效率非常高,在并發(fā),大流量的情況下,會比LT少很多epoll的系統(tǒng)調(diào)用,因此效率高。但是對編程要求高,需要細致的處理每個請求,否則容易發(fā)生丟失事件的情況。
Level Triggered (LT),水平觸發(fā)是缺省的工作方式,并且同時支持block和no-block socket.在這種做法中,內(nèi)核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內(nèi)核還是會繼續(xù)通知你的,所以,這種模式編程出錯誤可能性要小一點。傳統(tǒng)的select/poll都是這種模型的代表。效率會低于ET觸發(fā),尤其在大并發(fā),大流量的情況下。但是LT對代碼編寫要求比較低,不容易出現(xiàn)問題。LT模式服務(wù)編寫上的表現(xiàn)是:只要有數(shù)據(jù)沒有被獲取,內(nèi)核就不斷通知你,因此不用擔心事件丟失的情況。
?
三、值得注意的情況:
1.當使用epoll的ET模型來工作時,當產(chǎn)生了一個EPOLLIN事件后,讀數(shù)據(jù)的時候需要考慮的是當recv()返回的大小如果等于請求的大小,那么很有可能是緩沖區(qū)還有數(shù)據(jù)未讀完,也意味著該次事件還沒有處理完,所以還需要再次讀取:
while(rs) {buflen = recv(events[i].data.fd, buf, sizeof(buf), 0);if(buflen < 0){// 由于是非阻塞的模式,所以當errno為EAGAIN時,表示當前緩沖區(qū)已無數(shù)據(jù)可讀// 在這里就當作是該次事件已處理處.if(errno == EAGAIN)break;elsereturn;}else if(buflen == 0){// 這里表示對端的socket已正常關(guān)閉. }if(buflen == sizeof(buf)rs = 1; // 需要再次讀取elsers = 0; }2.如果發(fā)送端流量大于接收端的流量,也就是說,epoll所在的程序讀比轉(zhuǎn)發(fā)的socket要慢,由于是非阻塞的socket,那么send()函數(shù)雖然返回,但實際緩沖區(qū)的數(shù)據(jù)并未真正發(fā)給接收端,這樣不斷的讀和發(fā),當緩沖區(qū)滿后會產(chǎn)生EAGAIN錯誤(參考mansend),同時,不理會這次請求發(fā)送的數(shù)據(jù)。所以,需要封裝socket_send()的函數(shù)用來處理這種情況,該函數(shù)會盡量將數(shù)據(jù)寫完再返回,返回-1表示出錯。在socket_send()內(nèi)部,當寫緩沖已滿(send()返回-1,且errno為EAGAIN),那么會等待后再重試。這種方式并不很完美,在理論上可能會長時間的阻塞在socket_send()內(nèi)部,但暫沒有更好的辦法。
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收到信號時,可以繼續(xù)寫,但這里返回-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 //設(shè)置非阻塞 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結(jié)構(gòu)體的變量,ev用于注冊事件,數(shù)組用于回傳要處理的事件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設(shè)置為非阻塞方式//setnonblocking(listenfd);//設(shè)置與要處理的事件相關(guān)的文件描述符ev.data.fd = listenfd;//設(shè)置要處理的事件類型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事件的發(fā)生nfds = epoll_wait(epfd, events, 20, 500);//處理所發(fā)生的所有事件 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;//設(shè)置用于讀操作的文件描述符ev.data.fd = connfd;//設(shè)置用于注測的讀操作事件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;//設(shè)置用于寫操作的文件描述符ev.data.fd = sockfd;//設(shè)置用于注冊的寫操作事件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);//設(shè)置用于讀操作的文件描述符ev.data.fd = sockfd;//設(shè)置用于注測的讀操作事件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發(fā)現(xiàn),server僅僅讀取了5字節(jié)的數(shù)據(jù),而client其實發(fā)送了10字節(jié)的數(shù)據(jù),也就是說,server僅當?shù)谝淮伪O(jiān)聽到了EPOLLIN事件,由于沒有讀取完數(shù)據(jù),而且采用的是ET模式,狀態(tài)在此之后不發(fā)生變化,因此server再也接收不到EPOLLIN事件了。當關(guān)閉客戶端時,會另外觸發(fā)一個事件,這個事件又觸發(fā)了一次讀操作,也就將后面的5個字節(jié)讀取出來。
測試腳本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:
可以發(fā)現(xiàn),在server接收完5字節(jié)的數(shù)據(jù)之后一直監(jiān)聽不到client的事件,而當client休眠5秒之后重新發(fā)送數(shù)據(jù),server再次監(jiān)聽到了變化,只不過因為只是讀取了5個字節(jié),仍然有10個字節(jié)的數(shù)據(jù)(client第二次發(fā)送的數(shù)據(jù))沒有接收完。
如果上面的實驗中,對accept的socket都采用的是LT模式,那么只要還有數(shù)據(jù)留在buffer中,server就會繼續(xù)得到通知,可以將上面標黃的選項去掉則變?yōu)長T模式。
五、總結(jié)
? ? ET模式僅當狀態(tài)發(fā)生變化的時候才獲得通知,這里所謂的狀態(tài)的變化并不包括緩沖區(qū)中還有未處理的數(shù)據(jù),也就是說,如果要采用ET模式,需要一直read/write直到出錯為止,很多人反映為什么采用ET模式只接收了一部分數(shù)據(jù)就再也得不到通知了,大多是這個原因造成的;而LT模式是只要有數(shù)據(jù)沒有處理就會一直通知下去的。
補充說明一下這里一直強調(diào)的"狀態(tài)變化"是什么:
1)對于監(jiān)聽可讀事件時,如果是socket是監(jiān)聽socket,那么當有新的主動連接到來為狀態(tài)發(fā)生變化;對一般的socket而言,協(xié)議棧中相應(yīng)的緩沖區(qū)有新的數(shù)據(jù)為狀態(tài)發(fā)生變化。但是,如果在一個時間同時接收了N個連接(N>1),但是監(jiān)聽socket只accept了一個連接,那么其它未 accept的連接將不會在ET模式下給監(jiān)聽socket發(fā)出通知,此時狀態(tài)不發(fā)生變化;對于一般的socket,就如例子中而言,如果對應(yīng)的緩沖區(qū)本身已經(jīng)有了N字節(jié)的數(shù)據(jù),而只取出了小于N字節(jié)的數(shù)據(jù),那么殘存的數(shù)據(jù)不會造成狀態(tài)發(fā)生變化。
2)對于監(jiān)聽可寫事件時,同理可推,不再詳述。
? ? ?不論是監(jiān)聽可讀還是可寫,對方關(guān)閉socket連接都將造成狀態(tài)發(fā)生變化,比如在例子中,如果強行中斷client腳本,也就是主動中斷了socket連接,那么都將造成server端發(fā)生狀態(tài)的變化,從而server得到通知,將已經(jīng)在本方緩沖區(qū)中的數(shù)據(jù)讀出。
? ? 把前面的描述可以總結(jié)如下:僅當對方的動作(發(fā)出數(shù)據(jù),關(guān)閉連接等)造成的事件才能導致狀態(tài)發(fā)生變化,而本方協(xié)議棧中已經(jīng)處理的事件(包括接收了對方的數(shù)據(jù),接收了對方的主動連接請求)并不是造成狀態(tài)發(fā)生變化的必要條件,狀態(tài)變化一定是對方造成的。所以在ET模式下的,必須一直處理到出錯或者完全處理完畢,才能進行下一個動作,否則可能會發(fā)生錯誤。
部分轉(zhuǎn)自他處-沒有找到最終來源
總結(jié)
以上是生活随笔為你收集整理的linux-epoll研究的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Nginx 学习笔记(四) Nginx+
- 下一篇: MATLAB生成正态样本以及正态矩阵、从