【Linux & IO多路转接】——epoll详解

目录

一. epoll简介

二. epoll相关系统的调用

1. epoll_create

2. epoll_ctl

3. epoll_wait

 三. epoll工作方式

1. 水平触发模式(level-triggered,LT)

2. 边缘触发模式(edgetriggered,ET)

四.简易的epoll服务器代码编写


  • 💂 个人主页:努力学习的少年
  • 🤟 版权: 本文由【努力学习的少年】原创、在CSDN首发、需要转载请联系博主
  • 💬 如果文章对你有帮助、欢迎关注、点赞、收藏(一键三连)和订阅专栏哦

一. epoll简介

epoll的功能一样跟select和poll一样,都是用来检测文件描述符中的事件是否就绪,当有事件就绪,可以通知给应用层,上层调用 read,recv,write,send 等类似接口就不会被阻塞。

我们之前学过select,poll应该知道,select 和 poll 有如下缺陷:

  • 它们需要 额外创建数组保存文件描述符,每一次检测时候,都需要将数组中的文件描述重新设置设置进 文件描述符集 中。
  • 除此之外,调用select,epoll检测文件描述符集是否有文件描述符事件就的事件复杂度
  • O(N),因为内核需要依次检测文件描述符集中每个文件符的事件是否就绪。
  • select中的文件描述集能够设置的文件描述符是有限的.

epoll通过两方面就很好的解决了select和epoll的缺陷

  • 第一, epoll在内核中使用 红黑树 跟踪进程所有待检测的文件描述符,把需要监 控socket通过epoll_ctl函数加入到内核的红黑树里,红黑树是个高效的数据结构,它的增删查改的时间复杂度是O(logN),当需要进行加入某个文件描述符进行跟踪检测,需要epoll_ctl接口将文件描述符到红黑树中,添加到红黑树的文件描述符则会不断的进行检测,如果想取消 epoll跟踪的检测某个文件描述符,则也可以使用epoll_ctl接口将红黑树中相对应的节点给删除掉.
  • 第二,epoll使用事件的驱动机制内核中会维护着一个就绪队列,当某个文件描述符有事件发生时,则通过回调函数内核将其事件加入到这个就绪队列中,当用户调用epoll_wait接口时,通过就绪队列是否为空来判断是否有某个文件描述符的事件就绪,如果不为空,则说明有文件描述符就绪,则返回就绪队列中文件描述符的个数,因此epoll检测是否有文件描述符就绪的时间复杂度时O(1)。

二. epoll相关系统的调用

1. epoll_create

int epoll_create(int size);

调用epoll_create后,内核会创建一个epoll_create对象,对象中包括跟踪检测事件的红黑树,就绪队列回调机制

  • 参数: 自从linux2.6.8之后,size参数是被忽略的。
  • 返回值:创建epoll_create后会返回一个epoll对象的文件描述符,调用者可以通过文件描述符访问到epoll对象

2. epoll_ctl

epoll_ctl 接口是用来 维护 epoll 对象中红黑树的节点,epoll_ctl可以在红黑树中添加,删除,修改节点

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

 参数:

  • epfd:eppoll对象的文件描述符
  • op:选择修改epoll中红黑树的方式,如下:
  1.   EPOLL_CTL_ADD:往红黑树中插入节点
  2.   EPOLL_CTL_MOD:修改红黑树中的节点的信息。
  3.   EPOLL_CTL_DELL:删除红黑树中节点。
  • fd:文件描述符。
  • 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:表示读事件
  • EPOLLOUT:表示写事件。
  • EPOLLPRI:表示有紧急数据可以读。
  • EPOLLET:表示使用ET的工作方式。

如果想要设置events多个条件,可以将用" | “表示,比如,既想要读事件又想要是ET的触发事件方式,则可以用 EPOLLIN | EPOLLET 表示。

epoll_data是一个联合体,他只能记录一个信息,他可以是指针,或者是一个文件描述符等等.如果是epoll服务器,epoll_data中一般记录的是socket文件描述符.

返回值

