轉(zhuǎn)自:https://blog.csdn.net/ljx0305/article/details/4065058
epoll - I/O event notification facility
在linux的網(wǎng)絡(luò)編程中,很長的時(shí)間都在使用select來做事件觸發(fā)纲酗。在linux新的內(nèi)核中币励,有了一種替換它的機(jī)制乡洼,就是epoll。
相比于select缰贝,epoll最大的好處在于它不會隨著監(jiān)聽fd數(shù)目的增長而降低效率。因?yàn)樵趦?nèi)核中的select實(shí)現(xiàn)中,它是采用輪詢來處理的纺且,輪詢的fd數(shù)目越多,自然耗時(shí)越多稍浆。并且载碌,在linux/posix_types.h頭文件有這樣的聲明:
define __FD_SETSIZE 1024
表示select最多同時(shí)監(jiān)聽1024個(gè)fd,當(dāng)然衅枫,可以通過修改頭文件再重編譯內(nèi)核來擴(kuò)大這個(gè)數(shù)目嫁艇,但這似乎并不治本。
epoll的接口非常簡單弦撩,一共就三個(gè)函數(shù):
1. int epoll_create(int size);
創(chuàng)建一個(gè)epoll的句柄步咪,size用來告訴內(nèi)核這個(gè)監(jiān)聽的數(shù)目一共有多大。這個(gè)參數(shù)不同于select()中的第一個(gè)參數(shù)益楼,給出最大監(jiān)聽的fd+1的值猾漫。需要注意的是,當(dāng)創(chuàng)建好epoll句柄后感凤,它就是會占用一個(gè)fd值悯周,在linux下如果查看/proc/進(jìn)程id/fd/,是能夠看到這個(gè)fd的陪竿,所以在使用完epoll后禽翼,必須調(diào)用close()關(guān)閉,否則可能導(dǎo)致fd被耗盡族跛。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注冊函數(shù)闰挡,它不同與select()是在監(jiān)聽事件時(shí)告訴內(nèi)核要監(jiān)聽什么類型的事件,而是在這里先注冊要監(jiān)聽的事件類型庸蔼。第一個(gè)參數(shù)是epoll_create()的返回值解总,第二個(gè)參數(shù)表示動(dòng)作,用三個(gè)宏來表示:
EPOLL_CTL_ADD:注冊新的fd到epfd中姐仅;
EPOLL_CTL_MOD:修改已經(jīng)注冊的fd的監(jiān)聽事件花枫;
EPOLL_CTL_DEL:從epfd中刪除一個(gè)fd刻盐;
第三個(gè)參數(shù)是需要監(jiān)聽的fd,第四個(gè)參數(shù)是告訴內(nèi)核需要監(jiān)聽什么事劳翰,struct epoll_event結(jié)構(gòu)如下:
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可以是以下幾個(gè)宏的集合:
EPOLLIN :表示對應(yīng)的文件描述符可以讀(包括對端SOCKET正常關(guān)閉)敦锌;
EPOLLOUT:表示對應(yīng)的文件描述符可以寫;
EPOLLPRI:表示對應(yīng)的文件描述符有緊急的數(shù)據(jù)可讀(這里應(yīng)該表示有帶外數(shù)據(jù)到來)佳簸;
EPOLLERR:表示對應(yīng)的文件描述符發(fā)生錯(cuò)誤乙墙;
EPOLLHUP:表示對應(yīng)的文件描述符被掛斷;
EPOLLET: 將EPOLL設(shè)為邊緣觸發(fā)(Edge Triggered)模式生均,這是相對于水平觸發(fā)(Level Triggered)來說的听想。
EPOLLONESHOT:只監(jiān)聽一次事件,當(dāng)監(jiān)聽完這次事件之后马胧,如果還需要繼續(xù)監(jiān)聽這個(gè)socket的話汉买,需要再次把這個(gè)socket加入到EPOLL隊(duì)列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的產(chǎn)生,類似于select()調(diào)用佩脊。參數(shù)events用來從內(nèi)核得到事件的集合蛙粘,maxevents告之內(nèi)核這個(gè)events有多大,這個(gè) maxevents的值不能大于創(chuàng)建epoll_create()時(shí)的size威彰,參數(shù)timeout是超時(shí)時(shí)間(毫秒出牧,0會立即返回,-1將不確定歇盼,也有說法說是永久阻塞)舔痕。該函數(shù)返回需要處理的事件數(shù)目,如返回0表示已超時(shí)旺遮。
4赵讯、關(guān)于ET、LT兩種工作模式:
可以得出這樣的結(jié)論:
ET模式僅當(dāng)狀態(tài)發(fā)生變化的時(shí)候才獲得通知,這里所謂的狀態(tài)的變化并不包括緩沖區(qū)中還有未處理的數(shù)據(jù),也就是說,如果要采用ET模式,需要一直read/write直到出錯(cuò)為止,很多人反映為什么采用ET模式只接收了一部分?jǐn)?shù)據(jù)就再也得不到通知了,大多因?yàn)檫@樣;而LT模式是只要有數(shù)據(jù)沒有處理就會一直通知下去的.
那么究竟如何來使用epoll呢耿眉?其實(shí)非常簡單边翼。
通過在包含一個(gè)頭文件#include <sys/epoll.h> 以及幾個(gè)簡單的API將可以大大的提高你的網(wǎng)絡(luò)服務(wù)器的支持人數(shù)。
首先通過create_epoll(int maxfds)來創(chuàng)建一個(gè)epoll的句柄鸣剪,其中maxfds為你epoll所支持的最大句柄數(shù)组底。這個(gè)函數(shù)會返回一個(gè)新的epoll句柄,之后的所有操作將通過這個(gè)句柄來進(jìn)行操作筐骇。在用完之后债鸡,記得用close()來關(guān)閉這個(gè)創(chuàng)建出來的epoll句柄。
之后在你的網(wǎng)絡(luò)主循環(huán)里面铛纬,每一幀的調(diào)用epoll_wait(int epfd, epoll_event events, int max events, int timeout)來查詢所有的網(wǎng)絡(luò)接口厌均,看哪一個(gè)可以讀,哪一個(gè)可以寫了告唆」妆祝基本的語法為:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd為用epoll_create創(chuàng)建之后的句柄晶密,events是一個(gè)epoll_event*的指針,當(dāng)epoll_wait這個(gè)函數(shù)操作成功之后模她,epoll_events里面將儲存所有的讀寫事件稻艰。max_events是當(dāng)前需要監(jiān)聽的所有socket句柄數(shù)。最后一個(gè)timeout是 epoll_wait的超時(shí)侈净,為0的時(shí)候表示馬上返回尊勿,為-1的時(shí)候表示一直等下去,直到有事件范圍畜侦,為任意正整數(shù)的時(shí)候表示等這么長的時(shí)間(單位:毫秒)元扔,如果一直沒有事件,則返回夏伊。一般如果網(wǎng)絡(luò)主循環(huán)是單獨(dú)的線程的話摇展,可以用-1來等吻氧,這樣可以保證一些效率溺忧,如果是和主邏輯在同一個(gè)線程的話,則可以用0來保證主循環(huán)的效率盯孙。
epoll_wait范圍之后應(yīng)該是一個(gè)循環(huán)鲁森,遍利所有的事件。
幾乎所有的epoll程序都使用下面的框架:
for( ; ; )
{
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); //accept這個(gè)連接
ev.data.fd=connfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //將新的fd添加到epoll的監(jiān)聽隊(duì)列中
}
else if( events[i].events&EPOLLIN ) //接收到數(shù)據(jù)振惰,讀socket
{
n = read(sockfd, line, MAXLINE)) < 0 //讀
ev.data.ptr = md; //md為自定義類型歌溉,添加數(shù)據(jù)
ev.events=EPOLLOUT|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);//修改標(biāo)識符,等待下一個(gè)循環(huán)時(shí)發(fā)送數(shù)據(jù)骑晶,異步處理的精髓
}
else if(events[i].events&EPOLLOUT) //有數(shù)據(jù)待發(fā)送痛垛,寫socket
{
struct myepoll_data* md = (myepoll_data*)events[i].data.ptr; //取數(shù)據(jù)
sockfd = md->fd;
send( sockfd, md->ptr, strlen((char*)md->ptr), 0 ); //發(fā)送數(shù)據(jù)
ev.data.fd=sockfd;
ev.events=EPOLLIN|EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改標(biāo)識符,等待下一個(gè)循環(huán)時(shí)接收數(shù)據(jù)
}
else
{
//其他的處理
}
}
}
下面給出一個(gè)完整的服務(wù)器端例子:
#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 argc, char* argv[])
{
int i, maxi, listenfd, connfd, sockfd,epfd,nfds, portnumber;
ssize_t n;
char line[MAXLINE];
socklen_t clilen;
if ( 2 == argc )
{
if( (portnumber = atoi(argv[1])) < 0 )
{
fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
return 1;
}
}
else
{
fprintf(stderr,"Usage:%s portnumber/a/n",argv[0]);
return 1;
}
//聲明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è)置要處理的事件類型
ev.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(portnumber);
serveraddr.sin_port=htons(portnumber);
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)//如果新監(jiān)測到一個(gè)SOCKET用戶連接到了綁定的SOCKET端口桶蛔,建立新的連接匙头。
{
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;
//注冊ev
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev);
}
else if(events[i].events&EPOLLIN)//如果是已經(jīng)連接的用戶,并且收到數(shù)據(jù)仔雷,那么進(jìn)行讀入蹂析。
{
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;
} else
std::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) // 如果有數(shù)據(jù)發(fā)送
{
sockfd = events[i].data.fd;
write(sockfd, line, n);
//設(shè)置用于讀操作的文件描述符
ev.data.fd=sockfd;
//設(shè)置用于注測的讀操作事件
ev.events=EPOLLIN|EPOLLET;
//修改sockfd上要處理的事件為EPOLIN
epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev);
}
}
}
return 0;
}
客戶端直接連接到這個(gè)服務(wù)器就好了。碟婆。