Chinaunix首页 | 论坛 | 博客
  • 博客访问: 188167
  • 博文数量: 41
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 275
  • 用 户 组: 普通用户
  • 注册时间: 2009-07-24 19:36
文章存档

2017年(17)

2016年(13)

2015年(4)

2014年(6)

2011年(1)

我的朋友

分类: C/C++

2014-06-26 08:47:49

一、Epoll简介
epoll是
因为它不会复用集合来传递结果而迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。
 
 
二、Epoll优点
<1> 支持一个进程打开大数目的socket描述符(FD)
    epoll 没有传统select/poll的“一个进程所打开的FD是有一定限制”的这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于select 所支持的2048。举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。
 
<2> IO效率不随FD数目增加而线性下降
    传统select/poll的另一个致命弱点就是当你拥有一个很大的socket集合,由于网络得延时,使得任一时间只有部分的socket是"活跃"的,而select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对"活跃"的socket进行操作---这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。于是,只有"活跃"的socket才会主动去调用callback函数,其他idle状态的socket则不会,在这点上,epoll实现了一个"伪"AIO,因为这时候推动力在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的---比如一个高速LAN环境,epoll也不比select/poll低多少效率,但若过多使用的调用epoll_ctl,效率稍微有些下降。然而一旦使用idle connections模拟WAN环境,那么epoll的效率就远在select/poll之上了。
 
<3> 使用mmap加速内核与用户空间的消息传递
    这点实际上涉及到epoll的具体实现。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就显得很重要,在这点上,epoll是通过内核于用户空间mmap同一块内存实现的。而如果你像我一样从2.5内核就开始关注epoll的话,一定不会忘记手工mmap这一步的。
 
<4> 内核微调
    这一点其实不算epoll的优点,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,可以在运行期间动态地调整这个内存pool(skb_head_pool)的大小---通过echo XXXX>/proc/sys/net/core/hot_list_length来完成。再比如listen函数的第2个参数(TCP完成3次握手的数据包队列长度),也可以根据你平台内存大小来动态调整。甚至可以在一个数据包面数目巨大但同时每个数据包本身大小却很小的特殊系统上尝试最新的NAPI网卡驱动架构。
 
三、
LT:水平触发
是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你 的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。
       效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。
ET:边缘触发
是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述 符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致 了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)。
效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。
在许多测试中我们会看到如果没有大量的idle -connection或者dead-connection,epoll的效率并不会比select/poll高很多,但是当我们遇到大量的idle- connection(例如WAN环境中存在大量的慢速连接),就会发现epoll的效率大大高于select/poll。
 
四、epoll具体使用方法
(1)epoll的接口非常简单,一共就三个函数:
              1. int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的 值。需要注意的是,当创建好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结构如下:
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告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有 说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。

 
 (2)具体的实现步骤如下:
(a) 使用epoll_create()函数创建文件描述,设定可管理的最大socket描述符数目。
(b) 创建与epoll关联的接收线程,应用程序可以创建多个接收线程来处理epoll上的读通知事件,线程的数量依赖于程序的具体需要。
(c) 创建一个侦听socket的描述符ListenSock,并将该描述符设定为非阻塞模式,调用Listen()函数在该套接字上侦听有无新的连接请求,在epoll_event结构中设置要处理的事件类型EPOLLIN,工作方式为 epoll_ET,以提高工作效率,同时使用epoll_ctl()来注册事件,最后启动网络监视线程。
(d) 网络监视线程启动循环,epoll_wait()等待epoll事件发生。
(e) 如果epoll事件表明有新的连接请求,则调用accept()函数,将用户socket描述符添加到epoll_data联合体,同时设定该描述符为非阻塞,并在epoll_event结构中设置要处理的事件类型为读和写,工作方式为epoll_ET。
(f) 如果epoll事件表明socket描述符上有数据可读,则将该socket描述符加入可读队列,通知接收线程读入数据,并将接收到的数据放入到接收数据的链表中,经逻辑处理后,将反馈的数据包放入到发送数据链表中,等待由发送线程发送。
 
