epoll 订阅
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。 展开全文
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
信息
外文名
epoll
工作方式
LT和ET
系统调用
epoll_create, epoll_ctl等
优    点
内核微调等
epoll优点
支持一个进程打开大数目的socket描述符select 最不能忍受的是一个进程所打开的FD是有一定限制的,由FD_SETSIZE设置,默认值是1024。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译服务器代码,不过资料也同时指出这样会带来网络效率的下 降,二是可以选择多进程的解决方案(传统的Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的方案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max查看,一般来说这个数目和系统内存关系很大。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之上了。没有使用mmap加速内核与用户空间的消息传递
收起全文
精华内容
参与话题
问答
  • epoll

    2019-09-10 14:55:22
    1、epoll epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取...

    1、epoll

            epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。

    注:

    poll:是Linux中的字符设备驱动中的一个函数。Linux 2.5.44版本后,poll被epoll取代。和select实现的功能差不多,poll的作用是把当前的文件指针挂到等待队列。

    select:用于监视文件描述符的变化情况——读写或是异常。

     

    2、epoll用法:

    epoll的用法通常是使用以下三个函数:

    (1)epoll_create( )

    int epoll_create(int size);

    功能和返回:

           返回epoll 文件描述符,用来存放 socket fd 上是否发生以及发生了什么事件

    参数:

            size:返回的 epoll fd 上能关注的最大socket fd数(fild descriptor套接字描述器),即最大的socket数量。

    (2)epoll_ctl( )

    int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

    功能:

           控制 epoll 文件描述符上的事件:注册、修改、删除。

    参数:

    • epfd:epoll_create() 创建的 epoll 文件描述符。
    • op:epoll 监听事件,通常为一下三个参数:

    EPOLL_CTL_ADD 增加一个新的 epoll 事件。

    EPOLL_CTL_DEL 减少一个 epoll 事件,

    EPOLL_CTL_MOD 改变一个事件的监听方式。

    • fd:socket() 返回值。
    • event:struct epoll_event,被用于注册事件和回传所发生待处理的事件。

    注:struct epoll_event定义:

    typedef union epoll_data
    {
        void* ptr;
        int fd;
        __uint32_t u32;
        __uint64_t u64;
    }
    epoll_data_t;  /* 保存触发事件的某个文件描述符相关的数据 */
    
    struct epoll_event
    {
        __uint32_t events;  /* epoll event */
        epoll_data_t data;  /* User data variable */
    };
    /* epoll_event.events:
      EPOLLIN  表示对应的文件描述符可以读
      EPOLLOUT 表示对应的文件描述符可以写
      EPOLLPRI 表示对应的文件描述符有紧急的数据可读
      EPOLLERR 表示对应的文件描述符发生错误
      EPOLLHUP 表示对应的文件描述符被挂断
      EPOLLET  表示对应的文件描述符有事件发生
    */

    返回值:

           成功,返0,失败返-1

    (3)epoll_wait()

    int epoll_wait(int epfd, struct epoll_event* events,int maxevents, int timeout);

    等待I/O事件的发生

    参数:epfd: epoll_create() 生成的 epoll 文件描述符;

    • epoll_event: 用于回传代处理事件的数组;
    • maxevents: 每次能处理的事件数;
    • timeout: 等待I/O事件发生的超时值;

    返回值:

            发生事件数。

    展开全文
  • EPOLL

    2015-04-27 16:24:08
    1.简介  Linux I/O多路复用技术在比较多的TCP网络服务器中有使用,即比较多的用到select函数。Linux 2.6内核中有提高网络I/O性能的新方法,即epoll 。... epoll_create(2),epoll_ctl(2),epoll_wait

    1.简介

       Linux I/O多路复用技术在比较多的TCP网络服务器中有使用,即比较多的用到select函数。Linux 2.6内核中有提高网络I/O性能的新方法,即epoll 。 epoll是什么?按照man手册的说法是为处理大批量句柄而作了改进的poll。要使用epoll只需要以下的三个系统函数调用:  epoll_create(2),epoll_ctl(2),epoll_wait(2)。


    2.select模型的缺陷

       (1) 在Linux内核中,select所用到的FD_SET是有限的

       内核中有个参数__FD_SETSIZE定义了每个FD_SET的句柄个数:#define __FD_SETSIZE 1024。也就是说,如果想要同时检测1025个句柄的可读状态是不可能用select实现的;或者同时检测1025个句柄的可写状态也是不可能的。

       (2) 内核中实现select是使用轮询方法

       每次检测都会遍历所有FD_SET中的句柄,显然select函数的执行时间与FD_SET中句柄的个数有一个比例关系,即select要检测的句柄数越多就会越费时


    3.Windows IOCP模型的缺陷

        windows完成端口实现的AIO,实际上也只是使用内部用线程池实现的,最后的结果是IO有个线程池,你的应用程序也需要一个线程池。很多文档其实已经指出了这引发的线程context-switch所带来的代价。


    4.EPOLL模型的优点

       (1) 支持一个进程打开大数目的socket描述符(FD)

       epoll没有select模型中的限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远大于select 所支持的2048

       (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加速内核与用户空间的消息传递

       无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就显得很重要。在这点上,epoll是通过内核于用户空间mmap同一块内存实现。


    5.EPOLL模型的工作模式

       (1) LT模式

       LT:level triggered,这是缺省的工作方式,同时支持block和no-block socket,在这种模式中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表。

       (2) ET模式

       LT:edge-triggered,这是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核就通过epoll告诉你,然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作而导致那个文件描述符不再是就绪状态(比如你在发送,接收或是接受请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核就不会发送更多的通知(only once)。不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。


    6.EPOLL模型的使用方法

       epoll用到的所有函数都是在头文件sys/epoll.h中声明的,下面简要说明所用到的数据结构和函数:

       (1) epoll_data、epoll_data_t、epoll_event

       typedef union epoll_data {

          void *ptr;

          int fd;

          __uint32_t u32;

          __uint64_t u64;

       } epoll_data_t;

     

       struct epoll_event {

          __uint32_t events; /* Epoll events */

          epoll_data_t data; /* User data variable */

       };

     

       结构体epoll_event 被用于注册所感兴趣的事件和回传所发生待处理的事件。epoll_event 结构体的events字段是表示感兴趣的事件和被触发的事件,可能的取值为:

       EPOLLIN:      表示对应的文件描述符可以读;

       EPOLLOUT:     表示对应的文件描述符可以写;

       EPOLLPRI:     表示对应的文件描述符有紧急的数据可读;

       EPOLLERR:     表示对应的文件描述符发生错误;

       EPOLLHUP:     表示对应的文件描述符被挂断;

       EPOLLET:      表示对应的文件描述符有事件发生;

     

       联合体epoll_data用来保存触发事件的某个文件描述符相关的数据。例如一个client连接到服务器,服务器通过调用accept函数可以得到于这个client对应的socket文件描述符,可以把这文件描述符赋给epoll_data的fd字段,以便后面的读写操作在这个文件描述符上进行。

     

       (2)epoll_create

       函数声明:intepoll_create(intsize)

       函数说明:该函数生成一个epoll专用的文件描述符,其中的参数是指定生成描述符的最大范围。

     

       (3) epoll_ctl函数

       函数声明:intepoll_ctl(int epfd,int op, int fd, struct epoll_event *event)

       函数说明:该函数用于控制某个文件描述符上的事件,可以注册事件、修改事件、删除事件。

          epfd:由 epoll_create 生成的epoll专用的文件描述符;

          op:要进行的操作,可能的取值EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修改、EPOLL_CTL_DEL      删除;

          fd:关联的文件描述符;

          event:指向epoll_event的指针;

       如果调用成功则返回0,不成功则返回-1。

     

       (4) epoll_wait函数

       函数声明:int epoll_wait(int epfd, structepoll_event * events, int maxevents, int timeout)

       函数说明:该函数用于轮询I/O事件的发生。

       epfd:由epoll_create 生成的epoll专用的文件描述符;

       epoll_event:用于回传代处理事件的数组;

       maxevents:每次能处理的事件数;

       timeout:等待I/O事件发生的超时值;

       返回发生事件数。

     

       设计思路:

       首先通过create_epoll(int maxfds)来创建一个epoll的句柄,其中maxfds为你的epoll所支持的最大句柄数。这个函数会返回一个新的epoll句柄,之后的所有操作都将通过这个句柄来进行操作。在用完之后,记得用close()来关闭这个创建出来的epoll句柄。

       然后在你的网络主循环里面,调用epoll_wait(int epfd, epoll_event events, int max_events,int timeout)来查询所有的网络接口,看哪一个可以读,哪一个可以写。基本的语法为:

       nfds = epoll_wait(kdpfd, events, maxevents, -1); 

       其中kdpfd为用epoll_create创建之后的句柄,events是一个epoll_event*的指针,当epoll_wait函数操作成功之后,events里面将储存所有的读写事件。max_events是当前需要监听的所有socket句柄数。最后一个timeout参数指示 epoll_wait的超时条件,为0时表示马上返回;为-1时表示函数会一直等下去直到有事件返回;为任意正整数时表示等这么长的时间,如果一直没有事件,则会返回。一般情况下如果网络主循环是单线程的话,可以用-1来等待,这样可以保证一些效率,如果是和主循环在同一个线程的话,则可以用0来保证主循环的效率。epoll_wait返回之后,应该进入一个循环,以便遍历所有的事件。

    对epoll 的操作就这么简单,总共不过4个API:epoll_create, epoll_ctl,epoll_wait和close。以下是man中的一个例子。

    struct epoll_event ev, *events;

    for(;;) 

    {

       nfds = epoll_wait(kdpfd, events, maxevents, -1);    //等待IO事件

       for(n = 0; n < nfds; ++n)

       {

       //如果是主socket的事件,则表示有新连接进入,需要进行新连接的处理。

          if(events[n].data.fd == listener)

          {

             client = accept(listener, (struct sockaddr *) &local,  &addrlen);

    if(client < 0)

             {

                perror("accept error");

                continue;

             }

             // 将新连接置于非阻塞模式

             setnonblocking(client);

             ev.events = EPOLLIN | EPOLLET; 

             //注意这里的参数EPOLLIN | EPOLLET并没有设置对写socket的监听,

             //如果有写操作的话,这个时候epoll是不会返回事件的,

             //如果要对写操作也监听的话,应该是EPOLLIN | EPOLLOUT | EPOLLET。

             // 并且将新连接也加入EPOLL的监听队列

             ev.data.fd = client;

             // 设置好event之后,将这个新的event通过epoll_ctl

             if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0)

             {

                //加入到epoll的监听队列里,这里用EPOLL_CTL_ADD

                //来加一个新的 epoll事件。可以通过EPOLL_CTL_DEL来减少

                //一个epoll事件,通过EPOLL_CTL_MOD来改变一个事件的监听方式。

                fprintf(stderr, "epoll set insertion error: fd=%d"0, client);

                return -1;

             }

          }

          else

          // 如果不是主socket的事件的话,则代表这是一个用户的socket的事件,

          // 则用来处理这个用户的socket的事情是,比如说read(fd,xxx)之类,或者一些其他的处理。

             do_use_fd(events[n].data.fd);

       }

    }


    7.EPOLL模型的一个实例

    #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;

       //声明epoll_event结构体的变量, ev用于注册事件, events数组用于回传要处理的事件

       struct epoll_event ev,events[20];

       //生成用于处理accept的epoll专用的文件描述符, 指定生成描述符的最大范围为256 

       epfd = epoll_create(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) // 这里和IOCP不同

                {

                   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;   //设置用于注测的写操作事件 

                //修改sockfd上要处理的事件为EPOLLOUT

                epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);

             } 

             else if(events[i].events&EPOLLOUT)//写事件

             {

                sockfd = events[i].data.fd;

                write(sockfd, line, n);

                ev.data.fd = sockfd;               //设置用于读操作的文件描述符

                ev.events = EPOLLIN | EPOLLET;     //设置用于注册的读操作事件

                //修改sockfd上要处理的事件为EPOLIN

                epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd, &ev);

             } 

          }

       }

    }


    8.EPOLL进阶思考

    8.1. 问题来源

       最近学习EPOLL模型,介绍中说将EPOLL与Windows IOCP模型进行比较,说其的优势在于解决了IOCP模型大量线程上下文切换的开销,于是可以看出,EPOLL模型不需要多线程,即单线程中可以处理EPOLL逻辑。如果引入多线程反而会引起一些问题。但是EPOLL模型的服务器端到底可以不可以用多线程技术,如果可以,改怎么取舍,这成了困扰我的问题。上网查了一下,有这样几种声音:

       (1) “要么事件驱动(如epoll),要么多线程,要么多进程,把这几个综合起来使用,感觉更加麻烦。”;

       (2) “单线程使用epoll,但是不能发挥多核;多线程不用epoll。”;

       (3) “主通信线程使用epoll所有需要监控的FD,有事件交给多线程去处理”;

       (4) “既然用了epoll, 那么线程就不应该看到fd, 而只看到的是一个一个的业务请求/响应; epoll将网络数据组装成业务数据后, 转交给业务线程进行处理。这就是常说的半同步半异步”。

       我比较赞同上述(3)、(4)中的观点

       EPOLLOUT只有在缓冲区已经满了,不可以发送了,过了一会儿缓冲区中有空间了,就会触发EPOLLOUT,而且只触发一次。如果你编写的程序的网络IO不大,一次写入的数据不多的时候,通常都是epoll_wait立刻就会触发 EPOLLOUT;如果你不调用 epoll,直接写 socket,那么情况就取决于这个socket的缓冲区是不是足够了。如果缓冲区足够,那么写就成功。如果缓冲区不足,那么取决你的socket是不是阻塞的,要么阻塞到写完成,要么出错返回。所以EPOLLOUT事件具有较大的随机性,ET模式一般只用于EPOLLIN, 很少用于EPOLLOUT。

    8.2. 具体做法

       (1) 主通信线程使用epoll所有需要监控的FD,负责监控listenfd和connfd,这里只监听EPOLLIN事件,不监听EPOLLOUT事件;

       (2) 一旦从Client收到了数据以后,将其构造成一个消息,放入消息队列中;

       (3) 若干工作线程竞争,从消息队列中取出消息并进行处理,然后把处理结果发送给客户端。发送客户端的操作由工作线程完成。直接进行write。write到EAGAIN或EWOULDBLOCK后,线程循环continue等待缓冲区队列

    发送函数代码如下:

    bool send_data(int connfd, char *pbuffer, unsigned int &len,int flag)

    {

       if ((connfd < 0) || (0  == pbuffer))

       {

          return false;

       }

       

       int result = 0;

       int remain_size = (int) len;

       int send_size = 0;

       const char *p = pbuffer; 


       time_t start_time = time(NULL);

       int time_out = 3;


       do

       {

          if (time(NULL) > start + time_out)

          {

             return false;

          }


          send_size = send(connfd, p, remain_size, flag);

          if (nSentSize < 0)

          {

             if ((errno == EAGAIN) || (errno == EWOULDBLOCK) || (errno == EINTR))

             {

                continue;

             }

             else

             {

                len -= remain_size;

                return false;

             }

          }

     

          p += send_size;

          remain_size -= send_size;

       }while(remain_size > 0);


       return true;

    }

    linux
    展开全文
  • Epoll

    2015-11-09 14:01:18
    Epoll 一:概念 首先我们来定义流的概念,一个流可以是文件,socket,pipe等等可以进行I/O操作的内核对象。 不管是文件,还是套接字,还是管道,我们都可以把他们看作流。 之后我们来讨论I/O的操作,通过read,我们...

    一:概念

    首先我们来定义流的概念,一个流可以是文件,socket,pipe等等可以进行I/O操作的内核对象。
    不管是文件,还是套接字,还是管道,我们都可以把他们看作流。
    之后我们来讨论I/O的操作,通过read,我们可以从流中读入数据;通过write,我们可以往流写入数据。现在假定一个情形,我们需要从流中读数据,但是流中还没有数据,(典型的例子为,客户端要从socket读如数据,但是服务器还没有把数据传回来),这时候该怎么办?

    · 阻塞。阻塞是个什么概念呢?比如某个时候你在等快递,但是你不知道快递什么时候过来,而且你没有别的事可以干(或者说接下来的事要等快递来了才能做);那么你可以去睡觉了,因为你知道快递把货送来时一定会给你打个电话(假定一定能叫醒你)。

    · 非阻塞忙轮询。接着上面等快递的例子,如果用忙轮询的方法,那么你需要知道快递员的手机号,然后每分钟给他挂个电话:你到了没?

    很明显一般人不会用第二种做法,不仅显很无脑,浪费话费不说,还占用了快递员大量的时间。
    大部分程序也不会用第二种做法,因为第一种方法经济而简单,经济是指消耗很少的CPU时间,如果线程睡眠了,就掉出了系统的调度队列,暂时不会去瓜分CPU宝贵的时间片了。
    为了了解阻塞是如何进行的,我们来讨论缓冲区,以及内核缓冲区,最终把I/O事件解释清楚。缓冲区的引入是为了减少频繁I/O操作而引起频繁的系统调用(你知道它很慢的),当你操作一个流时,更多的是以缓冲区为单位进行操作,这是相对于用户空间而言。对于内核来说,也需要缓冲区。
    假设有一个管道,进程A为管道的写入方,B为管道的读出方。

    1. 假设一开始内核缓冲区是空的,B作为读出方,被阻塞着。然后首先A往管道写入,这时候内核缓冲区由空的状态变到非空状态,内核就会产生一个事件告诉B该醒来了,这个事件姑且称之为缓冲区非空

    2. 但是缓冲区非空事件通知B后,B却还没有读出数据;且内核许诺了不能把写入管道中的数据丢掉这个时候,A写入的数据会滞留在内核缓冲区中,如果内核也缓冲区满了,B仍未开始读数据,最终内核缓冲区会被填满,这个时候会产生一个I/O事件,告诉进程A,你该等等(阻塞)了,我们把这个事件定义为缓冲区满

    3. 假设后来B终于开始读数据了,于是内核的缓冲区空了出来,这时候内核会告诉A,内核缓冲区有空位了,你可以从长眠中醒来了,继续写数据了,我们把这个事件叫做缓冲区非满

    4. 也许事件Y1已经通知了A,但是A也没有数据写入了,而B继续读出数据,知道内核缓冲区空了。这个时候内核就告诉B,你需要阻塞了!,我们把这个时间定为缓冲区空

    这四个情形涵盖了四个I/O事件,缓冲区满,缓冲区空,缓冲区非空,缓冲区非满(注都是说的内核缓冲区,且这四个术语都是我生造的,仅为解释其原理而造)。这四个I/O事件是进行阻塞同步的根本。(如果不能理解“同步”是什么概念,请学习操作系统的锁,信号量,条件变量等任务同步方面的相关知识)。
    然后我们来说说阻塞I/O的缺点。但是阻塞I/O模式下,一个线程只能处理一个流的I/O事件。如果想要同时处理多个流,要么多进程(fork),要么多线程(pthread_create),很不幸这两种方法效率都不高。
    于是再来考虑非阻塞忙轮询的I/O方式,我们发现我们可以同时处理多个流了(把一个流从阻塞模式切换到非阻塞模式再此不予讨论):

    我们只要不停的把所有流从头到尾问一遍,又从头开始。这样就可以处理多个流了,但这样的做法显然不好,因为如果所有的流都没有数据,那么只会白白浪费CPU。这里要补充一点,阻塞模式下,内核对于I/O事件的处理是阻塞或者唤醒,而非阻塞模式下则把I/O事件交给其他对象(后文介绍的select以及epoll)处理甚至直接忽略。
    为了避免CPU空转,可以引进了一个代理(一开始有一位叫做select的代理,后来又有一位叫做poll的代理,不过两者的本质是一样的)。这个代理比较厉害,可以同时观察许多流的I/O事件,在空闲的时候,会把当前线程阻塞掉,当有一个或多个流有I/O事件时,就从阻塞态中醒来,于是我们的程序就会轮询一遍所有的流(于是我们可以把“忙”字去掉了)。代码长这样:

    while true {
    for i in stream[]; {
    if i has data
    read until unavailable
    }
    }

    于是,如果没有I/O事件产生,我们的程序就会阻塞在select处。但是依然有个问题,我们从select那里仅仅知道了,有I/O事件发生了,但却并不知道是那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。
    但是使用select,我们有O(n)的无差别轮询复杂度,同时处理的流越多,每一次无差别轮询时间就越长。再次
    说了这么多,终于能好好解释epoll了,epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll之会把哪个流发生了怎样的I/O事件通知我们。此时我们对这些流的操作都是有意义的。(复杂度降低到了O(k),k为产生I/O事件的流的个数,也有认为O(1)的)
    在讨论epoll的实现细节之前,先把epoll的相关操作列出:

    · epoll_create 创建一个epoll对象,一般epollfd = epoll_create()

    · epoll_ctl (epoll_add/epoll_del的合体),往epoll对象中增加/删除某一个流的某一个事件

    · epoll_wait(epollfd,...)等待直到注册的事件发生

    (注:当对一个非阻塞流的读写发生缓冲区满或缓冲区空,write/read会返回-1,并设置errno=EAGAIN。而epoll只关心缓冲区非满和缓冲区非空事件)。
    一个epoll模式的代码大概的样子是:

    while true {
    active_stream[] = epoll_wait(epollfd)
    for i in active_stream[] {
    read or write till unavailable
    }
    }

    二:selectpoll多路复用模型

    select的缺点:

    1. 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024,当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;

    2. (在linux内核头文件中,有这样的定义:#define __FD_SETSIZE 1024)

    3. 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;

    4. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;

    5. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

    相比select模型,poll使用链表保存文件描述符,因此没有了监视文件数量的限制,但其他三个缺点依然存在。

    拿select模型为例,假设我们的服务器需要支持100万的并发连接,则在__FD_SETSIZE 为1024的情况下,则我们至少需要开辟1k个进程才能实现100万的并发连接。除了进程间上下文切换的时间消耗外,从内核/用户空间大量的无脑内存拷贝、数组轮询等,是系统难以承受的。因此,基于select模型的服务器程序,要达到10万级别的并发访问,是一个很难完成的任务。

    因此,该epoll上场了。

    三:epoll多路复用模型实现机制

    由于epoll的实现机制与select/poll机制完全不同,上面所说的 select的缺点在epoll上不复存在。

    设想一下如下场景:有100万个客户端同时与一个服务器进程保持着TCP连接。而每一时刻,通常只有几百上千个TCP连接是活跃的(事实上大部分场景都是这种情况)。如何实现这样的高并发?

    在select/poll时代,服务器进程每次都把这100万个连接告诉操作系统(从用户态复制句柄数据结构到内核态),让操作系统内核去查询这些套接字上是否有事件发生,轮询完后,再将句柄数据复制到用户态,让服务器应用程序轮询处理已发生的网络事件,这一过程资源消耗较大,因此,select/poll一般只能处理几千的并发连接。

    epoll的设计和实现与select完全不同。epoll通过在Linux内核中申请一个简易的文件系统(文件系统一般用什么数据结构实现?B+树)。把原先的select/poll调用分成了3个部分:

    1)调用epoll_create()建立一个epoll对象(在epoll文件系统中为这个句柄对象分配资源)

    2)调用epoll_ctl向epoll对象中添加这100万个连接的套接字

    3)调用epoll_wait收集发生的事件的连接

    如此一来,要实现上面说是的场景,只需要在进程启动时建立一个epoll对象,然后在需要的时候向这个epoll对象中添加或者删除连接。同时,epoll_wait的效率也非常高,因为调用epoll_wait时,并没有一股脑的向操作系统复制这100万个连接的句柄数据,内核也不需要去遍历全部的连接。

    四:Linux内核具体的epoll机制实现思路

    当某一进程调用epoll_create方法时,Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关。eventpoll结构体如下所示:

    struct eventpoll{
        ....
        /*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
        struct rb_root  rbr;
        /*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
        struct list_head rdlist;
        ....
    };

    每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件。这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度)。

    而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当相应的事件发生时会调用这个回调方法。这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中。

    在epoll中,对于每一个事件,都会建立一个epitem结构体,如下所示:

    struct epitem{
        struct rb_node  rbn;//红黑树节点
        struct list_head    rdllink;//双向链表节点
         struct epoll_filefd  ffd;  //事件句柄信息
        struct eventpoll *ep;    //指向其所属的eventpoll对象
      struct epoll_event event; //期待发生的事件类型
    }

    当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可。如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户。

    epoll数据结构示意图


    从上面的讲解可知:通过红黑树和双链表数据结构,并结合回调机制,造就了epoll的高效。

    OK,讲解完了Epoll的机理,我们便能很容易掌握epoll的用法了。一句话描述就是:三步曲。

    第一步:epoll_create()系统调用。此调用返回一个句柄,之后所有的使用都依靠这个句柄来标识。

    第二步:epoll_ctl()系统调用。通过此调用向epoll对象中添加、删除、修改感兴趣的事件,返回0标识成功,返回-1表示失败。

    第三部:epoll_wait()系统调用。通过此调用收集收集在epoll监控中已经发生的事件。

    最后,附上一个epoll编程实例.

    Epoll_server.cpp

    /*
     * FileName: epoll_server.cpp
     *
     * Description: this is a epoll server example.
     *
     * Version: 1.0
     *
     * Created: 27/10/2015
     *
     * Revision: none
     *
     * Author: kay wang-kai@cardinfo.com.cn
     *
     */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/epoll.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <sys/fcntl.h>
    #include <sys/errno.h>
    #include <sys/unistd.h>
    #include <netdb.h>
    #include <arpa/inet.h>
    
    #define BUFFER_SIZE 40
    #define MAX_EVENTS 10
    #define PORT 8000
    #define MAX_LOG 5
    
    int main(int argc, char* argv[])
    {
    	// create server socket.
    	int server_socket = socket(PF_INET, SOCK_STREAM, 0);
    	if(server_socket < 0)
    	{
    		perror("create socket.");
    		return -1;
    	}
    
    	// init and bind.
    	struct sockaddr_in server_addr;
    	memset(&server_addr, 0, sizeof(server_addr));
    	server_addr.sin_family = AF_INET;
    	server_addr.sin_addr.s_addr = INADDR_ANY;
    	server_addr.sin_port = htons(PORT);
    	if(bind(server_socket, (struct sockaddr*)&server_addr, sizeof(struct sockaddr)) < 0)
    	{
    		perror("bind.");
    		return -1;
    	}
    
    	// listen.
    	if(listen(server_socket, MAX_LOG) < 0)
    	{
    		perror("listen.");
    		return -1;
    	}
    
    	// create epoll.
    	int epoll_fd = epoll_create(MAX_EVENTS);
    	if(epoll_fd < 0)
    	{
    		perror("epoll create failed.");
    		exit(EXIT_FAILURE);
    	}
    
    	struct epoll_event ev;// epoll event struct.
    	struct epoll_event events[MAX_EVENTS];// event listen queue.
    	ev.events = EPOLLIN;
    	ev.data.fd = server_socket;
    
    	// register listen event server_socket to epoll
    	if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_socket, &ev) < 0)
    	{
    		perror("epoll_ctl:server_socket register failed.");
    		exit(EXIT_FAILURE);
    	}
    
    	// accept client quest.
    	struct sockaddr_in client_addr;
    	socklen_t sin_size = sizeof(client_addr);
    	char buf[BUFFER_SIZE];
    	while(true)
    	{
    		printf("wait...\n");
    		// wait event happen.
    		int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
    		if(nfds < 0)
    		{
    			perror("start epoll_wait failed.");
    			exit(EXIT_FAILURE);
    		}
    		printf("nfds = %d\n", nfds);
    		for(int i = 0; i < nfds; ++i)
    		{
    			if((events[i].events & EPOLLERR) || (events[i].events & EPOLLHUP) || !(events[i].events & EPOLLIN))
    			{
    				perror("epoll error.");
    				close(events[i].data.fd);
    				continue;
    			}
    			if(events[i].data.fd == server_socket)// there is a new connection.
    			{
    				int client_socket = accept(server_socket, (struct sockaddr*)&client_addr, &sin_size);
    				if(client_socket < 0)
    				{
    					perror("accept client_socket failed.");
    					exit(EXIT_FAILURE);
    				}
    				struct epoll_event ev_tmp;
    				ev_tmp.events = EPOLLIN;
    				ev_tmp.data.fd = client_socket;
    				if(epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_socket, &ev_tmp) < 0)
    				{
    					perror("epoll_ctl: client_socket register failed.");
    					exit(EXIT_FAILURE);
    				}
    				printf("accept client %s\n", inet_ntoa(client_addr.sin_addr));
    			}
    			else// there is data.
    			{
    				if(recv(events[i].data.fd, buf, BUFFER_SIZE, 0) < 0)
    				{
    					perror("receive from client failed.");
    					exit(EXIT_FAILURE);
    				}
    				printf("receive from client:%s\n", buf);
    				send(events[i].data.fd, "I have received your message.", 30, 0);
    			}
    		}
    	}
    
    	return 0;
    }

    epoll_client.cpp

    /*
     * FileName: epoll_client.cpp
    
     * Description: this is a epoll client example.
     *
     * Version: 1.0
     *
     * Created: 27/10/2015
     *
     * Revision: none
     *
     * Author: kay wang-kai@cardinfo.com.cn
     *
     */
    
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <sys/epoll.h>
    #include <sys/types.h>
    #include <sys/socket.h>
    #include <sys/fcntl.h>
    #include <sys/errno.h>
    #include <sys/unistd.h>
    #include <netdb.h>
    #include <arpa/inet.h>
    
    #define BUFFER_SIZE 40
    #define MAX_EVENTS 10
    #define PORT 8000
    #define MAX_LOG 5
    
    int main(int argc, char* argv[])
    {
    	// init and connect.
    	struct sockaddr_in server_addr;
    	memset(&server_addr, 0, sizeof(server_addr));
    	server_addr.sin_family = AF_INET;
    	server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");// server ip addr.
    	server_addr.sin_port = htons(PORT);// server port.
    
    	int client_socket = socket(PF_INET, SOCK_STREAM, 0);
    	if(client_socket < 0)
    	{
    		perror("client socket create failed.");
    		exit(EXIT_FAILURE);
    	}
    
    	// connect.
    	if(connect(client_socket, (struct sockaddr*)&server_addr, sizeof(struct sockaddr)) < 0)
    	{
    		perror("connect to server failed.");
    		exit(EXIT_FAILURE);
    	}
    
    	// listen server.
    	char buf[BUFFER_SIZE];
    	while(true)
    	{
    		printf("please input the message:");
    		scanf("%s", buf);
    
    		// exit.
    		if(strcmp(buf, "q") == 0)
    		{
    			break;
    		}
    
    		// send.
    		send(client_socket, buf, BUFFER_SIZE, 0);
    
    		// receive message from sever.
    		int len = recv(client_socket, buf, BUFFER_SIZE, 0);
    		printf("receive from server:%s\n", buf);
    		if(len < 0)
    		{
    			perror("receive from server failed.");
    			exit(EXIT_FAILURE);
    		}
    	}
    	// close.
    	close(client_socket);
    
    	return 0;
    }


    【参考】

    http://www.cricode.com/3499.html

    http://www.zhihu.com/question/20122137/answer/14049112

    展开全文

空空如也

1 2 3 4 5 ... 20
收藏数 24,597
精华内容 9,838
关键字:

epoll