I/O多路转接之epoll
epoll:处理大批量句柄而做了改进的poll。
工作模式:
1、边缘触发(ET)当收到数据的时候,通知你读取数据。当你没有读取的时候,不会通知你,可能造成数据丢失。当你来读取数据的时候,就应该对这次的数据读取完毕。
LT(level triggered)是epoll缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。 传统的select/poll都是这种模型的代表.
2、水平触发(LT)读取数据的时候没有读取继续通知你来读取。
ET(edge-triggered)是高速工作方式,只支持no-block socket,它效率要比LT更高。ET与LT的区别在于,当一个新的事件到来时,ET模式下当然可以从epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的。而LT模式正好相反,只要一个事件对应的套接字缓冲区还有数据,就总能从epoll_wait中获取这个事件。 因此,LT模式下开发基于epoll的应用要简单些,不太容易出错。而在ET模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲区中的用户请求得不到响应。
三个系统调用:epoll_create,epoll_ctl,epoll_wait。
创建一个epoll句柄,size参数是被忽略的。
注意:当创建好epoll句柄后,他就会占用一个fd值,所以在用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
epoll的事件注册函数,它不同于select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数是epoll_create()的返回值。
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd。
第四个参数是告诉内核需要监听什么事,struct epoll_event结构如下:
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
收集在epoll监控的事件中已经发送的事件。参数events是分配好的epoll_event结构体数组, epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。maxevents告诉内核这个 events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时。
epoll工作原理
1)调用epoll_create时,做了以下事情:
内核帮我们在epoll文件系统里建了个file结点;
在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket;
建立一个list链表,用于存储准备就绪的事件。
2)调用epoll_ctl时,做了以下事情:
把socket放到epoll文件系统里file对象对应的红黑树上;
给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。
3)调用epoll_wait时,做了以下事情:
观察list链表里有没有数据。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已。
总结如下:
一颗红黑树,一张准备就绪句柄链表,少量的内核cache,解决了大并发下的socket处理问题。
执行epoll_create时,创建了红黑树和就绪链表;
执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向
准备就绪链表中插入数据;
执行epoll_wait时立刻返回准备就绪链表里的数据即可。
在epoll ET模式中,为何fd必须要设置为非阻塞?
ET(边缘触发)数据就绪只会通知一次,也就是说,如果要使用ET模式,当数据就绪时, 需要一直read,直到出错或完成为止。但倘若当前fd为阻塞(默认),那么在当读完缓冲区的数据时,如果对端并没有关闭写端,那么该read函数会一直阻塞,影响其他fd以及后续逻 辑!所以此时将该fd设置为非阻塞,当没有数据的时候,read虽然读取不到任何内容,但是肯定不会被hang住,那么此时,说明缓冲区数据已经读取完毕,需要继续处理后续逻辑(读取其他fd或者进入wait)。
epoll的优点:
1、本身没有最大并发连接的限制,仅受系统中进程能打开的最大文件数目限制;
2、I/O效率不随fd数量的1增加而线性下降
3、使用mmap加速内核与用户空间的消息传递
4、内核微调:这一点其实不算epoll的优点了,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么可以在运行时期动态调整这个内存pool(skb_head_pool)的大小--- 通过echo XXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网卡驱动架构。
epoll的缺点:
1、跨平台性不够强,只能工作在linux平台下
2、使用复杂一些,但相比IOCP,增加的一点复杂度基本上达到了IOCP的并发量和性能,而复杂度远远小于IOCP.
3、相比于IOCP来说,对多核/多线程的支持不够好,在性能要求比较苛刻的情况下不然IOCP。
/**************************************************************************
>file name:my_epoll.c
>author:Comedly
>create time:2016-8-9
**************************************************************************/
#include<stdio.h>
#include<sys/epoll.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<errno.h>
#include<fcntl.h>
#include<unistd.h>
//static void my_read(int epfd,int fd,char *buf,int len)
//{
// int rs = 1;
// while(rs)
// {
// ssize_t size = recv(fd,buf,len,0);
// if(size < 0)
// {
// if(errno = EAGAIN)
// {
// break;
// }
// else
// {
// perror("recv");
// return 9;
// }
// }
// else if(size == 0)
// {
// //表示对端的sock已经正常关闭
// printf("client close....\n");
// struct epoll_event ev;
// epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
// close(fd);
// break;
// }
// else
// {
// buf[size - 1] = '\0';
// printf("client # %s\n",buf);
// if(size == len)
// {
// rs = 1;
// }
// else
// {
// rs = 0;
// }
// }
// }
//}
//static void my_write(int fd,char *buf,int len)
//{
// int ws = 1;
// while(ws)
// {
// ssize_t size = send(fd,buf,len,0);
// if(size < 0)
// {
// //缓冲区已满,延时重试
// if(errno == EAGAIN)
// {
// usleep(1000);
// continue;
// }
// if(errno == EINTR)
// {
// return -1;
// }
// }
// if(size == len)
// {
// continue;
// }
// len -= size;
// buf += size;
// }
//}
ssize_t my_read(int fd,char buf[])
{
int ret = 0;
int total = 0;
while((ret = read(fd,buf+total,sizeof(buf)-total)) && errno != EAGAIN)
{
total +=ret;
}
buf[total] = '\0';
return total;
}
static void set_no_block(int fd)
{
//先得到之前的状态,在之前的状态上加入新状态,将其设置为非阻塞。
int before = fcntl(fd,F_GETFL);
fcntl(fd,F_SETFL,before|O_NONBLOCK);
}
static int startup(const char *ip,int port)
{
int sock = socket(AF_INET,SOCK_STREAM,0);
if(sock < 0 )
{
perror("socket");
return 2;
}
int opt = 1;
setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt));
struct sockaddr_in local;
local.sin_family = AF_INET;
local.sin_port = htons(port);
local.sin_addr.s_addr = inet_addr(ip);
if(bind(sock,(struct sockaddr*)&local,sizeof(local))<0)
{
perror("bind");
return 3;
}
if(listen(sock,5) < 0)
{
perror("listen");
return 4;
}
return sock;
}
static void usage(const char* proc)
{
printf("%s [ip][port]\n",proc);
}
int main(int argc,char* argv[])
{
if(argc != 3)
{
usage(argv[0]);
return 1;
}
int listen_sock = startup(argv[1],atoi(argv[2]));
int epfd = epoll_create(128);
if(epfd < 0)
{
perror("epoll_create");
return 5;
}
struct epoll_event ev;
ev.events = EPOLLIN;
ev.data.fd = listen_sock;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev) < 0)
{
perror("epoll_ctl");
return 6;
}
struct epoll_event evs[128];
int len = sizeof(evs)/sizeof(evs[0]);
int ready = 0;
int timeout = -1;
while(1)
{
switch(ready = epoll_wait(epfd,evs,len,timeout))
{
case 0:
printf("timeout..\n");
break;
case -1:
printf("epoll_wait");
break;
default:
{
int i = 0;
for(;i < ready;i++)
{
int fd = evs[i].data.fd;
if(i == 0 && fd == listen_sock && evs[i].events&EPOLLIN)
{
struct sockaddr_in peer;
socklen_t len = sizeof(peer);
int new_sock = accept(listen_sock,(struct sockaddr*)&peer,&len);
if(new_sock < 0)
{
perror("accept");
return 8;
}
else
{
//在epoll_wait之后改变其文件状态,为非阻塞,ET工作
set_no_block(new_sock);
printf("get new client:ip %s:port %d\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));
ev.events = EPOLLIN|EPOLLET;
ev.data.fd=new_sock;
if(epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev)<0)
{
perror("epoll_ctl");
return 9;
}
}
}
else
{
//read
if(evs[i].events&EPOLLIN)
{
char buf[1024];
ssize_t _s = my_read(fd,buf);
//ssize_t _s = recv(fd,buf,sizeof(buf)-1,0);
if(_s > 0)
{
buf[_s] = '\0';
printf("client # %s\n",buf);
ev.data.fd = fd;
ev.events = EPOLLOUT |EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev);
}
else if(_s == 0)
{
//client close
printf("client close...\n");
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
close(fd);
}
else
{
perror("my_read");
return 10;
}
}
else if(evs[i].events&EPOLLOUT)
{
char *msg = "HTTP/1.1 200 ok\r\n\r\n<html><h1>hello Comedly</h1></html>\r\n";
//send(fd,msg,sizeof(msg)-1);
//send(fd,msg,strlen(msg),0);
write(fd,msg,strlen(msg));
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
close(fd);
}
else
{
continue;
}
}
}
}
break;
}
}
return 0;
}
测试结果:
本文深入解析epoll的工作原理,探讨其两种主要模式:边缘触发(ET)与水平触发(LT),并提供实际测试代码示例。
931

被折叠的 条评论
为什么被折叠?