例子代码:
 
#include
#include
#include
#include
#include
#include
#include
#include
 
#define MAXLINE 10
#define OPEN_MAX 100
#define LISTENQ 20
#define SERV_PORT 5555
#define INFTIM 1000
 
void setnonblocking(int sock)
{
 int opts;
 opts=fcntl(sock,F_GETFL);
 
 if(opts<0)
 {
    perror("fcntl(sock,GETFL)");
    exit(1);
 }
 
 opts = opts | O_NONBLOCK;
 
 if(fcntl(sock,F_SETFL,opts)<0)
 {
    perror("fcntl(sock,SETFL,opts)");
    exit(1);
 }
}
 
 
int main()
{
 int i, maxi, listenfd, connfd, sockfd, epfd, nfds;
 ssize_t n;
  char line[MAXLINE];
 socklen_t clilen;
 
 struct epoll_event ev,events[20]; //声明epoll_event结构体的变量, ev用于注册事件, events数组用于回传要处理的事件
 epfd=epoll_create(256); //生成用于处理accept的epoll专用的文件描述符, 指定生成描述符的最大范围为256
 
 struct sockaddr_in clientaddr;
 struct sockaddr_in serveraddr;
 
 listenfd = socket(AF_INET, SOCK_STREAM, 0);
 
 setnonblocking(listenfd); //把用于监听的socket设置为非阻塞方式
 
 ev.data.fd=listenfd; //设置与要处理的事件相关的文件描述符
 ev.events=EPOLLIN | EPOLLET; //设置要处理的事件类型
 epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev); //注册epoll事件
 
 bzero(&serveraddr, sizeof(serveraddr));
 serveraddr.sin_family = AF_INET;
 char *local_addr="200.200.200.204";
 inet_aton(local_addr,&(serveraddr.sin_addr));
 serveraddr.sin_port=htons(SERV_PORT); //或者htons(SERV_PORT);
 
 bind(listenfd,(sockaddr *)&serveraddr, sizeof(serveraddr));
 
 listen(listenfd, LISTENQ);
 
 maxi = 0;
 
 for( ; ; ) {
    nfds=epoll_wait(epfd,events,20,500); //等待epoll事件的发生
 
    for(i=0;i
      {
       if(events[i].data.fd==listenfd)    /**监听事件**/
        {
           connfd = accept(listenfd,(sockaddr *)&clientaddr, &clilen);
           if(connfd<0){
             perror("connfd<0");
             exit(1);
           }
 
         setnonblocking(connfd); //把客户端的socket设置为非阻塞方式
 
         char *str = inet_ntoa(clientaddr.sin_addr);
         std::cout<<"connect from "<
 
         ev.data.fd=connfd; //设置用于读操作的文件描述符
         ev.events=EPOLLIN | EPOLLET; //设置用于注测的读操作事件
         epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&ev); //注册ev事件
       }
      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
                  {
                    std::cout<<"readline error"<
                  }
             } else if (n == 0) {
                close(sockfd);
               events[i].data.fd = -1;
              }
 
          ev.data.fd=sockfd; //设置用于写操作的文件描述符
          ev.events=EPOLLOUT | EPOLLET; //设置用于注测的写操作事件
          epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改sockfd上要处理的事件为EPOLLOUT
       }
      else if(events[i].events&EPOLLOUT)    /**写事件**/
        {
          sockfd = events[i].data.fd;
          write(sockfd, line, n);
 
          ev.data.fd=sockfd; //设置用于读操作的文件描述符
          ev.events=EPOLLIN | EPOLLET; //设置用于注册的读操作事件
          epoll_ctl(epfd,EPOLL_CTL_MOD,sockfd,&ev); //修改sockfd上要处理的事件为EPOLIN
        }
     }
 }
}
附录:linux下epoll的测试效率(网上下载)
 测试程序分客户端(client)及服务端(server). 服务端分别以select和epoll两种I/O模型实现.
