linux下epoll架构

 

1、Epoll简介
由于它不会复用 文件描述符 集合来传递结果而迫使开发者每次等待事件以前都必须从新准备要被侦听的文件描述符集合,另外一点缘由就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就好了。
 
 
2、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。
 
4、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 <iostream>
#include <sys/socket.h>
#include <sys/epoll.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
 
#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<nfds;++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 "<<str<<std::endl;
 
         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"<<std::endl;
                  }
             } 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]

>>离线阅读请点击下载附件html

相关文章
相关标签/搜索