epoll编程接口
一、概述
同I/O多路复用和信号驱动一样,Linux的epoll函数可以检查多个文件描述符上的I/O就绪状态,epoll函数的优点:
-
当检查大量文件描述符时,epoll的性能比select和poll函数高很多。
-
epoll既支持水平触发也支持边缘触发,而select和poll支持水平触发,信号驱动只支持边缘触发。
-
epoll性能和信号驱动I/O类似,但epoll可避免复杂的信号处理流程,可以指定检查的事件类型。
epoll把用户关心的文件描述符上的事件放在内核的一个事件表中,epoll的核心数据结构称为epoll实例,它和一个打开的文件描述符相关联,这个文件描述符不是用来I/O操作的,是内核数据结构的句柄,这些内核数据结构实现的两个目的:
-
记录在进程中声明过的感兴趣的文件描述符列表(interest list-兴趣列表)。
-
维护了处于I/O就绪态的文件描述符列表(ready list-就绪列表)。
ready list中的成员是interest list的子集。
对于由epoll检测的每一个文件描述符,我们可以指定一个位掩码来表示我们感兴趣的事件。这些位掩码同poll()所使用的位掩码有紧密的关联。
epoll API由下面3个系统调用组成:
-
系统调用
epoll_create()
创建一个epoll实例,返回代表该实例的文件描述符。 -
系统调用
epoll_ctl()
操作同epol 实例相关联的兴趣列表。通过epoll_ctl()
,可以增加新的描述符到列表中,将已有的文件描述符从该列表中移除,以及修改代表文件描述符上事件类型的位掩码。 -
系统调用
epoll_wait()
返回与 epoll 实例相关联的就绪列表中的成员。
二、epoll_create函数
此函数用于创建额为文件描述符表示内核中的事件表。
#include<sys/epoll.h>
int epoll_create(int size);
size参数并不起作用,只是给内核一个提示,告诉事件表需要多大。该函数返回的文件描述符将用作其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。
三、epoll_ctl函数
此函数用于操作epoll的内核事件表。
#include<sys/epoll.h>
int epoll_ctl(int epfd,int op,int fd,struct epoll_enent *event);
//fd:操作的文件描述符
//成功返回0,出错返回-1并设置errno
op指定操作类型,操作类型属性值如下:
EPOLL_CTL_ADD
:往事件表中注册fd上的事件。EPOLL_CTL_MOD
:修改fd上的注册事件。EPOLL_CTL_DEL
:删除fd上的注册事件。
event指定事件,指向epoll_evnet结构类型:
struct epoll_event{
_uint32_t events;//epoll事件
epoll_data data;//用户数据
};
-
events成员描述事件类型,epoll支持的事件类型和poll基本相同。
-
表示epoll事件类型的宏是在poll对应的宏前加上
"E"
,比如epoll的数据可读事件是EPOLLIN
。 -
epoll有两个额外的事件类型一
EPOLLET和EPOLLONESHOT
,它们对于epoll的高效运作非常关键。
data成员用于存储用户数据,类型epoll_data_t的定义如下:
typedef union epoll_data{
void *ptr;//指定与fd相关的用户数据。
int fd;//指定事件所从属的目标文件描述符。
uint32_t u32;
uint64_t u64;
}epoll_data_t;//联合体
由于epoll_data_t
是联合体,不能同时使用其ptr成员和fd成员,若需将文件描述符和用户数据关联起来,实现快速的数据访问,只能使用其他手段,如放弃fd成员,而在ptr指向的用户数据中包含fd。
通过max_user_watches
文件查看文件描述符总数,避免上限。
四、epoll_wait函数
此函数作用是在一段超时时间内等待一组文件描述符上的事件。
#include<sys/epoll.h>
int epoll_wait(int epfd,struct epoll_event *event,int maxevents,int timeout);
//maxevents:指定最多监听多少事件,必须大于0
//成功返回文件描述符个数,timeout超时间隔内没有任何文件描述符处于就绪态,返回0,出错返回-1设置errno。
timeout用来确定epoll_wait
函数的阻塞行为:
- 若timeout 等于−1,调用将一直阻塞,直到兴趣列表中的文件描述符上有事件产生,或者直到捕获到一个信号为止。
- 若timeout 等于 0,执行一次非阻塞式的检查,看兴趣列表中的文件描述符上产生了哪个事件。
- 若timeout 大于 0,调用将阻塞至多 timeout 毫秒,直到文件描述符上有事件发生,或者直到捕获到一个信号为止。
五、poll和epoll使用上的区别
//如何索引poll返回的就绪文件描述符
int ret = poll(fds, MAX_EVENT_NUMBER, -1);
//必须遍历所有已注册文件描述符并找到其中的就绪者(当然,可以利用ret来稍做优化)
for(int i= 0; i < MAX_EVENT_NUMBER; ++i)
{
if(fds[i].revents & POLLIN)//判断第i个文件描述符是否就绪
{
int sockfd = fds[i].fd;
//处理sockfd
}
}
//如何索引epoll返回的就绪文件描述符
int ret = epoll_wait(epollfd,events,MAX_EVENT_NUMBER,-1);
//仅遍历就绪的ret个文件描述符
for(int i=0;i<ret;i++)
{
int sockfd = events[i].data.fd;
//sockfd肯定就绪,直接处理
六、LT和ET模式
- LT(水平触发模式):如果文件描述符上可以非阻塞地执行I/O系统调用,此时认为它已经就绪。默认的工作模式阻塞和非阻塞都支持,相当于效率较高的poll。
- ET(边缘触发通知):如果文件描述符自上次状态检查以来有了新的I/O活动(新的输入),此时需要触发通知,只支持非阻塞。epoll的高效工作模式。
下表总结了I/O多路复用,信号驱动I/O以及epoll所采用的通知模型:
采用LT工作模式的文件描述符,当epoll_wait
检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理该事件。当应用程序下一次调用epoll_wait
时,epoll_wait
还会再次向应用程序通知此事件,知道该事件被处理。
采用ET工作模式的文件描述符,当epoll_wait
检测到其上有事件发生并将事件通知应用程序后,应用程序必须立即处理该事件,因为后续的epoll_wait
调用不再向应用程序通知这一事件。
ET模式降低了同一个epoll事件被重复触发的次数,因此效率比LT模式高。
-
对于监听的 sockfd,最好使用水平触发模式,边缘触发模式会导致高并发情况下,有的客户端会连接不上。如果非要使用边缘触发,可以用 while 来循环 accept()。
-
对于读写的 connfd,水平触发模式下,阻塞和非阻塞效果都一样,因为在阻塞模式下,如果数据读取不完全则返回继续触发,反之读取完则返回继续等待。全建议设置非阻塞。
-
对于读写的 connfd,边缘触发模式下,必须使用非阻塞 IO,并要求一次性地完整读写全部数据。
LT模式下,不需要读写的事件要及时移除,避免不必要的触发,浪费CPU资源;ET模式下,读写事件触发后,如果还需要得到为读写完的数据,就要及时再一次注册可读写事件
1.LT和ET工作上的差异
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <pthread.h>
#define MAX_EVENT_NUMBER 1024
#define BUFFER_SIZE 10
//将文件描述符设置成非阻塞的
int setnonblocking( int fd )
{
int old_option = fcntl( fd, F_GETFL );
int new_option = old_option | O_NONBLOCK;
fcntl( fd, F_SETFL, new_option );
return old_option;
}
/*将文件描述符fd上的EPOLLIN注册到epollfd指示的epoll内核事件表中,参数enable_et指定是否对fd启用ET模式*/
void addfd( int epollfd, int fd, bool enable_et )
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN;
if( enable_et )
{
event.events |= EPOLLET;
}
epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
setnonblocking( fd );
}
//LT模式的工作流程
void lt( epoll_event* events, int number, int epollfd, int listenfd )
{
char buf[ BUFFER_SIZE ];
for ( int i = 0; i < number; i++ )
{
int sockfd = events[i].data.fd;
if ( sockfd == listenfd )
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
addfd( epollfd, connfd, false );
}
else if ( events[i].events & EPOLLIN )
{
printf( "event trigger once\n" );
memset( buf, '\0', BUFFER_SIZE );
int ret = recv( sockfd, buf, BUFFER_SIZE-1, 0 );
if( ret <= 0 )
{
close( sockfd );
continue;
}
printf( "get %d bytes of content: %s\n", ret, buf );
}
else
{
printf( "something else happened \n" );
}
}
}
//ET模式的工作流程
void et( epoll_event* events, int number, int epollfd, int listenfd )
{
char buf[ BUFFER_SIZE ];
for ( int i = 0; i < number; i++ )
{
int sockfd = events[i].data.fd;
if ( sockfd == listenfd )
{
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof( client_address );
int connfd = accept( listenfd, ( struct sockaddr* )&client_address, &client_addrlength );
addfd