再论epoll

之前已经讲过IO复用,这次重新拿出来还是因为服务器的高并发处理。将epoll再进行一次深挖。
**情景:**假设此时有100万个用户和一个进程保持着连接, 但并不是所有的都会在一个时刻活跃,也就是说某个时间内只有少数的用户是活跃状态。select和poll对此的处理就是全部轮询,但是可想而知,效率低下至极。而且需要将这100w个连接进行用户和内核之间的拷贝,消耗内存。所以,select和poll并不能处理如此大量的连接。
这就是epoll的高明所在。步步道来。

int epoll_create(int size);  
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的三部曲:

  1. create一个epoll对象,进程开始时创建后,其他只需要往上连接就可以。
  2. ctl添加或者删除连接
  3. wait收集就绪的事件

epoll

create后,linux会创建一个eventpoll结构体。这个结构体中有两个epoll的高效保证成员。
epoll是怎么保证增删查的高效性的呢?红黑树,用来存储连接的套接字。还有就是会建立一个双向链表,存储就绪的事件。wait不需要盯着庞大的红黑树,只需要查看链表就可以。有数据就返回,没有就等到timeout返回。
当epollwait检查是否有事件发生时,只需要检查双向链表即可,每个事件都是一个下面的epitem结构体。

struct epitem {
  ...
  //红黑树节点
  struct rb_node rbn;
  //双向链表节点
  struct list_head rdllink;
  //事件句柄等信息
  struct epoll_filefd ffd;
  //指向其所属的eventepoll对象
  struct eventpoll *ep;
  //期待的事件类型
  struct epoll_event event;
  ...
}; // 这里包含每一个事件对应着的信息。

同时,epoll在检测到事件后,将事件复制到用户态内存(此处可以利用mmap映射,共享内存,效率加快),将事件数量也返回给用户。所以epoll的整体事件处理效率很快。同时,添加删除事件时,从红黑树查找也很快。所以可以更快的处理更多的连接操作。
总结:
create的时候,红黑树和就绪链表创建。ctl时,先检查红黑树上存在与否,存在直接返回,不存在则添加上去,然后向内核注册相应的回调函数,当事件就绪时通过此回调插入就绪链表。wait直接检查就绪链表即可。

LT&ET

水平模式下:LT,只要文件描述符还有数据可读,每次wait都会通知程序处理。
边沿模式下:ET,检测到IO,wait通知文件描述符,用户程序必须立刻处理,如果没有读完,下次不会通知此描述符,也就是会丢失数据。

我对这个理解是,水平模式下会通知所有就绪的文件描述符,也就是说检测状态是否就绪来通知。边沿模式则是检测状态变化通知,上一次2fd没有读完,下一次wait他不会有状态变化,因此不会通知。
怎么选择的话,我的理解是:水平模式下大家相等,雨露均沾。边沿模式下,就好像有vip了,我关心这个描述符,立即处理他的事,处理不完下次他还会来,再继续处理。

正题-epoll反应堆

第一步:
epoll反应堆是和epoll接口时是不同的,我称它为epoll反应堆模型:epoll+ET+非阻塞+自定义结构体。
为什么自定义结构体?上面我们说过红黑树上对应的结构体,他说到底还是传入了文件描述符本身,此处我们将自定义结构体指针传入。自定义结构体如下:

struct my_events {  
    int        m_fd;                             //监听的文件描述符
    void       *m_arg;                           //泛型参数
    void       (*call_back)(void *arg);          //回调函数
    /*
     *  可以在此处封装更多的数据内容
     *  例如用户缓冲区、节点状态、节点上树时间等等
     */
};
 /*
 * 用户需要自行开辟空间存放my_events类型的数组,并在每次上树前用epoll_data_t里的  ptr指向一个my_events元素。
 */

可以看到,每个里面都有自己的回调函数,这样只需要将ptr指针传入,我们就可以让每个都有自己的处理函数。这样,在wait返回后,就可以直接调用回调函数。

 while(1) {
      /* 监听红黑树, 1秒没事件满足则返回0 */ 
      int n_ready = epoll_wait(ep_fd, events, MAX_EVENTS, 1000);
      if (n_ready > 0) {
         for (i=0; i<n_ready; i++) 
            events[i].data.ptr->call_back(/* void *arg */);
       }
       else
       /*
       *****
       */
 }

epoll模型步骤:
(1) 程序设置边沿触发以及每一个上树的文件描述符设置非阻塞
(2) 调用epoll_create()创建一个epoll对象
(3) 调用epoll_ctl()向epoll对象中进行增加、删除等操作
(4) 调用epoll_wait()(定时检测) 返回待处理的事件集合
(5) 依次调用事件集合中的每一个元素中的ptr所指向那个结构体中的回调函数

顺着我们的思路走一遍流程:
ET监听可读事件-》数据到来-》触发事件-》epoll_wait返回-》处理回调函数-》继续epoll_wait-》……循环

所以,只需改动一下就能成为我们的反应堆模型:
ET监听可读事件-》数据到来-》触发事件-》epoll_wait返回-》
接下来的回调函数内部我们需要干这些事(读取完数据-》此节点从树删除-》设置可写事件和对应的回调函数-》上树-》处理数据)-》
ET监听可写事件-》对方可读-》触发事件-》epoll_wait返回-》
接下来可写回调函数中(写完数据-》此节点从树删除-》设置可读事件和对应的回调函数-》上树-》处理数据)-》epoll_wait等待-》
……循环

看着好像有点摸不着头脑,第一是为什么要将节点从树上一直增删?
因为socket在完成收发的时候至少是要两个树上的位置的(一个读一个写),而这样增删只需要一个节点就可以了。
其实编程本来就是个很多时候不能双全的事,空间和时间。结合第二个一起看,为什么读写交叉设置?
因为上面一个原因,其实就是tcp的模式,一来一回,服务器收到,服务器回复。
还有就是可读不一定可写,如果在收到客户端数据后,接收端的接收滑动窗口已满,当前服务器就会阻塞在可写的状态,这样就出现了阻塞等待,如果多个一起,很容易就能使服务器效率下降。还有就是客户端发送完后就断开,这是服务器写时就会出错,导致进程终止。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值