我们目前的网络模型大都是epoll的,因为epoll模型会比select模型性能高很多, 尤其在大连接数的情况下,作为后台开发人员需要理解其中的原因。
下面根据自己对select和epoll的理解写下这边文章,希望对大家有帮助,欢迎大家挑战:)
select的特点:select 选择句柄的时候,是遍历所有句柄,也就是说句柄有事件响应时,select需要遍历所有句柄才能获取到哪些句柄有事件通知,因此效率是非常底的。但是如果连接很少的情况下, select和epoll的LT触发模式相比, 性能上差别不大。
这里要多说一句,select支持的句柄数是有限制的, 同时只支持1024个,这个是句柄集合限制的,如果超过这个限制,很可能导致溢出,而且非常不容易发现问题, TAF就出现过这个问题, 调试了n天,才发现:)
epoll的特点:epoll对于句柄事件的选择不是遍历的,是事件响应的,就是句柄上事件来就马上选择出来,不需要遍历整个句柄链表,因此效率非常高(不知道内核怎么实现的)。但是对于epoll而言还有ET和LT的区别,LT表示水平触发,ET表示边缘触发,两者在性能以及代码实现上差别也是非常大的。
LT是缺省的工作方式,并且同时支持block和no-block socket;在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
ET是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述 符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就 绪),内核不会发送更多的通知。
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
上面介绍select,epoll(LT,ET)区别,从性能的数据上考虑,根据自己的对服务性能的了解,大概数据如下:(2CPU,2.8G)
select模型:少连接情况下面,单网络线程,吞吐量大约在1w/s左右,连接数大了,效率下降比较快。(ICE采用这种模型,ICE的吞吐量大约在8k/s,以及以前负责图铃时写的select prefork模型的吞吐量大概也是这个量级)
epoll(LT):单网络线程,吞吐量大约在1.2w/s左右,连接数增大的情况下, 吞吐量不会有太大变化。(GNP目前采用这种方式,不过新版本的GNP 马上就只要支持ET了)
epoll(ET):单网络线程,吞吐量可以到3w以上,很明显比LT会高很多,如果网络线程增加,吞吐量还会增加,我估计到5w应该不成问题(目前TAF采用这种方式)
ET模式,实际是减少了陷入内核的次数,减少了系统调用。
epoll的内部实现是使用红黑树代替了select用的位数组方式,能快速的定位。
红黑树应该算是Linux内核中用的最复杂的数据结构了,在内存管理等部分也有使用,除了reiserfs中使用的B+树。
select可支持的描述符的数量通过修改内核代码重编内核,可以支持超过1024个,但是位数组遍历的效率实在太低了,呵呵。
// 从网上看到一个代码, 解释ET和LT的区别, 比较生动
// 拷贝可以直接编译 g++ -g -Wall aaa.cpp
#include <stdio.h>
#include <iostream>
#include <errno.h>
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
using namespace std;
#define MAXLINE 4
void
setnonblocking (int sock)
{
int opts;
opts = fcntl (sock, F_GETFL);
if (opts < 0)
{
cout << "fcntl(sock,GETFL)" << endl;
exit (1);
}
opts = opts | O_NONBLOCK;
if (fcntl (sock, F_SETFL, opts) < 0)
{
cout << "fcntl(sock,SETFL,opts)" << endl;
exit (1);
}
};
int main(int argc, char *argv[])
{
int epfd = epoll_create(256);
int fds[2];
pipe(fds);
setnonblocking (fds[0]);
int i, n;
char line[MAXLINE];
struct epoll_event ev, events[20];
int nfds;
int ret;
int fk = fork();
if (!fk)
{
//printf("writing something : %d\n", fds[1]);
for(;;)
{
ret = write(fds[1], "123456", 6);
printf("\nwrite size n = %d, content= [123456] ", ret); cout << endl;
sleep(2);
}
}
// 改动这里,看到的结果是不一样的
ev.events = EPOLLIN | EPOLLET;
//ev.events = EPOLLIN;
ev.data.fd = fds[0];
ret = epoll_ctl (epfd, EPOLL_CTL_ADD, fds[0], &ev);
//printf("ctl ret = %dn", ret); cout << endl;
for(;;)
{
nfds = epoll_wait (epfd, events, 20, -1);
for (i = 0; i < nfds; ++i)
{
//printf("reading something from fds=: %d", fds[0]); cout << endl;
if (events[i].events & EPOLLIN)
{
//printf("reading something : %dn", fds[0]);
if ((n = read (fds[0], line, sizeof(line))) < 0)
{
printf("are you kidding? %d", (errno)); cout << endl;
exit(1);
}
else if (n == 0)
{
printf("read 0 bytes."); cout << endl;
exit(1);
}
printf("read size n = %d, content= [", n);
int j;
for(j = 0; j < n; j++)
{
printf("%c", line[j]);
}
cout <<"]"<< endl;
//line[n] = ' ';
//printf("read : %sn", line);
//memset(line, 0, sizeof(line));
}
}
}
}