epoll并发服务器

Posted 松狮MVP

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了epoll并发服务器相关的知识,希望对你有一定的参考价值。


http://www.nowcoder.com/test/question/done?tid=4861371&qid=25654#summary(评论是重点)


1、基本模型

(1)多进程并发服务器:http://blog.csdn.net/songshimvp1/article/details/51819765

(2)多线程并发服务器:http://blog.csdn.net/songshimvp1/article/details/51895311

(3)I/O服用并发服务器:select、poll


2、epoll服务器

(1)epoll_create();

(2)epoll_ctl();——添加、修改、删除要监控的文件描述符;

(3)epoll_wait();——监控阻塞;


优点:

            epoll是什么? 按照man手册的说法:是为处理大批量句柄而作了改进的poll。当然,这不是2.6内核才有的,它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44),被公认为Linux2.6下性能最好的多路I/O就绪通知方法。

        (1)epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这个数目和系统内存关系很大 ,具体值可以 cat /proc/sys/fs/file-max[599534] 察看。

        (2)效率提升, Epoll最大的优点就在于它基于事件的就绪通知方式只管“活跃”的连接 ,而跟连接总数无关,其算法复杂度为O(1),因此在实际的网络环境中,epoll的效率就会远远高于 select 和 poll 。select和poll都是轮询方式的,每次调用要扫描整个注册文件描述符集合,并将其中就绪描述符返回给用户程序。epoll_wait采用的是回调方式,内核检测到就绪描述符时,将触发回调函数,回调函数就将该文件描述符上对应的事件插入内核就绪队列,不需要轮询。

        (3)内存拷贝, Epoll 在这点上使用了“共享内存“,因此没有内存拷贝的开销。

         epoll同样只告知那些就绪的文件描述符,而且当我们调用epoll_wait()获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定的一个数组中依次取得相应数量的文件描述符即可,这里也使用了内存映射(mmap)技术,这样便彻底省掉了这些文件描述符在系统调用时复制的开销。

        (4)另一个本质的改进在于epoll采用基于事件的就绪通知方式。在select/poll中,进程只有在调用一定的方法后,内核才对所有监视的文件描述符进行扫描,而epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。

        (5)和poll类似,epoll的定时也是int类型,单位是毫秒。

不足

              epoll的局限性在于它在Linux2.6才实现,而其他平台都没有,这与Apache这样的优秀跨平台服务器无法并论。select跨平台性能很好,几乎每个平台都支持。



epoll函数及参数

    int epoll_create(int size);  
    int epoll_create1(int  flags);  
    int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);   
    int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);  

第一个函数epoll_create():
         对于epoll_create1 的flag参数: 可以设置为0 或EPOLL_CLOEXEC,为0时函数表现与epoll_create一致, EPOLL_CLOEXEC标志与open 时的O_CLOEXEC 标志类似,即进程被替换时会关闭打开的文件描述符。

        创建一个epoll的句柄。自从linux2.6.8之后,size参数是被忽略的。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽


第二个函数epoll_ctl():

      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 */  
;  
typedef union epoll_data  
  
    void        *ptr;  
    int          fd;  
    uint32_t     u32;  
    uint64_t     u64;  
 epoll_data_t; 

events可以是以下几个宏的集合:
           EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
           EPOLLOUT:表示对应的文件描述符可以写;
           EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
           EPOLLERR:表示对应的文件描述符发生错误;
           EPOLLHUP:表示对应的文件描述符被挂断;
           EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
           EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里


第三个函数epoll_wait():

         收集在epoll监控的事件中已经发生的事件。参数events是分配好的epoll_event结构体数组,epoll将会把发生的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在用户态中分配内存)。maxevents告之内核这个events有多大,这个 maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。如果函数调用成功,返回对应I/O上已准备好的文件描述符数目,如返回0表示已超时。epoll事先通过epoll_ctl()来注册一个文件描述符,一旦基于某个文件描述符就绪时,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait()时便得到通知。


