由于select/poll存在着高并发下效率低,监视数目受限的缺点,基于poll改进的epoll得以出现。
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。无须遍历整个被侦听的描述符集,只须遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
epoll句柄创建函数:
int epfd=int epoll_create(int size);
szie为监听的数目,自从linux2.6.8之后,size参数是被忽略的,一旦创建,便会占用一个fd值,记得在用完epoll之后close。
epoll事件注册函数:
int epoll_ctl(int epfd,int op,int fd,struct epoll_event *event);
epfd为创建的句柄
op表示动作,用三个宏表示:
EPOLL_CTL_ADD:注册新的fd到epfd中。
EPOLL_CTL_MOD:修改已经注册的fd的监听事件。
EPOLL_CTL_DEL:从epfd中删除一个fd。
fd为op动作的对象。
*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_data_t data;
}
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
等待fd状态:
int epoll_wait(int epfd,struct epoll_event *events,int maxevents,int timeout);
epfd为创建的句柄
*events里存储的是已准备好的fd及其状态
maxevents告诉内核events有多大,不能大于size,如果maxevents=3,就有events[0],events[1],events[2],若maxevents小于同一时间要处理的事件数,则多出来的事件会在下一次wait时处理
timeout为超时时间,为0立即返回,为-1不确定返回时间(也说是永久阻塞)
工作原理:
epoll同样只告知那些就绪的文件描述符,当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值。我们需要去wait中指定的数组中找准备好的描述符。
工作方式:
epoll分为水平触发(LT)和边缘触发(ET),水平触发是epoll默认的工作方式。
如果我们将描述符准备就绪设置为1,没有准备就绪设置为0。当从0变为1时,内核会告诉你描述符准备好,此时可进行操作,此后只要描述符状态不从1变为0,内核就会一直告诉你描述符准备好,可一直操作描述符,这就是水平触发。而从0变为1时,内核会告诉你描述符准备好,此时可操作,此后直到新的事件到来(0变1),内核不会告诉你描述符是否准备好,这便是边缘触发。如果边缘触发时,没有完全处理完缓冲区的数据,会导致用户的请求得不到相应(毕竟只有一次处理的机会)。
水平触发出错的可能性要小些,但速度也要慢些,支持所有描述符。边缘触发速度快,但易出错,且只支持非阻塞的描述符。
与select/poll不同之处:
select/poll需要将各描述符的状态传入内核中调用相关的函数(select或poll),再从内核中传出来给程序。而epoll在调用内核函数epoll_wait时,直接从之前创建的句柄epfd中拿便是,不需要进内核又出内核。因为各文件描述符的信息都注册在epfd中,可直接用。
server大体框架:
struct epoll_event event;
struct epoll_event *events;
epfd=epoll_create(MAX_SIZE);//创建句柄,设置监听的最大数目(受进程能打开的最大的描述符数目影响)出错返回-1
event.events=...;//设置关心的描述符的哪个事件
event.data.fd=listenfd(监听描述符);//设置关心的描述符
epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&event);//出错返回-1
events = calloc (size, sizeof (events));//为events分配内存空间,用于存放已准备好的fd及其状态,不分配会报错
for( ; ; )
{
readynum=epoll_wait(epfd,events,MAXEVENTS,TIMEOUT);//等待描述符状态(此处的MAXEVENTS,TIMEOUT为设置的宏)
for(i=0;i<readynum;i++)
{
if ((events[i].events & EPOLLERR)||(events[i].events & EPOLLHUP)||(!(events[i].events & EPOLLIN)))//当发生错误或挂断或不可读时,提示错误
{
......//此处为发生错误要进行的操作
}
else if(listenfd==events[i].data.fd)//当监听描述符准备好,进行以下操作
{
event.events=...;
event.data.fd=connfd(连接描述符);
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&event);//将connfd加入到epfd中
continue;//跳出for(i=0;i<n;i++)当前循环
}
else if(connfd==events[i].data.fd)//由于当listenfd相应时,添加了connfd,故下一次的for循环中有了connfd的状态选择
{
.......//cs数据传输操作
}
}
}
......
close(epfd);//记得关闭epfd
free(events);//记得释放events所占的内存空间
......//其他该关闭的fd及操作