Epoll工作模式详解
我们目前的网络模型大都是epoll的,因为epoll模型会比select模型性能高很多, 尤其在大连接数的情况下,作为后台开发人员需要理解其中的原因。
select的特点:select 选择句柄的时候,是遍历所有句柄,也就是说句柄有事件响应时,select需要遍历所有句柄才能获取到哪些句柄有事件通知,因此效率是非常低。但是如果连接很少的情况下, select和epoll的LT触发模式相比, 性能上差别不大。
这里要多说一句,select支持的句柄数是有限制的, 同时只支持1024个,这个是句柄集合限制的,如果超过这个限制,很可能导致溢出,而且非常不容易发现问题, TAF就出现过这个问题, 调试了n天,才发现:)当然可以通过修改linux的socket内核调整这个参数。
epoll的特点:epoll对于句柄事件的选择不是遍历的,是事件响应的,就是句柄上事件来就马上选择出来,不需要遍历整个句柄链表,因此效率非常高,内核将句柄用红黑树保存的。
对于epoll而言还有ET和LT的区别,LT表示水平触发,ET表示边缘触发,两者在性能以及代码实现上差别也是非常大的。
LT:水平触发,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。
ET:边缘触发,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。
下面举一个列子来说明LT和ET的区别(都是非阻塞模式,阻塞就不说了,效率太低):
采用LT模式下, 如果accept调用有返回就可以马上建立当前这个连接了,再epoll_wait等待下次通知,和select一样。
但是对于ET而言,如果accpet调用有返回,除了建立当前这个连接外,不能马上就epoll_wait还需要继续循环accpet,直到返回-1,且errno==EAGAIN,TAF里面的示例代码:
if(ev.events & EPOLLIN)
{
do
{
struct sockaddr_in stSockAddr;
socklen_t iSockAddrSize = sizeof(sockaddr_in);
TC_Socket cs;
cs.setOwner(false);
//接收连接
TC_Socket s;
s.init(fd, false, AF_INET);
int iRetCode = s.accept(cs, (struct sockaddr *) &stSockAddr, iSockAddrSize);
if (iRetCode > 0)
{
...建立连接
}
else
{
//直到发生EAGAIN才不继续accept
if(errno == EAGAIN)
{
break;
}
}
}while(true);
}
同样,recv/send等函数, 都需要到errno==EAGAIN
从本质上讲:与LT相比,ET模型是通过减少系统调用来达到提高并行效率的。
ET模型的逻辑:内核的读buffer有内核态主动变化时,内核会通知你, 无需再去mod。写事件是给用户使用的,最开始add之后,内核都不会通知你了,你可以强制写数据(直到EAGAIN或者实际字节数小于 需要写的字节数),当然你可以主动mod OUT,此时如果句柄可以写了(send buffer有空间),内核就通知你。
这里内核态主动的意思是:内核从网络接收了数据放入了读buffer(会通知用户IN事件,即用户可以recv数据)
并且这种通知只会通知一次,如果这次处理(recv)没有到刚才说的两种情况(EAGIN或者实际字节数小于 需要读写的字节数),则该事件会被丢弃,直到下次buffer发生变化。
与LT的差别就在这里体现,LT在这种情况下,事件不会丢弃,而是只要读buffer里面有数据可以让用户读,则不断的通知你。
另外对于ET而言,当然也不一定非send/recv到前面所述的结束条件才结束,用户可以自己随时控制,即用户可以在自己认为合适的时候去设置IN和OUT事件:
1 如果用户主动epoll_mod OUT事件,此时只要该句柄可以发送数据(发送buffer不满),则epoll
_wait就会响应(有时候采用该机制通知epoll_wai醒过来)。
2 如果用户主动epoll_mod IN事件,只要该句柄还有数据可以读,则epoll_wait会响应。
这种逻辑在普通的服务里面都不需要,可能在某些特殊的情况需要。 但是请注意,如果每次调用的时候都去epoll mod将显著降低效率,已经吃过几次亏了!
因此采用et写服务框架的时候,最简单的处理就是:
建立连接的时候epoll_add IN和OUT事件, 后面就不需要管了
每次read/write的时候,到两种情况下结束:
1 发生EAGAIN
2 read/write的实际字节数小于 需要读写的字节数
对于第二点需要注意两点:
A:如果是UDP服务,处理就不完全是这样,必须要recv到发生EAGAIN为止,否则就丢失事件了
因为UDP和TCP不同,是有边界的,每次接收一定是一个完整的UDP包,当然recv的buffer需要至少大于一个UDP包的大小
随便再说一下,一个UDP包到底应该多大?
对于internet,由于MTU的限制,UDP包的大小不要超过576个字节,否则容易被分包,对于公司的IDC环境,建议不要超过1472,否则也比较容易分包。
B 如果发送方发送完数据以后,就close连接,这个时候如果recv到数据是实际字节数小于读写字节数,根据开始所述就认为到EAGIN了从而直接返回,等待下一次事件,这样是有问题的,close事件丢失了!
因此如果依赖这种关闭逻辑的服务,必须接收数据到EAGIN为止,例如lb。
版权所有,转载请注明出处。http://www.vimer.cn
原创文章,版权所有。转载请注明:转载自Vimer的程序世界 [ http://www.vimer.cn ]
本文链接地址: http://www.vimer.cn/?p=289
关于epoll方面的学习有什么好一点的参考书不?
我对网络编程一窍不通。
研究方向是嵌入式;未来的工作需求需要我学这个。
有兴趣的话我们可以QQ或者E-mail联系不:-)
[回复]
Dante 回复:
十一月 18th, 2009 at 9:42 上午
epoll的话,看一下这篇文章
http://www.vimer.cn/2009/11/epoll%e4%bd%bf%e7%94%a8%e5%ae%9e%e4%be%8b%e8%af%b4%e6%98%8e.html
应该对你能有所帮助~~
[回复]
写的非常清楚,thanks
[回复]
Dante 回复:
十二月 23rd, 2009 at 7:42 下午
呵呵,有用就好啊~~
[回复]
请问文中的TAF 是什么
[回复]
[...] 更多的关于这两种工作模式的说明可以参考这里:EPOLL工作方式详解 [...]
你的文章对我有很大帮助,谢谢!
但是:你在文章中说明了LT与ET的区别,我想问一下,怎样设置epoll为ET的模式,是设置为EPOLLET么?
[回复]
Dante 回复:
二月 18th, 2011 at 8:08 下午
对,默认是水平触发,如果要边缘出发就要设置上EPOLLET
[回复]
Ricky 回复:
二月 21st, 2011 at 9:34 上午
Hi,非常感谢您的回复。
在网上提到Epoll模式的多用户并发连接时,存在多用户同时到达,ET模式下只提醒一次。想请教一下,这样的话,应该怎样处理?
[回复]
Dante 回复:
二月 21st, 2011 at 9:50 上午
你说多个用户共用一条链接,然后client发送一堆包之后等待回包?
我很少这样用,一般都是client进行一次发一次收,不过即使这种简单的形式,对ET来说只要没有recv完(或者recv=0)就要一直recv,这样就会造成阻塞。
所以其实大部分情况用LT即可,简单而且不用阻塞。
[回复]
Ricky 回复:
二月 21st, 2011 at 10:47 上午
我想,可能您说的和我说的并不一致。
我的意思是:
在多用户并发访问的情况下,有可能多个用户“同时”到达server端,此时ET模式下,只提醒一次。想请教这样的情况下,应该怎样处理?在这种情况下,它的epoll_wait的返回值会大于1,循环读取events逐个读取信息。是这样处理的么?
另:
我现在写了个测试程序。在client端,开启多个线程,模拟多用户连接,并且发送多条数据。但是出现这样的情况:
服务端部分代码:
else if(events[i].events & EPOLLIN)
{
if((sockfd = events[i].data.fd) < 0)
{
continue;
}
if((n = read(sockfd, line, MAXLINE)) < 0)
{
if(errno == ECONNRESET)
{
close(sockfd);
events[i].data.fd = -1;
}
else
{
printf("readline error\n");
}
}else if(n == 0)
{
close(sockfd);
events[i].data.fd = -1;
}
line[n] = '\n';
printf("read:\t %s\n", line);
events[i].data.fd = sockfd;
events[i].events = EPOLLOUT | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);
}
输出:
accept a content from 192.168.2.80
NFDS: 1
read: Thread 0: HelloWorld Times: 0
NFDS: 1
read: Thread 0: HelloWorld Times: 1
NFDS: 1
read: Thread 1: HelloWorld Times: 1
NFDS: 1
read: Thread 0: HelloWorld Times: 1
NFDS: 2
readline error
read:
readline error
read:
NFDS: 1
read: Thread 0: HelloWorld Times: 2
NFDS: 1
readline error
read:
NFDS: 1
readline error
read:
NFDS: 1
read: Thread 0: HelloWorld Times: 3
NFDS: 1
readline error
read:
NFDS: 1
readline error
read:
NFDS: 1
read: Thread 0: HelloWorld Times: 4
NFDS: 1
readline error
read:
NFDS: 1
readline error
read:
NFDS: 1
read:
NFDS: 0
麻烦帮忙看一下,十分感谢
[回复]
Dante 回复:
二月 21st, 2011 at 10:09 下午
基本是这样写,建议去看一下别人写的epoll recv的代码,几句话讲不清楚
[回复]
[...] Epoll工作模式详解 [...]
大哥,TAF是什么东西呢
[回复]