epoll机制

在linux的网络编程中,很长的时间都在使用select来做事件触发。在linux新的内核中,有了一种替换它的机制,就是epoll。相比于select,epoll最大的好处在于:

1、epoll不会随着监听fd数目的增长而降低效率。因为在内核中的实现中,select是采用轮询来处理的,轮询的fd数目越多,自然耗时越多。

2、select同时监听文件描述符数受限,并且修改select监听文件描述符数要困难些。

linux/posix_types.h头文件有这样的声明:
          表示select最多同时监听1024个fd,当然,可以通过修改头文件再重编译内核来扩大这个数目,但这似乎并不治本。

#define __FD_SETSIZE    1024

3、epoll使用mmap加速内核与用户空间的消息传递。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。

epoll和select是I/O多路复用,可以同时处理不同的文件描述符类型,包括客户端、服务端、TCP和UDP等。

epoll机制的简介


epoll的接口非常简单,一共就三个函数:


1.创建epoll句柄
   int  epfd = epoll_create(int size);                                                                   

        创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值(不是fd数目+1)。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。


2.将被监听的描述符添加到epoll句柄或从epoll句柄中删除或者对监听事件进行修改。

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 

    epoll的事件注册函数,它不同于select(),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 */
          };


          联合体epoll_data_t中,一般用fd,但也可以用ptr指向自己要使用的结构或者字符串等。

         events可以是以下几个宏的集合:
         EPOLLIN :
             触发该事件,表示对应的文件描述符上有可读数据。(包括对端SOCKET正常关闭);
EPOLLIN事件只有当对端有数据写入时才会触发,所以触发一次后需要不断读取所有数据直到读完EAGAIN为止。否则剩下的数据只有在下次对端有写入时才能一起取出来了。
现在明白为什么说 epoll必须要求异步socket了吧?如果同步socket,而且要求读完所有数据,那么最终就会堵死在阻塞里。

ET模式:

    每次epoll_ctl做EPOLL_CTL_ADD或EPOLL_CTL_MOD时,如果加入前,就是可读状态,那么加入后会触发1次。
    不管socket缓冲是否读完,只要对方有send或connect或close或强退,epoll_wait就会触发EPOLLIN。

LT模式:只要socket可读,每次epoll_wait就会一直触发EPOLLIN。


          EPOLLOUT:          触发该事件,表示对应的文件描述符上可以写数据;
                                            