epoll的两种工作模式

          epoll有Level-Triggered和Edge-Triggered两种工作模式。

          Level-Triggered是缺省工作方式,有阻塞和非阻塞两种方式。内核告诉你一个描述符是否就绪,然后可以对就绪的fd进行IO操作。如果不做任何操作,内核还会继续通知,因此该模式下编程出错可能性小。传统的select/poll是这样的模型。此方式可以认为是一个快速的poll。

         Edge-Triggered是只支持非阻塞模式。当一个新的事件到达时,ET模式从epoll_wait调用中获取该事件,如果这次没有将该事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的。而LT模式,只要一个事件对应的套接字缓冲区中还有数据,就总能从epoll_wait中获取这个事件。

          二者的差异在于 level-trigger 模式下只要某个 fd 处于 readable/writable 状态,无论什么时候进行 epoll_wait 都会返回该 fd;而 edge-trigger 模式下只有某个 fd 从unreadable 变为 readable 或从 unwritable 变为 writable 时,epoll_wait 才会返回该 fd


        以下来自http://www.cppblog.com/peakflys/archive/2012/08/26/188344.aspx

    使用LT意味着只要fd处于readable/writable状态,每次 epoll_wait 时都会返回该 fd,系统开销不说,自己处理时每次都要把这些fd轮询一遍,如果fd很多的话,不管这些fd有没有事件发生,epoll_wait 都会触发这些fd的轮询判断。
    查阅了一些资料,才知道常用的事件处理库很多都选择了 LT 模式,包括大家熟知的libevent和boost::asio等,为什么选择LT呢?那就不得不从ET的弊端的弊端说起。
    ET模式下,当有事件发生时,系统只会通知你一次,也就是调用epoll_wait 返回fd后,不管事件你处理与否,或者处理完全与否,再调用epoll_wait 时,都不会再返回该fd,这样programmer要自己保证在事件发生时及时有效的处理完。比如此时fd发生了EPOLLIN事件,在调用epoll_wait 后发现此事件,programmer要保证在本次轮询中对此fd进行了读操作,并且还要循环调用recv操作,一直读到recv的返回值小于请求值,或者遇到EAGAIN错误,不然下次轮询时,如果此fd没有再次触发事件,你就没有机会知道这个fd需要你的处理。这样无形中就增加了programmer的负担和出错的机会。
   ET模式的短处正是LT模式的长处,无论此fd是否有事件发生,或者有事件未处理完,每次epoll_wait 时总会得到此fd供你处理。显而易见,OS在LT模式下维护的 ready list 的大小肯定比ET模式下长,而且你自己轮询所有的fd时也要比ET下要多,这种消耗和ET模式下循环调用处理函数(如recv和send等),还要逻辑处理是否处理完毕,理论上应该是LT更大一些,不过个人感觉应该差别不会太大。但是LT模式下带来的逻辑处理的方便性和不易出错性,让我们有理由把它作为首选。我想这可能也是为什么epoll后来在ET的基础上又增加了LT,并且将其作为默认模式的原因吧。

在epoll的ET模式下,正确的读写方式为:
         读:只要可读,就一直读,直到返回0,或者 errno = EAGAIN
         写:只要可写,就一直写,直到数据发送完,或者 errno = EAGAIN


例如,向socket中写数据:

                                   


从socket中读数据:

                         




       使用Linux epoll模型,水平触发模式(Level-Triggered);当socket可写时,会不停的触发socket可写的事件,如何处理?


第一种最普通的方式:  
    当需要向socket写数据时,将该socket加入到epoll模型(epoll_ctl);等待可写事件。
    接收到socket可写事件后,调用write()或send()发送数据。。。
    当数据全部写完后, 将socket描述符移出epoll模型。
   
    这种方式的缺点是:  即使发送很少的数据,也要将socket加入、移出epoll模型。有一定的操作代价。



第二种方式,(是本人的改进方案, 叫做directly-write)

    向socket写数据时,不将socket加入到epoll模型;而是直接调用send()发送;
    只有当或send()返回错误码EAGAIN(系统缓存满),才将socket加入到epoll模型,等待可写事件后,再发送数据。
    全部数据发送完毕,再移出epoll模型。

     这种方案的优点:   当用户数据比较少时,不需要epool的事件处理。
     在高压力的情况下,性能怎么样呢?   
      对一次性直接写成功、失败的次数进行统计。如果成功次数远大于失败的次数, 说明性能良好。(如果失败次数远大于成功的次数,则关闭这种直接写的操作,改用第一种方案。同时在日志里记录警告)
     在我自己的应用系统中,实验结果数据证明该方案的性能良好。
     
    事实上,网络数据可分为两种到达/发送情况:
     一是分散的数据包, 例如每间隔40ms左右,发送/接收3-5个 MTU(或更小,这样就没超过默认的8K系统缓存)。
     二是连续的数据包, 例如每间隔1s左右,连续发送/接收 20个 MTU(或更多)。



第三种方式:  使用Edge-Triggered(边沿触发),这样socket有可写事件,只会触发一次。
             可以在应用层做好标记。以避免频繁的调用 epoll_ctl( EPOLL_CTL_ADD, EPOLL_CTL_MOD)。  这种方式是epoll 的 man 手册里推荐的方式, 性能最高。但如果处理不当容易出错,事件驱动停止。



第四种方式:  在epoll_ctl()使用EPOLLONESHOT标志,当事件触发以后,socket会被禁止再次触发。
             需要再次调用epoll_ctl(EPOLL_CTL_MOD),才会接收下一次事件。   这种方式可以禁止socket可写事件,应该也会同时禁止可读事件。会带来不便,同时并没有性能优势,因为epoll_ctl()有一定的操作代价。


以上是关于epoll并发服务器的主要内容,如果未能解决你的问题,请参考以下文章

epoll并发

epoll并发服务器

select和epoll的前世今生

Linux高并发机制——epoll模型

epoll与selector的简单理解

epoll,select,poll的区别