一、原理
epoll使用mmap减少复制开销。 并且epoll使用“事件”的就绪通知方式,通过epoll_ctl注册fd,一旦该fd就绪,内核就会采用类似callback的回调机制来激活该fd,epoll_wait便可以收到通知。
二、最大连接数
虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接
三、效率
因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。
四、消息传递
epoll通过内核和用户空间共享一块内存来实现的,内核于用户空间mmap同一块内存。
五、EPOLL模型的工作模式
(1) LT模式
LT:level triggered,这是缺省的工作方式,同时支持block和no-block socket,在这种模式中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
(2) ET模式
LT:edge-triggered,这是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核就通过epoll告诉你,然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作而导致那个文件描述符不再是就绪状态(比如你在发送,接收或是接受请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核就不会发送更多的通知(only once)。不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。
六、EPOLL模型的使用方法
epoll用到的所有函数都是在头文件sys/epoll.h中声明的,
1.使用的数据结构: epoll_data、epoll_data_t、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 */
};
epoll_event 结构体的events字段是被触发的事件,可能的取值为:
EPOLLIN: 表示对应的文件描述符可以读;
EPOLLOUT: 表示对应的文件描述符可以写;
EPOLLPRI: 表示对应的文件描述符有紧急的数据可读;
EPOLLERR: 表示对应的文件描述符发生错误;
EPOLLHUP: 表示对应的文件描述符被挂断;
EPOLLET: 表示对应的文件描述符有事件发生;
联合体epoll_data用来保存触发事件的某个文件描述符相关的数据。例如一个client连接到服务器,服务器通过调用accept函数可以得到于这个client对应的socket文件描述符,可以把这文件描述符赋给epoll_data的fd字段,以便后面的读写操作在这个文件描述符上进行。
2.使用的函数:
(1)epoll_create
函数声明:intepoll_create(int size)
函数说明:该函数生成一个epoll专用的文件描述符,其中的参数是指定生成描述符的最大范围,告诉内核要监听的数目。
(2) epoll_ctl函数
函数声明:intepoll_ctl(int epfd,int op, int fd, struct epoll_event *event)
函数说明:该函数用于控制某个文件描述符上的事件,可以注册事件、修改事件、删除事件。
epfd:由 epoll_create 生成的epoll专用的文件描述符;
op:要进行的操作,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL 删除;
fd:关联的文件描述符;
event:指向epoll_event的指针;
如果调用成功则返回0,不成功则返回-1。
(3) epoll_wait函数
函数声明:int epoll_wait(int epfd, structepoll_event * events, int maxevents, int timeout)
函数说明:该函数用于轮询I/O事件的发生。
epfd:由epoll_create 生成的epoll专用的文件描述符;
epoll_event:用于回传代处理事件的数组;
maxevents:每次能处理的事件数;
timeout:等待I/O事件发生的超时值;
返回发生事件数。
调用流程:
1.通过create_epoll(int maxfds)来创建一个epoll的句柄,其中maxfds为你的epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的所有操作都将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。
2.在网络主循环里面,调用epoll_wait(int epfd, epoll_event events, int max_events,int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写。基本的语法为:
nfds = epoll_wait(kdpfd, events, maxevents, -1);
其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait函数操作成功之后,events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout参数指示 epoll_wait的超时条件,为0时表示马上返回;为-1时表示函数会一直等下去直到有事件返回;为任意正整数时表示等这么长的时间,如果一直没有事件,则会返回。一般如果网络主循环是单线程,用-1来等待可以保证效率,如果是和主循环在同一个线程,可以用0来保证主循环的效率。
3.epoll_wait返回之后,需要进入一个循环,遍历所有的事件。
七、EPOLL模型的一个实例
#include
#include
#include
#include
#include
#include
#include
#include
#define MAXLINE 10
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5555
#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用于注册事件, events数组用于回传要处理的事件
struct epoll_event ev,events[20];
//生成用于处理accept的epoll专用的文件描述符, 指定生成描述符的最大范围为256
epfd = epoll_create(256);
struct sockaddr_in clientaddr;
struct sockaddr_in serveraddr;
listenfd = socket(AF_INET, SOCK_STREAM, 0);
setnonblocking(listenfd); //把用于监听的socket设置为非阻塞方式
ev.data.fd = listenfd; //设置与要处理的事件相关的文件描述符
ev.events = EPOLLIN | EPOLLET; //设置要处理的事件类型
epoll_ctl(epfd, EPOLL_CTL_ADD, listenfd, &ev); //注册epoll事件
bzero(&serveraddr, sizeof(serveraddr));
serveraddr.sin_family = AF_INET;
char *local_addr = "200.200.200.204";
inet_aton(local_addr, &(serveraddr.sin_addr));
serveraddr.sin_port = htons(SERV_PORT); //或者htons(SERV_PORT);
bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
listen(listenfd, LISTENQ);
maxi = 0;
for( ; ; )
{
nfds = epoll_wait(epfd, events, 20, 500); //等待epoll事件的发生
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); //把客户端的socket设置为非阻塞方式
char *str = inet_ntoa(clientaddr.sin_addr);
std::cout << "connect from " << str < ev.data.fd=connfd; //设置用于读操作的文件描述符 ev.events=EPOLLIN | EPOLLET; //设置用于注测的读操作事件 epoll_ctl(epfd, EPOLL_CTL_ADD, connfd, &ev); //注册ev事件 } else if(events[i].events&EPOLLIN) //读事件 { if ( (sockfd = events[i].data.fd) < 0) { continue; } if ( (n = read(sockfd, line, MAXLINE)) < 0) // 这里和IOCP不同 { if (errno == ECONNRESET) { close(sockfd); events[i].data.fd = -1; } else { std::cout<<"readline error"< } } else if (n == 0) { close(sockfd); events[i].data.fd = -1; } 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上要处理的事件为EPOLIN epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev); } } } }