调用成功,返回0,调用失败返回-1,并设置errno错误码.

epoll_ctl本质是 以 fd-event 作为 key-value 映射关系插入到红黑树中,底层会根据红黑树节点的event中 events 判断是 需要检测读事件 还是检测写事件

3. epoll_wait

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

描述:调用epoll_wait能够获取就绪队列中已经就绪的事件

  • epfd:epoll对象的文件描述符
  • events:将从就绪队列中获取到的事件信息保存进 events 数组中中,上层就可以通过eventsl中获取到事件信息判断接下来的操作,如果想要从就绪队列中取出多个文件描述符信息,则需要传进去一个event_poll类型的数组。
  • maxevents:期望获取就绪队列中的事件的个数
  • timeout:在内核阻塞为timeout秒,直到就绪队列不为空。
  • 返回值:成功返回获取到的事件的数量,返回0,表明在timeout时间内就绪队列一直为空,返回-1表示epoll_wait发生错误,并且设置errno错误码.

epoll_wait本质是从就绪队列中已经就绪的节点event信息复制上来,上层可以通过event的信息判断是哪一个文件描述符事件就绪,events中判断是读事件就绪还是写事件就绪

 三. epoll工作方式

epoll有两种触发模式,一种是水平触发(level-triggered,LT),

另一种是边缘触发(edgetriggered,ET),epoll默认的的工作方式是LT,如果想要设置ET工作方式,需要使用epoll_ctl进行设置。

1. 水平触发模式(level-triggered,LT)

使用水平触发模式,当socket缓冲区如果一直有数据,则就会一直触发回调函数将其socket的事件加入到就绪队列中,只有当socket缓冲区中没有数据,才不会触发回调函数.水平触发模式的socket,水平触发模式的socket可以不用一次性读取socket缓冲区中的数据,因为只要socket缓冲区有数据,则会一直触发回调函数,将socket的事件加入到就绪队列中,上层调用epoll_wait则就可以一直获得到该socket文件描述符.

2. 边缘触发模式(edgetriggered,ET)

  • 使用边缘触发,当底层的socket文件描述符中的缓冲区出现变化的时候(缓冲区数据从无到有,从有到多),才会触发回调函数将socket的事件加入到就绪队列中
  • 如果socket缓冲区中没有发生变化,则socket一直不会被触发.即使相对应的socket缓冲区中有数据,。
  • 如果是ET模式触发的socket,则每次都需要通过循环调用recv将事件中的socket缓冲区中的数据读取干净,如果没有将数据读取干净,那么下次socket的缓冲区没有数据就绪,那么就一直不会触发socket事件,socket事件就不会加入到就绪队列中,那么socket缓冲区剩下的数据就一直不会读取上来.
  • 这里有个小细节,如果是ET模式触发的socket,那么往 socket中的缓冲区读取数据时,使用recv 或者 read 等接口时 去读取缓冲区的数据 一定要设置为非阻塞,因为每次读取都需要循环调用recv接口 去读取socket缓冲区的数据,最后一次读取socket缓冲区一定是为空,则最后一次recv不会读取的时候不会被阻塞在内核中。如果调用read,recv是阻塞读取,那么读取到socket缓冲区为空时,则read,recv则会阻塞在内核中,等待socket数据就绪,此时就相当于破坏了epoll的作用,因为epoll的作用本质是消除recv和read等接口等待数据就绪的过程。

总结:

使用边缘触发模式的效率相比使用水平触发模式的效率更高,因为 边缘触发模式 会逼迫上层一次性读完缓冲区,如果没有读取干净,则剩下的数据可能就不会读取到。每次检测socket触发回调机制,回调机制是会消耗cpu资源。

四.简易的epoll服务器代码编写

server.hpp文件

#pragma once    
#include<iostream>                                                                                                                                                                          
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<string.h>
#define LOG_NUM 5
using std::cout;
using std::endl;
namespace sjp{    
  class server{    
    public:    
    server(){}    
    ~server(){}    
    
