之所以是纸上谈兵,是因为在工作中写的服务器架构中,还是select/poll,没有尝试过epoll,下一次尝试一下。
比较好的几个讨论:
论epoll的使用:http://www.cppblog.com/peakflys/archive/2012/08/26/188344.html
浅谈服务器单I/O线程+工作者线程池模型架构及实现要点: http://www.cnblogs.com/ccdev/p/3542669.html
select、poll、epoll之间的区别总结: http://www.cnblogs.com/Anker/p/3265058.html
三个函数:
1. int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的 数目一共有多大。需要注意的是,当创建好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()是在监听事件时告诉内核要监听什么类型的事件,而是先注册要监听的事件类型。
第一个参数是epoll_create()的返回值,
第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,
第四个参数是告诉内核需要监听什么事,struct 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 */
};
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents最多监听事件的数量,maxevents的值不能大于创建epoll_create(int size)时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
两种模型:
Edge Triggered (ET)
只有在监视的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件,则只是汇报一次。必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
i 基于非阻塞文件句柄
ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。
但这并不是说每次read()时都需要循环读,直到读到产生一个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有数据了,也就可以认为此事读事件已处理完成。
Level Triggered (LT)
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.
在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如广域网环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。
使用总结
1.EPOLL 默认是水平触发模式LT, 如果要使用边缘触发模式ET,要将listen的socket事件加上EPOLLET
ev.data.fd = listen_fd;ev.events = EPOLLIN | EPOLLOUT | EPOLLHUP | EPOLLERR | EPOLLET;
//add server socket fd in to event set
epoll_ctl( epfd, EPOLL_CTL_ADD, listen_fd, &ev );
2.需要设置非阻塞
int opts;
opts = fcntl( sock_fd, F_GETFL );
if( opts < 0 )
{
printf("err:fcntl F_GETFL");
exit( 1 );
}
opts = opts | O_NONBLOCK ;
if( fcntl( sock_fd, F_SETFL, opts ) < 0 )
{
printf("err:fcntl F_SETFL");
exit( 1 );
}
3.如果是以LT方式处理,只要数据未处理完(send、recv内核缓冲有数据)就会不停的收到 EPOLLIN/EPOLLOU消息(已设置监听)。如果是以ET方式处理,事件只会通知一次,如数据到达可接收,需要使用while循环来接入数据。同样可发送也只会通常一次。send/recv都以收到EAGAIN为标识,如果还没处理完,才会现收到事件通知。
注意EPOLLOUT事件,如果是LT模式,只要send缓冲区空闲就会一直有这个事件通知,程序需要判断是否有数据可发送。如果是ET模式,只有在accept一个socket时触发一次,除非发送,缓冲区满了,再变为空闲时,才会再通知一次
4 ET模式EPOLLOUT和EPOLLIN触发时刻
ET模式称为边缘触发模式,顾名思义,不到边缘情况,是死都不会触发的。
EPOLLOUT事件:EPOLLOUT事件只有在连接时触发一次,表示可写,其他时候想要触发,那你要先准备好下面条件:
1.某次write,写满了发送缓冲区,返回错误码为EAGAIN。
2.对端读取了一些数据,又重新可写了,此时会触发EPOLLOUT。
简单地说:EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!
其实,如果你真的想强制触发一次,也是有办法的,直接调用epoll_ctl重新设置一下event就可以了,event跟原来的设置一模一样都行(但必须包含EPOLLOUT),关键是重新设置,就会马上触发一次EPOLLOUT事件。
EPOLLIN事件:EPOLLIN事件则只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。
其余要点:
1、如果fd被注册到两个epoll中时,如果fd有事件发生则两个epoll都会触发事件。
2、如果注册到epoll中的fd被关闭,则其会自动被清除出epoll监听列表。
3、如果多个事件同时触发epoll,则多个事件会被联合在一起返回。
4、epoll_wait会一直监听EPOLLHUP事件发生,所以其不需要添加到events中。
5、为了避免大数据量io时,ET模式下只处理一个fd,其他fd被饿死的情况发生。linux建议可以在fd联系到的结构中增加ready位,然后epoll_wait触发事件之后仅将其置位为ready模式,然后在循环检测所有事件后轮询ready fd列表。
示例:
Server
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <memory.h>
#include <pthread.h>
#include <errno.h>
#define MAX_LINE_LEN 1024
#define OPEN_MAX 256
#define SVR_PORT 17143
struct STask
{
int fd;
struct STask * next;
};
struct SData
{
int fd;
int size;
char data[ MAX_LINE_LEN ];
};
int epfd;
struct epoll_event ev, events[20];
void setNonBlock( int sock_fd )
{
int opts;
opts = fcntl( sock_fd, F_GETFL );
if( opts < 0 )
{
printf("err:fcntl F_GETFL");
exit( 1 );
}
opts = opts | O_NONBLOCK ;
if( fcntl( sock_fd, F_SETFL, opts ) < 0 )
{
printf("err:fcntl F_SETFL");
exit( 1 );
}
}
bool setSockReuse( int sock_fd )
{
int flags = 1;
if(setsockopt( sock_fd , SOL_SOCKET, SO_REUSEADDR, &flags, sizeof(flags)) == -1)
{
return false;
}
printf("set sock reuse ok.\n");
return true;
}
void OnAccept( int conn_fd, sockaddr_in * cln_addr )
{
setNonBlock( conn_fd );
char * str = inet_ntoa( cln_addr->sin_addr );
printf("connect_from:%s\n", str);
//add to epoll event for reading event
ev.data.fd = conn_fd;
ev.events = EPOLLIN | EPOLLOUT | EPOLLET;
epoll_ctl( epfd, EPOLL_CTL_ADD, conn_fd, &ev );
}
void OnRead( epoll_event * aEv )
{
int n;
struct SData * newData = (struct SData *)malloc( sizeof(struct SData ) );
newData->fd = aEv->data.fd;
int bRead = 1;
int recv_len = 0;
memset( newData->data, 0, MAX_LINE_LEN );
while( bRead )
{
n = read( aEv->data.fd, newData->data + recv_len, 5 );
printf("read return n:%d\n",n);
recv_len += n;
if( n < 0 )
{
if( errno == ECONNRESET ) //connection reset
{
close( aEv->data.fd );
aEv->data.fd = -1;
break;
}
else if( errno == EAGAIN ) //read interupt(no data in buf)
{
printf("EAGAIN\n");
break;
}
else if( errno == EWOULDBLOCK ) //operation would block
{
perror("recv not over...\n");
}
else
{
printf("read err");
break;
}
}
else if( n == 0 )
{
close( aEv->data.fd );
aEv->data.fd = -1;
break;
}
}
newData->data[ recv_len ] = '\n';
printf("read: %s", newData->data );
free( newData );
//trigger EPOLLOUT event
ev.data.fd = aEv->data.fd;
ev.events = EPOLLIN | EPOLLOUT | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, aEv->data.fd ,&ev);
}
void OnWrite( epoll_event * aEv )
{
struct SData * newData = (struct SData *)malloc( sizeof(struct SData ) );
newData->fd = aEv->data.fd;
newData->size = snprintf( newData->data, MAX_LINE_LEN, "hello! fd=%d\n", aEv->data.fd );
if( 0 != newData->size )
{
printf("send back: %s\n", newData->data );
write( aEv->data.fd , newData->data, newData->size );
}
free( newData );
}
int main()
{
int i;
int listen_fd, conn_fd,sock_fd, nfds;
epfd = epoll_create( OPEN_MAX );
struct sockaddr_in svrAddr;
struct sockaddr_in clnAddr;
socklen_t clnLen;
listen_fd = socket( AF_INET, SOCK_STREAM, 0);
setNonBlock( listen_fd );
setSockReuse( listen_fd );
ev.data.fd = listen_fd;
ev.events = EPOLLIN | EPOLLET | EPOLLOUT | EPOLLHUP | EPOLLERR;
//add server socket fd in to event set
epoll_ctl( epfd, EPOLL_CTL_ADD, listen_fd, &ev );
memset( &svrAddr, 0 , sizeof( sockaddr_in ) );
inet_aton( "127.0.0.1", &svrAddr.sin_addr );
svrAddr.sin_family = AF_INET;
svrAddr.sin_port = htons( SVR_PORT );
int ret;
ret = bind( listen_fd, (sockaddr*)&svrAddr, sizeof( sockaddr_in ) );
if( ret != 0 )
{
perror( "bind fail" );
exit(-3);
}
listen( listen_fd, 20 );
for(;;)
{
nfds = epoll_wait( epfd, events, 20, 500 );
//maybe serval event arrive
for(i=0; i<nfds;++i)
{
if( events[i].data.fd == listen_fd )
{
printf("event accept.\n");
clnLen = sizeof( sockaddr_in );
memset( &clnAddr, 0 , sizeof( sockaddr_in ) );
conn_fd = accept( listen_fd, (sockaddr*)&clnAddr, &clnLen );
if( conn_fd < 0 )
{
if( errno == EWOULDBLOCK )
printf("EWOULDBLOCK\n");
perror("conn_fd < 0");
break;
}
OnAccept( conn_fd, &clnAddr );
}
else if( events[i].events & EPOLLIN ) //对应的文件描述符可以读(包括对端SOCKET正常关闭)
{
printf("event EPOLLIN.\n");
if( ( sock_fd = events[i].data.fd ) < 0 )
continue;
OnRead( &events[i] );
}
else if( events[i].events & EPOLLOUT ) //对应的文件描述符可以写
{
printf("event EPOLLOUT.\n");
if( ( sock_fd = events[i].data.fd ) < 0 )
continue;
OnWrite( &events[i] );
}
else if( events[i].events & EPOLLHUP ) //对应的文件描述符被挂断
{
printf("event EPOLLHUP. fd=%d.\n", events[i].data.fd );
}
else if( events[i].events & EPOLLERR ) //对应的文件描述符发生错误
{
printf("event EPOLLERR. fd=%d.\n", events[i].data.fd );
}
}
}
close(epfd);
return 0;
}
Clinet
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
int main(int argc, char *argv[])
{
if (argc < 3)
{
fprintf(stderr,"Please enter the server's hostname!\n");
return -1;
}
int sockfd;
if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1)
{
perror("socket bind");
return -2;
}
printf("IP: %s, PORT: %d\n", argv[1], atoi(argv[2]));
struct sockaddr_in serv_addr;
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr(argv[1]);
serv_addr.sin_port = htons(atoi(argv[2]));
if (connect(sockfd, (struct sockaddr *)&serv_addr, sizeof(struct sockaddr)) == -1)
{
perror("connect error");
return -3;
}
const char* str="Hello World";
if (send(sockfd, str, strlen(str)+1, 0) == -1)
{
perror("send error");
}
char buf[1024];
for(int i = 0; i != 2; i++)
{
int rn = read(sockfd, buf, 1024);
buf[rn] = '\0';
printf("%d: %s\n", i, buf);
send(sockfd, buf, strlen(buf), 0);
sleep(1);
}
close(sockfd);
return 0;
}