• 为什么epoll会那么高效


    参考(原文简直超赞):https://zhidao.baidu.com/question/687563051895364284.html
    下面是我结合原文写的,为了便于自己理解:
    关于阻塞和非阻塞的理解可以看这个:http://www.cnblogs.com/xcywt/p/8146123.html


    1.举例子说明
    假设你在读大学,有个朋友F来找你,你住在A栋。但是不知道具体是哪个房间。于是你们约好在A栋门口见面。
    如果用阻塞IO模型来处理这个问题,你就相当于一直在A栋门口等着,这个时候你不能做别的事情,效率比较低,如果F一直不来你就得一直在那等着。
    接着来看用非阻塞模型来处理这个问题,主要有两种select/poll(这两个可以看成一种)和epoll:
    select大妈做的事情是这样:当朋友F到了楼下时,她带着F一个个房间了轮询的去找你。
    epoll大妈就比较高级了:大妈拿本子记录下你的房间号,当朋友F来的时候告诉F你的房间号。这样就不用整栋楼去跑了。
    在大并发服务器中,轮询IO是一件比较费时的操作,就跟select大妈一样。
    epoll大妈多用了一个本子,就有点用空间去换取时间的意思。


    2.select/poll为什么慢:
    1)select/poll 是遍历所有添加进fd_set的fd。并且需要将所有用户态的fd拷贝到内核态。数量巨大时这个效率比较慢
    2)并且返回之后,还要轮询将所有集合查询一次
    3)内核空间的数据需要拷贝到用户空间


    3.epoll的实现原理:

    具体使用方法可以参考:http://www.cnblogs.com/xcywt/p/8146094.html
    先说几个函数的作用
           int epoll_create(int size); // 创建一个epoll对象,size是内核保证能够正确处理的最大句柄数。
           int epoll_create1(int flags);// 上面的加强版本,参数只能是EPOLL_CLOEXEC
           int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 操作epoll对象
           int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);// 在给定时间内,监控的所有句柄中有时间发生就返回
    下面我们来看具体做了什么:
      epoll在内核初始化的时候向内核注册了一个文件系统,用于存储上述被监控的socket,同时还会开辟出epoll自己的内核高速cache区,用于安置需要监控的fd。这些fd以红黑树的形式保存在内核cache里,以支持快速的查找、插入、删除。这个内核高速cache区,就是建立连续的物理内存页,然后在之上建立slab层,简单的说就是物理上分配好你想要的大小的内存对象,每次使用时都是使用空闲的已分配好的对象。


      每次调用epoll_create时,会在这个虚拟的epoll文件系统里创建一个file节点,在内核cache中建立个红黑树来存储通过epoll_ctl添加进来的fd。这些fd其实已经在内核态了,当你再次调用epoll_wait时,不需要再拷贝进内核态(select需要再全部拷贝到内核态)


      同时还会建立一个list链表,用来存储已经就绪的事件。被epoll_wait调用时,就去看这个list链表是不是为空,若不为空就返回,为空就等待指定的事件再返回。


      list链表是如何维护的呢:当我们执行epoll_ctl时,会把对应fd放到红黑树中,还会给内核终端处理程序注册一个回调函数。如果这个句柄的中断到了,就把它放在list链表中去。
      总结一下:一棵红黑树和一个list链表就解决大并发的问题。epoll_create时创建红黑树和就绪链表,epoll_ctl时添加到红黑树中(若存在则不添加)并向内核注册回调函数。epoll_wait时返回list就绪链表里面的数据就可以了。


    4.epoll的两个工作模式:
    LT:只要一个句柄上的事件一次没有处理完,接着调用epoll_wait时仍然会返回这个句柄。
    ET:尽在空闲状态->就绪状态返回一次。
    这件事是怎么做到的呢:当有fd'发生事件时,就放到list就绪链表中去了。然后epoll_wait返回,再然后清空准备list就绪链表。
    最后如果是LT模式,并且仍有未处理的事件,就把这个fd重新放回到list就绪链表中。
    如果是ET,就不管了,不管有没有事件未处理完都不再添加到list就绪链表中。

    就有点像下面的流程:

    wait返回 -> 清空list就绪链表
    if(LT模式)
    {
      if(存在未处理完的事件)
      {
        重新添加进list就绪链表中
      }
    }
    else // ET 模式
    {
    
    }

    关于触发模式详解,这里面也讲的比较详细:
    http://blog.csdn.net/weiyuefei/article/details/52242778
    5.ET模式被唤醒的条件
    对于读取操作:
    1)buffer由不可读,变为可读的时候。
    2)buffer数据变多的时候,有新的数据到来
    3)当buffer不为空(有数据可读),且用户对相应fd进行epoll_mod  IN 事件时。(待会用代码演示)
    对于写操作:

    1)由不可写,变成可写
    2)buffer是数据变少的时候,也就是被读走了一部分3)buffer有可写空间,且用户对相应fd进行epoll_mod OUT 事件时。


    对于LT模式:

    读操作:只要缓冲区中有数据,且读完一部分之后还不空的时候,就会返回

    写操作:当发送缓冲区没满,写了一下还不满的时候,epoll_wait返回读事件。

    补充一个例子1:验证ET模式的读取返回的前2个:

    #include<unistd.h>
    #include<iostream>
    #include<sys/epoll.h>
    using namespace  std;
    int main()
    {
        int epfd, ret;
        struct epoll_event ev, events[5];
        epfd = epoll_create(1);
        ev.data.fd = STDIN_FILENO;
        ev.events = EPOLLIN|EPOLLET; // 标记A,这里是ET模式
        //ev.events = EPOLLIN; // 标记B。表示默认是LT模式
        char buf[1024] = {0};
        epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev); //添加标准输入
        while(1)
        {
            ret = epoll_wait(epfd, events, 5, -1);
            for(int i=0; i < ret; i++)
            {
                if(events[i].data.fd == STDIN_FILENO)
                {
                    //read(STDIN_FILENO, buf, sizeof(buf)); // 标记C
                    cout << "hello world, recv:" << buf << endl;
                }
            } 
        }
        return 0;
    }

    分三种情况讨论:
    1)打开标记A,注释B和C:这种情况运行,虽然输入缓冲区里面还有数据,但是“hello world”也不会一直打印。
    因为边沿触发,一定要等到下一次事件到来 wait才会返回。
    2)打开B,注释A和C:切换成了LT模式,只要缓冲区里面还有数据吗,wait会一直返回。所以helloworld会一直打印
    3)打开B和C,注释A:LT模式,但是每次wait之后把缓冲区里面的数据读完了,相当于处理完了这个事件。wait就不会返回了。除非标准输入中再输入数据。

    例子2:验证ET模式的读取返回的第3个:

    #include<unistd.h>
    #include<iostream>
    #include<sys/epoll.h>
    using namespace  std;
    int main()
    {
        int epfd, ret;
        struct epoll_event ev, events[5];
        epfd = epoll_create(1);
        ev.data.fd = STDIN_FILENO;
        ev.events = EPOLLIN|EPOLLET; 
        char buf[1024] = {0};
        epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);
        while(1)
        {
            ret = epoll_wait(epfd, events, 5, -1);
            for(int i=0; i < ret; i++)
            {
                if(events[i].data.fd == STDIN_FILENO)
                {
                    cout << "hello world << endl;
                    ev.data.fd = STDIN_FILENO;
                    ev.events = EPOLLIN|EPOLLET;
                    epoll_ctl(epfd, EPOLL_CTL_MOD, STDIN_FILENO, &ev); // 这里对fd进行epoll_mod  IN 事件
                }
            } 
        }
        return 0;
    }

    可以看到当输入一次之后,依然会有死循环打印helloworld。

    例子3:验证ET模式的写返回,前2个

    #include<unistd.h>
    #include<iostream>
    #include<sys/epoll.h>
    using namespace  std;
    int main()
    {
        int epfd, ret;
        struct epoll_event ev, events[5];
        epfd = epoll_create(1);
        ev.data.fd = STDIN_FILENO;
        ev.events = EPOLLOUT|EPOLLET; 
        char buf[1024] = {0};
        epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);
        while(1)
        {
            ret = epoll_wait(epfd, events, 5, -1);
            for(int i=0; i < ret; i++)
            {
                if(events[i].data.fd == STDIN_FILENO)
                {
                    //cout << "hello world" << endl; // 标记A
                    cout << "hello world"; // 标记B
                }
            } 
        }
        return 0;
    }

    对于ET模式。
    1)打开标记A,注释标记B:可以看到会死循环,因为这里有 endl 。标准输出为控制台的时候缓冲的“行缓冲”,所以换行符号导致buffer中的内容被清空。就相当于上面条件中的第二个,有数据发送走了。所以会一直循环
    2)打开B,注释A:不发送endl,就相当于buffer中一直有数据存在,所以wait不会一直返回。

    例子4,ET模式的写返回第三个条件。

    #include<unistd.h>
    #include<iostream>
    #include<sys/epoll.h>
    using namespace  std;
    
    int main()
    {
        int epfd, ret;
        struct epoll_event ev, events[5];
        epfd = epoll_create(1);
        ev.data.fd = STDIN_FILENO;
        ev.events = EPOLLOUT|EPOLLET;
    
        char buf[1024] = {0};
        epoll_ctl(epfd, EPOLL_CTL_ADD, STDIN_FILENO, &ev);
        while(1)
        {
            ret = epoll_wait(epfd, events, 5, -1);
            for(int i=0; i < ret; i++)
            {
                if(events[i].data.fd == STDIN_FILENO)
                {
                    cout << "hello world";
                    ev.data.fd = STDIN_FILENO;
                    ev.events = EPOLLOUT;
                    epoll_ctl(epfd, EPOLL_CTL_MOD, STDIN_FILENO, &ev); // 这里对fd进行epoll_mod  OUT 事件
                }
            } 
        }
    
        return 0;
    }

    每次输出helloworld后重新MOD OUT 事件。也会一直循环打印。
    注意:LT模式没有验证

  • 相关阅读:
    hdu 1199 Color the Ball 离散线段树
    poj 2623 Sequence Median 堆的灵活运用
    hdu 2251 Dungeon Master bfs
    HDU 1166 敌兵布阵 线段树
    UVALive 4426 Blast the Enemy! 计算几何求重心
    UVALive 4425 Another Brick in the Wall 暴力
    UVALive 4423 String LD 暴力
    UVALive 4872 Underground Cables 最小生成树
    UVALive 4870 Roller Coaster 01背包
    UVALive 4869 Profits DP
  • 原文地址:https://www.cnblogs.com/xcywt/p/8146143.html
Copyright © 2020-2023  润新知