    //创建套接字    
    static int Socket(){    
       int sockfd=socket(AF_INET,SOCK_STREAM,0);    
       return sockfd;    
    }    
        
    //绑定套接字接口    
    static bool Bind(int sockfd,unsigned short int port){    
      struct sockaddr_in s;    
      memset(&s,'\0',sizeof(s));    
      s.sin_family=AF_INET;    
      s.sin_port=htons(port);    
      s.sin_addr.s_addr=0;    
      if(bind(sockfd,(struct sockaddr*)&s,sizeof(s))<0){    
        cout<<"bind error"<<endl;    
        _exit(-1);    
      }    
      return true;    

    }

    //监听套接字
    static bool Listen(int sockfd){
      int i=listen(sockfd,LOG_NUM);
      if(i==-1){
        cout<<"listen fail"<<endl;
        _exit(-2);
      }
      return true;
    }
  };
}       

epoll_server.hpp文件

#pragma once
#include"server.hpp"
#define NUM 1024
#define WAIT_NUM 32;
#include<sys/epoll.h>

namespace ep_server{

  class EpollServer{
    private:
      int port;//端口号
      int listen_sock;//监听套接字                                                                                                                                                          
      
      int epfd;
    public:
      EpollServer(int _port):port(_port){}

      void InitServer(){
        listen_sock=sjp::server::Socket(); 
        sjp::server::Bind(listen_sock,port);
        sjp::server::Listen(listen_sock);
        epfd=epoll_create(NUM);
      }

     void Run(){
       Addevent(listen_sock,EPOLLIN);
        while(1){
          struct epoll_event ep[32];//保存就绪事件
          int sz=epoll_wait(epfd,ep,32,1000);//sz是获取就绪事件的个数
          if(sz>0){
            for(int i=0;i<sz;i++){
              if(ep[i].events==EPOLLIN){
                //可读事件
                if(ep[i].data.fd==listen_sock){
                  //监听套接字就绪,读取socket
                  //LT触发可以不用一次性将所有链接读上来
                  //读取到事件的信息
                  struct sockaddr peer;
                  socklen_t len;
                  int fd=accept(listen_sock,&peer,&len);
                  if(fd>0){
                    Addevent(fd,EPOLLIN);//将新的socket添加到红黑树中
                  }
                }
                else{
                  char str[1024];
                  size_t sz=recv(ep[i].data.fd,(void*)str,1024,MSG_DONTWAIT);
                  if(sz>0){
                    str[sz]='\0';
                    cout<<str<<endl;
                  }else if(sz==0){
                                                                                                                                                                                            
                  }
                  else{
                     //对端关闭,需要在红黑树中删除等待事件
                     Deletevent(ep[i].data.fd);
                     close(ep[i].data.fd);//关闭socket
                  }
                }
              }
              else if(ep[i].events==EPOLLOUT){
                //可写事件
                
              }
              else{
                //其他事件

              }
            }
          }else if(sz==0){
            cout<<"without file fd"<<endl;
          }else{
            cout<<"epoll_wait failing"<<endl;
          }
        }
      }
      void Deletevent(int fd){        
        if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)<0){
          cout<<"delete event failing,fd :"<<fd<<endl;
        }
      }

      //添加等待事件函数
      void Addevent(int fd,uint64_t event){
         struct epoll_event _event;
         _event.events=event;
         _event.data.fd=fd;
         if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&_event)<0){
           cout<<"Add epoll fail,fd :"<<fd<<endl;
         }
      }      
  };
}

epoll_server.cc

#include"epoll_server.hpp"    
#include<stdlib.h>    
    
void Usage(){    
  cout<<"Usage Way: epollserver port"<<endl;    
}    
int main(int argc,char* argv[]){    
  if(argc!=2){    
    Usage();    
  }    
    
  int port= atoi(argv[1]);    
  //创建epoll服务器对象                                                                                                                                                                     
  ep_server::EpollServer* es=new ep_server::EpollServer(port);    
  es->InitServer();      
  es->Run();             
}     
  • 22
    点赞
  • 67
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值