注册 登录  
 加关注
   显示下一条  |  关闭
温馨提示!由于新浪微博认证机制调整,您的新浪微博帐号绑定已过期,请重新绑定!立即重新绑定新浪微博》  |  关闭

还东国的博客

行之苟有恒,久久自芬芳

 
 
 

日志

 
 

epoll的总结之二LT和ET的一些区别和分析  

2014-06-24 11:09:12|  分类: 网络服务编程技术 |  标签: |举报 |字号 订阅

  下载LOFTER 我的照片书  |

epoll的总结之二LTET的一些区别和分析

epoll的开发中,存在着两种方式,一种是水平触发(LT),一种是边缘触发(ET),一些没有开发过epoll的可能会感到奇怪,这有什么不同呢?其实这个跟在硬件电路中的沿触发和电平触发有一些类似的情况。

水平触发,意思是只要这个事件仍然存在就会不断的触发这个事件,而边缘触发则是这个事件只触发一次。举一个例子,比如网络编程过中要有accept监控,如果同时有五个客户端到达服务端添加到了内核中的监控对象中,对于水平LT触发来说,会触发五次,而对于边缘ET触发来说,只产生一次。那么他们处理监控和读与数据必然也就存在着一些不同。

也就是这个意思:level-trigger模式下只要某个socket处于readable/writable状态,无论什么时候进行epoll_wait都会返回该socket;而edge-trigger模式下只有某个socketunreadable变为readable或从unwritable变为writable时,epoll_wait才会返回该socket

ET(边缘触发)其操作形式如下:

读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN

:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN

正确的读

 

n = 0;

while ((nread = read(fd, buf + n, BUFSIZ-1)) > 0) {

    n += nread;

}

if (nread == -1 && errno != EAGAIN) {

    perror("read error");

}

正确的写

 

int nwrite, data_size = strlen(buf);

n = data_size;

while (n > 0) {

    nwrite = write(fd, buf + data_size - n, n);

    if (nwrite < n) {

        if (nwrite == -1 && errno != EAGAIN) {

            perror("write error");

        }

        break;

    }

    n -= nwrite;

}

然后回到一开始提出的那个ACCEPT的问题:在网络通信中,有阻塞和非阻塞两种形式:

阻塞模式 accept 存在下列问题:

TCP连接被客户端积极的退出,即在服务器调用accept之前,客户端主动发送RST终止连接,导致刚刚建立的连接从就绪队列中移出,如果套接口被设置成阻塞模式,服务器就会一直阻塞在accept调用上,直到其他某个客户建立一个新的连接为止。但是在此期间,服务器单纯地阻塞在accept调用上,就绪队列中的其他描述符都得不到处理。

解决办法是把监听套接口设置为非阻塞,当客户在服务器调用accept之前中止某个连接时,accept调用可以立即返回-1,这时源自Berkeley的实现会在内核中处理该事件,并不会将该事件通知给epool,而其他实现把errno设置为ECONNABORTED或者EPROTO错误,我们应该忽略这两个错误。

ET模式下accept存在的问题

考虑这种情况:多个连接同时到达,服务器的TCP就绪队列瞬间积累多个就绪连接,由于是边缘触发模式,epoll只会通知一次,accept只处理一个连接,导致TCP就绪队列中剩下的连接都得不到处理。

解决办法是用while循环抱住accept调用,处理完TCP就绪队列中的所有连接后再退出循环。如何知道是否处理完就绪队列中的所有连接呢?accept返回-1并且errno设置为EAGAIN就表示所有连接都处理完。

综合以上两种情况,服务器应该使用非阻塞地acceptacceptET模式下的正确使用方式为:

while ((conn_sock = accept(listenfd,(struct sockaddr *) &remote, (size_t *)&addrlen)) > 0) {

    handle_client(conn_sock);

}

if (conn_sock == -1) {

    if (errno != EAGAIN && errno != ECONNABORTED && errno != EPROTO && errno != EINTR)

    perror("accept");

}

但是,在水平触发时,则可以不必这样,epoll_wait会返回触发的事件的数量,在循环中,直接accept就可以了。不必反复的循环连接。(上面讲过,只要有这个事件存在,在LT的情况,epoll_wait会不断的被触发)

但这里又出现了一个问题,那就是在epoll的写时操作,同样也要分成两种情况:

LT下:这种情况下是最麻烦的,上面说过,只要有可写的事件,他就会不停的触发,那么你 写操作如何做呢?在上面可是一个死循环。

方法是有的,那就是在操作做完成EPOLLOUT后,将此事件移除出去,不再入到内核事件中监听:

Event.events &= ~EPOLLOUT或者直接重新设置相应的事件

epoll_ctl(epollfd,EPOLL_CTL_MOD,fd,&ev)

或者把fdepollfd监控中移除,方法就是close(fd)

最简单的,就是直接操作SOCKET,而不使用EPOLL

ET下:

EPOLL下的ET模式触发,倒是没有LT上面的麻烦,所以可以直接象上面一样修改事件,也可以直接操作SOCKET,如果发生EAGAIN错误,则可以再加入EPOLL事件中,进行写操作。

Epoll会自动从监控队列中删除close(fd)后的句柄,但是最好在这之前把SOCKET套接字上的监听事件移除,可以使用:epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,0),需要注意的是,在LINUX2.6.9以前最后一个参数需要一个非空的指针(虽然其并没有用处,被忽略),而之后则可以设置为NULL

这里面还有一个小小的坑儿,在ET中事件是只触发一次的,但是如果你在EPOLLIN事件中增加了EPOLLOUT事件,然后注册进去,再在EPOLLOUT中重新把EPOLLIN注册回来并去除EPOLLOUT,在这个过程中,有些网上的人说ET接收了两次数据,不符合只触发一次的原则,可是这里面其实是正确的(所以说不要轻易的怀疑成熟的代码及库),因为你在INOUT的来回注册过程 中正好完成了上面说的从读到写,然后又从写到读,产生了一次新的边缘触发,造成再次接收一次。

另外,多个进程或者多个线程上同时使用epoll_wait 同一个SOCKETRfd,会产生据说的惊群,但只会有一个epoll_wait返回,其它返回一个EAGAIN的错误。(但在新的内核中2.6.18以后,加入了独占操作exclusive,只会唤醒等待队列的第一个线程)所以在NGINX中采用锁的方式来控制。如果想分开ACCEPT和读写操作,那么一般是在ACCEPT线程和读写线程里分别创建并操作一个epollfd。然后通过管道或者其它方式把ACCEPT得到的新的客户端SOCKET句柄传送给读写的epollfd.

EPOLL一般来说更适用于慢速,高并发的情况下。

今天就到这里吧。

  评论这张
 
阅读(713)| 评论(0)
推荐 转载

历史上的今天

在LOFTER的更多文章

评论

<#--最新日志,群博日志--> <#--推荐日志--> <#--引用记录--> <#--博主推荐--> <#--随机阅读--> <#--首页推荐--> <#--历史上的今天--> <#--被推荐日志--> <#--上一篇,下一篇--> <#-- 热度 --> <#-- 网易新闻广告 --> <#--右边模块结构--> <#--评论模块结构--> <#--引用模块结构--> <#--博主发起的投票-->
 
 
 
 
 
 
 
 
 
 
 
 
 
 

页脚

网易公司版权所有 ©1997-2017