EPOLL纸上谈兵

之所以是纸上谈兵,是因为在工作中写的服务器架构中,还是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;
}



  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值