1.连接建立 速度测试
某个时刻连续向server发起大量连接请求,比较两种I/O模型下Server端的连接接收速度。在客户端记录下连接数完全建立后所花 费的时间.
操作步骤:
I.启动服务端程序.
Selectserver命令:(./SelectServer 192.168.0.30 8000 1>/dev/null)
EpollServer命令:   (./EpollServer 192.168.0.30 8000 1>/dev/null)
参数1(192.168.0.30)为server绑定的IP, 参数2(8000)为server监听的端口号;
II.启动客户端程序
命令:./deadlink 192.168.0.30 8000 800
参数1(192.168.0.30)是server端的IP, 参数2(8000)是server监听的端口,参数3(800)是你想要建立连接的数量.等连接全部建立完毕后程序会自动打印出所花费的时间及成功建立的 连接数.每个连接数量记录5组数据,去除一个最大及最小值后,取余下的3组数据的平均值作为最终结果.
2.数据传输性能测试
client端创建若干线程,每个线程与server建立一个连接。连接建立后向server发送取数据请求,然后读 取server端返回的数据.如此反复循环。每个client请求server返回的数据字节数为1K(1024bytes)大小.当连接全部建立后,系 统稳定下来,记录此时的服务程序对应的CPU占用率及内存使用率.每个连接数量记录下12组数据供分析使用.分析结果中将除去一个最大值及最小值,取余下 的10组数据的平均值作为最终结果。
操作步骤:
I启动服务端
Selectserver命令:(./SelectServer 192.168.0.30 8000 1>/dev/null)
EpollServer命令:   (./EpollServer 192.168.0.30 8000 1>/dev/null)
参数1(192.168.0.30)为server绑定的IP, 参数2(8000)为server监听的端口号;
II.启动客户端
命令: ./activelink 192.168.0.30 8000 800
参数1(192.168.0.30)是server端的IP, 参数2(8000)是server监听的端口,参数3(800)是你想要建立的线程数(连接数).因为每个线程建立一个连接,所以此数量亦即建立的连接 数。
III. netstat –la | grep “192.168.0.250” | wc –l 查看连接数量,等待建立完成.此处192.168.0.250为客户端机器IP地址。
IV.连接全部建立后等待5-6分钟,待系统稳定下来后 top查看并记录12 组Server 程序所占CPU/内存使用率.
1.2 测试平台说明
Server机器配置
CPU(处理 器) Intel(R) Pentium(R) 4 CPU 2.40GHz, L2 cache size: 512 KB
RAM(内 存) 248384kb, 约为242M
OS(操作系统) Redhat Linux 9.0, kernel 2.6.16-20
NIC(网 卡) Realtek Semiconductor RTL-8139/8139C/8139C+ (rev 10), work on negotiated 100baseTx-FD
client机器配置
CPU(处理器) Intel(R) Pentium(R) 4 CPU 2.0GHz, L2 cache size: 512 KB
RAM(内存) 222948kb, 约为218M
OS(操作系统) Redhat Linux 9.0, kernel 2.4.20-8
NIC(网卡) VIA Technologies VT6102 [Rhine-II] (rev 74)



 
2 测试结果
2.1 接收连接速度测试结果
表 2 1接收连接速度测试结果
连接数\IO模 型 SelectServer(单位 秒s) EpollServer(单位 秒s)
100 0s 0s
200 0s 0s
300 6s 0s
400 14s 0s
500 24s 0s
600 36s 0.3s
700 48s 0s
800 59s 0s
900 72s 0s
1000 84s 0s
2.2 数据传输性能测试
表 2 2数据传输性能测试结果
连接数\IO模型 SelectServer [cpu%, mem%] EpollServer [cpu%, mem%]
100 [28.06,    0.3] [21.74, 0.3]
200 [43.66,    0.3] [40.50, 0.3]
300 [47.09,    0.3] [42.73, 0.3]
400 [59.04,    0.3] [44.55, 0.3]
500 [54.44,    0.3] [51.00, 0.3]
600 [63.38,    0.3] [50.76, 0.3]
700 [65.77,    0.3] [51.47, 0.3]
800 [70.52,    0.3] [52.80, 0.3]
阅读(2309) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~