ET模式下,EPOLLOUT事件只有在连接时触发一次,表示可写(这句话的准确意思如下:

服务端:

在负责listen的socket上同时注册EPOLLIN | EPOLLOUT,收到connet请求时,只看到EPOLLIN事件。在accectp后的socket上同时注册EPOLLIN | EPOLLOUT,这时候客户端还没有操作,这时只发生了EPOLLOUT事件。

客户端的:

socket fd同时注册EPOLLIN | EPOLLOUT,会触发一次EPOLLOUT。

他时候想要触发,那你要先准备好下面条件:

1、(本地)某次write,写满了发送缓冲区,返回错误码为EAGAIN。

2、对端读取了一些数据,本地又重新可写了,此时会触发EPOLLOUT。

简单地说:

ET模式下,EPOLLOUT事件只有在不可写到可写的转变时刻,才会触发一次,所以叫边缘触发,这叫法没错的!

其实,如果你真的想强制触发一次,也是有办法的:每次epoll_ctl做EPOLL_CTL_ADD或EPOLL_CTL_MOD时,如果加入前,就是可写状态,那么加入后会触发1次。

对于LT 模式,如果注册了EPOLLOUT,只要该socket可写(发送缓冲区)未满,那么epoll_wait就会触发EPOLLOUT。


         EPOLLPRI:            表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
         EPOLLERR:         表示对应的文件描述符发生错误;
          EPOLLHUP:         表示对应的文件描述符被挂断;
         EPOLLET:            将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
         EPOLLONESHOT:   只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

3. 等待事件触发,当超过timeout还没有事件触发时,就超时。
    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表示已超时,-1表示出错了,可以查看errno来了解具体错误。
    返回的事件集合在events数组中,数组中实际存放的成员个数是函数的返回值。


ET和LT工作模式

EPOLL事件有两种模型:

Edge Triggered (ET)       //高速工作方式,只支持no_block socket  (非阻塞socket)

Level Triggered (LT)       //缺省工作方式,即默认的工作方式,支持block socket和no_block socket。

假如有这样一个例子:

1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符

2. 这个时候从管道的另一端被写入了2KB的数据

3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作

4. 然后我们读取了1KB的数据

5. 调用epoll_wait(2)......

上例中,第5步时,LT方式下,内核会继续通知可以读数据;ET方式,内核不会再通知可以读数据。


Edge Triggered 工作模式:

        上例中,如果我们在第1步将RFD添加到epoll描述符的时候,使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂起因为剩余的数据还存在于接收端的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。但是接收端只有在监视的文件句柄上发生了某个事件的时候,ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待已经存在于文件输入缓冲区内的剩余数据。

在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,事件将会在第3步被销毁。由于第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成后,会永远挂起。

epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作,把处理多个文件描述符的任务饿死。因为数据读写不需要用到CPU,而是使用DMA。所以,非阻塞读的方式,CPU可以处理其他连接,而阻塞读的方式就会把处理其他文件描述符的任务饿死。

最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。(LT方式可以解决这种缺陷)
   i    基于非阻塞文件句柄。
   ii   只有当read(2)或者write(2)返回EAGAIN时(认为读完)才需要挂起,等待。

ET方式,读必须判断是不是都读完了。如果read()返回的读到的数据长度等于要求的大小,即sizeof(buf),那么很有可能是缓冲区还有数据未读完,也意味着该次事件还没有处理完,所以还需要再次读取:

while(rs)           //ET模型  
{  
    buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);  
    if(buflen < 0)  
    {  
        // 由于是非阻塞的模式,所以当errno为EAGAIN时,表示当前缓冲区  
        // 已无数据可读  
        // 在这里就当作是该次事件已处理完.  
        // 即当buflen<0且errno=EAGAIN时,表示没有数据了。(读/写都是这样)  
        if(errno == EAGAIN || errno == EINT)    
            break;  
        else  
            return;                 //真的失败了。  
    }  
    else if(buflen == 0)  
    {  
        // 这里表示对端的socket已正常关闭.  
    }  
    if(buflen == sizeof(buf)  
        rs = 1;    // 需要再次读取(有可能是因为数据缓冲区buf太小,所以数据没有读完)  
    else  
        rs = 0;     //不需要再次读取(当buflen<sizeof(buf)时,这是ET模式下,非阻塞文件描述符的特性),  
}  


ET方式,写必须要关注返回值,当返回值小于实际要发送的数量,由于ET方式只有在不可写到可写才触发,所以也要跟读一样,循环发送,直到出现EAGAIN(说明发送完成或者发送缓冲区已满)。然后在EPOLL_OUT中发送剩余消息。所以要把发送数据和其他信息保存起来。

ET方式发送示例:

struct unixsock_info_t  
{  
    struct sockaddr_un *p_addrun_client;  
    char psz_sendbuf[8];  
    uint32_t ui_sendlen;  
};  
typedef struct unixsock_info_t unixsock_info_t;  
  
static int unixsock_sendall(   
int i_unixfd,   
unixsock_info_t *p_unixsock_info )  
  
{  
    if ( !p_unixsock_info )  
        return -1;  
      
    struct sockaddr_un *p_addrun_client = p_unixsock_info->p_addrun_client;  
    int i_addrlen = strlen( p_addrun_client->sun_path ) +   
        sizeof( p_addrun_client->sun_family );  
    int i_sendlen = 0;  
    char *psz_sendbuf = p_unixsock_info->psz_sendbuf;  
  
    if ( !psz_sendbuf )  
    {  
        log_Err( "psz_sendbuf is null" );  
        return -1;  
    }  
    while( true )  
    {  
        i_sendlen = sendto( i_unixfd, p_unixsock_info->psz_sendbuf,   
            p_unixsock_info->ui_sendlen, 0, ( struct sockaddr * )p_addrun_client,   
            i_addrlen );  
        if ( i_sendlen < 0 )  
        {  
            if ( errno == EINTR )  
                continue;  
            else if ( errno == EAGAIN )  
            {  
                log_Info( "----unixsock_sendall success or sendbuf full" );  
                break;  
            }  
            else  
            {  
                log_Err( "serve unix socket send failed: %s(%d)",   
                    strerror( errno ), errno );  
                break;  
            }  
        }
        else if ( i_sendlen == p_unixsock_info->ui_sendlen )
        {
            p_unixsock_info->ui_sendlen -= i_sendlen;
            break;
        }
        else if( i_sendlen < p_unixsock_info->ui_sendlen )  
        {  
            memmove( p_unixsock_info->psz_sendbuf,   
                p_unixsock_info->psz_sendbuf + i_sendlen,  
                p_unixsock_info->ui_sendlen - i_sendlen );  
            p_unixsock_info->ui_sendlen -= i_sendlen;  
        }  
    }  
    // send success  
    if ( 0 == p_unixsock_info->ui_sendlen )  
    {  
        // something to handle  
    }  
    return 0;  
}  


LT方式,如果没有写完,可以设置监听这个端口可写。当触发可写的时候(即epoll_wait得到EPOLL_OUT),把没发送完的数据发送完。这样的话,就需要把发送数据和其它相关信息保存起来,在下次触发时全部发送。

Level Triggered 工作模式    (默认的工作方式)

以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2)。

 LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket。在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作或者操作未完成,内核还是会继续通知你的(在epoll_wait调用后通知),所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。

调用者可以设定EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。


listen的第二个参数决定了你的服务端口能同时接受/保持多少个连接,最大值现在取决于 /proc/sys/net/core/somaxconn,说的默认128也是这个文件内容缺省为128,可以实时调整。

参考:

http://www.cnblogs.com/moodlxs/archive/2011/12/16/2290288.html

http://my.oschina.net/u/732357/blog/78122

http://blog.csdn.net/tianmohust/article/details/8691644#

http://www.ccvita.com/515.html

关于EPOLL读写完成的判断参考博主的另一篇博文:http://blog.csdn.net/bytxl/article/details/40855917


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值