1.epoll是什么:
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入就绪队列(Ready)的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait的调用,提高应用程序效率。
2.epoll,select,poll的区别:
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。
select和poll都需要在返回后,通过遍历文件描述符来获取已经就绪的socket。事实上,同时连接的大量客户端在一时刻可能只有很少的处于就绪状态,因此随着监视的描述符数量的增长,其效率也会线性下降。
select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点,事实上从现在看来,这也是它所剩不多的优点之一。
select的几大缺点:
(1)单个进程能够监视的文件描述符的数量存在最大限制,在Linux上一般为1024,不过可以通过修改宏定义甚至重新编译内核的方式提升这一限制。但是随着文件描述符数量的增大,其复制的开销也线性增长。同时,由于网络响应时间的延迟使得大量TCP连接处于非活跃状态,但调用select()会对所有socket进行一次线性扫描,所以这也浪费了一定的开销。
(2)每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
(3)同时每次调用select都需要在内核进行线性遍历,时间复杂度为O(N),这个开销在fd很多时也很大
poll的缺点:
它和select在本质上没有多大差别,但是poll没有最大文件描述符数量的限制(也就是没有select的缺点1,却有它的缺点2和3)。
poll和select同样存在一个缺点就是,包含大量文件描述符的数组被整体复制于用户态和内核的地址空间之间,而不论这些文件描述符是否就绪,它的开销随着文件描述符数量的增加而线性增大。
epoll的优点主要是以下几个方面:
1. 监视的描述符数量不受限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左 右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。 (我2G内存的机器上是239545)。select的最大缺点就是进程打开的fd是有数量限制的。
2. IO的效率不会随着监视fd的数量的增长而下降。epoll不同于select和poll轮询的方式,而是通过每个fd定义的回调函数来实现的。只有就绪的fd才会执行回调函数,时间复杂度为O(1)。
3. 支持电平触发和边沿触发两种方式(具体区别,看下面的2.2epoll详解-工作模式),理论上边缘触发的性能要更高一些,但是代码实现相当复杂。
4. mmap加速内核与用户空间的信息传递。epoll是通过内核于用户空间mmap同一块内存,避免了无谓的内存拷贝。
通过表格来看一下更清楚。
|
select |
poll |
epoll |
支持最大连接数 |
1024(2048) |
无上限 |
无上限 |
IO效率 |
每次调用进行线性遍历,时间复杂度为O(N) |
每次调用进行线性遍历,时间复杂度为O(N) |
使用“事件”通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到rdllist里面,这样epoll_wait返回的时候我们就拿到了就绪的fd。时间发复杂度O(1) |
fd拷贝 |
每次select都拷贝 |
每次poll都拷贝 |
调用epoll_ctl时拷贝进内核并由内核保存,之后每次epoll_wait不拷贝
|
二.epoll详解:
既然了解了epoll的基本概念和优点,那我们就来看看epoll怎么用。
1.epoll的接口:
epoll操作过程需要三个接口,分别如下:
#include
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
(1) int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。在linux-2.4.32内核中根据size大小初始化哈希表的大小,在linux2.6.10内核中该参数无用,使用红黑树管理所有的文件描述符,而不是hash。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件注册函数,它不同于select()在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
第一个参数epfd是epoll_create()的返回值;
第二个参数op表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中,
EPOLL_CTL_MOD:修改已经注册的fd的监听事件,
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数fd是需要监听的fd;
第四个参数*event是告诉内核需要监听什么事,struct epoll_event结构如下:
struct epoll_event {
__uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket(也就是第三个参数fd还是epoll_data.fd)的话,需要再次把这个socket加入到EPOLL队列里
(3) int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
epoll_wait运行的原理是
等侍注册在epfd上的socket fd的事件的发生,如果发生则将发生的sokct fd和事件类型放入到events数组中。
并 且将注册在epfd上的socket fd的事件类型给清空,所以如果下一个循环你还要关注这个socket fd的话,则需要用epoll_ctl(epfd,EPOLL_CTL_MOD,listenfd,&ev)来重新设置socket fd的事件类型。这时不用EPOLL_CTL_ADD,因为socket fd并未清空,只是事件类型清空。这一步非常重要。
2.epoll工作模式:
EPOLL事件有两种模型:
LT(level triggered-电平触发)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作或者未处理完,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
ET(edge triggered-边缘触发)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误),边缘触发的意思就是在两个状态的临界值进行改变的时候触发。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。
ET和LT的区别就在这里体现,LT事件不会丢弃,而是只要读buffer里面有数据可以让用户读,则不断的通知你。而ET则只在事件发生之时通知。可以简单理解为LT是水平触发,而ET则为边缘触发。LT模式只要有事件未处理就会触发,而ET则只在高低电平变换时(即状态从1到0或者0到1)触发。
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
三.epoll的例子:
代码github地址:https://github.com/majianfei/practice/tree/master/epoll
server:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_EPOLL 1000
#define BUF_SIZE 1024
#define PORT 6000
int setnonblocking( int fd )
{
if( fcntl( fd, F_SETFL, fcntl( fd, F_GETFD, 0 )|O_NONBLOCK ) == -1 )
{
printf("Set blocking error : %d\n", errno);
return -1;
}
return 0;
}
int main( int argc, char ** argv )
{
int listen_fd;
int conn_fd;
int epoll_fd;
int nread;
int i;
struct sockaddr_in servaddr;
struct sockaddr_in cliaddr;
struct epoll_event ev; //监听fd
struct epoll_event events[MAX_EPOLL];
char buf[BUF_SIZE];
socklen_t len = sizeof( struct sockaddr_in );
bzero( &servaddr, sizeof( servaddr ) );
servaddr.sin_family = AF_INET;
servaddr.sin_addr.s_addr = htonl( INADDR_ANY );
servaddr.sin_port = htons( PORT );
if( ( listen_fd = socket( AF_INET, SOCK_STREAM, 0 ) ) == -1 )
{
printf("socket error...\n" , errno );
exit( EXIT_FAILURE );
}
if( setnonblocking( listen_fd ) == -1 )
{
printf("setnonblocking error : %d\n", errno);
exit( EXIT_FAILURE );
}
if( bind( listen_fd, ( struct sockaddr *)&servaddr, sizeof( struct sockaddr ) ) == -1 )
{
printf("bind error : %d\n", errno);
exit( EXIT_FAILURE );
}
if( listen( listen_fd, 10 ) == -1 )
{
printf("Listen Error : %d\n", errno);
exit( EXIT_FAILURE );
}
epoll_fd = epoll_create( MAX_EPOLL ); //1.epoll_create
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = listen_fd;
if( epoll_ctl( epoll_fd, EPOLL_CTL_ADD, listen_fd, &ev ) < 0 ) //2.epoll_ctl
{
printf("Epoll Error : %d\n", errno);
exit( EXIT_FAILURE );
}
while( 1 )
{
int ready_counts = 0;
if( ( ready_counts = epoll_wait( epoll_fd, events, MAX_EPOLL, -1 ) ) == -1 ) //3.epoll_wait,就绪的event在events里面
{
printf( "Epoll Wait Error : %d\n", errno );
exit( EXIT_FAILURE );
}
for( i = 0; i < ready_counts; i++ )
{
if( events[i].data.fd == listen_fd) //监听端口有就绪事件
{
if( ( conn_fd = accept( listen_fd, (struct sockaddr *)&cliaddr, &len ) ) == -1 )
{
printf("Accept Error : %d\n", errno);
exit( EXIT_FAILURE );
}
printf( "Server get from client !\n"/*, inet_ntoa(cliaddr.sin_addr), cliaddr.sin_port */);
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_fd;
if( epoll_ctl( epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev ) < 0 )
{
printf("Epoll Error : %d\n", errno);
exit( EXIT_FAILURE );
}
}
else
{
nread = read( events[i].data.fd, buf, sizeof( buf ) );
if( nread <= 0 )
{
close( events[i].data.fd );
epoll_ctl( epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, &ev );
continue;
}
write( events[i].data.fd, buf, nread );
}
}
}
close( listen_fd );
return 0;
}
client:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MAX_EPOLL 1000
#define BUF_SIZE 1024
#define PORT 6000
int setnonblocking( int fd )
{
if( fcntl( fd, F_SETFL, fcntl( fd, F_GETFD, 0 )|O_NONBLOCK ) == -1 )
{
printf("Set blocking error : %d\n", errno);
return -1;
}
return 0;
}
int main( int argc, char ** argv )
{
int conn_fd;
int epoll_fd;
int nread;
int i;
struct sockaddr_in cliaddr;
struct epoll_event ev; //监听fd
struct epoll_event events[MAX_EPOLL];
char buf[BUF_SIZE];
socklen_t len = sizeof( struct sockaddr_in );
bzero( &cliaddr, sizeof( cliaddr ) );
cliaddr.sin_family = AF_INET;
cliaddr.sin_addr.s_addr = inet_addr("127.0.0.1");
cliaddr.sin_port = htons( PORT );
if( ( conn_fd = socket( AF_INET, SOCK_STREAM, 0 ) ) == -1 )
{
printf("socket error : %d\n" , errno );
exit( EXIT_FAILURE );
}
if( setnonblocking( conn_fd ) == -1 )
{
printf("setnonblocking error : %d\n", errno);
exit( EXIT_FAILURE );
}
int r = connect(conn_fd, (struct sockaddr*)&cliaddr, sizeof(cliaddr));
epoll_fd = epoll_create( MAX_EPOLL ); //1.epoll_create
//ev.events = EPOLLIN | EPOLLET;
ev.events = EPOLLOUT; //监听可写事件,代表连接成功,然后监听可读事件。
ev.data.fd = conn_fd;
if( epoll_ctl( epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev ) < 0 ) //2.epoll_ctl
{
printf("Epoll Error : %d\n", errno);
exit( EXIT_FAILURE );
}
while( 1 )
{
int ready_counts = 0;
if( ( ready_counts = epoll_wait( epoll_fd, events, MAX_EPOLL, -1 ) ) == -1 ) //3.epoll_wait,就绪的event在events里面
{
printf( "Epoll Wait Error : %d\n", errno );
exit( EXIT_FAILURE );
}
for( i = 0; i < ready_counts; i++ )
{
if( events[i].data.fd == conn_fd) //监听端口有就绪事件
{
if (events[i].events & EPOLLOUT){
printf("Connect success\n");
if( epoll_ctl( epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, &ev ) )
{
printf("Epoll Error : %d\n", errno);
exit( EXIT_FAILURE );
}
ev.events = EPOLLIN;
ev.data.fd = conn_fd;
if( epoll_ctl( epoll_fd, EPOLL_CTL_ADD, conn_fd, &ev ) < 0 )
{
printf("Epoll Error : %d\n", errno);
exit( EXIT_FAILURE );
}
char write_buf[] = "abcdefgh";
write( events[i].data.fd, write_buf, sizeof(write_buf));
}
else if (events[i].events & EPOLLIN){
printf("Read data\n");
nread = read( events[i].data.fd, buf, sizeof( buf ) );
if( nread <= 0 )
{
//close( events[i].data.fd );
//epoll_ctl( epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, &ev );
printf("Close Fd\n");
continue;
}
//write( events[i].data.fd, buf, nread );
}
}
}
}
close(conn_fd);
return 0;
}
四:总结:
既然epoll有这么多优点,是不是可以取代select和poll。
什么情况下用select/poll而不用epoll呢?
1.epoll更适合于处理大量的fd ,且活跃fd不是很多的情况。
反之如果处理的fd量不大,且基本都是活跃的,epoll并不比select/poll有什么效率。相反,过多使用epoll_ctl,效率相比还有稍微的下降。这时候使用select既简单又方便,就没必要用epoll这么复杂的写法了。
2.select目前几乎在所有的平台上支持,其良好跨平台支持也是它的一个优点