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

Posted 努力学习的少年

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了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();             
     

以上是关于Linux & IO多路转接——epoll详解的主要内容,如果未能解决你的问题,请参考以下文章

典型I/O模型——阻塞IO,非阻塞IO,信号驱动IO,异步IO,IO多路转接(select&poll&epoll)

epoll(eventpoll)是干嘛的?IO多路转接技术(相较selectpoll的优点)

IO多路转接 ——— selectpollepoll

高效IO——多路转接epoll

五种高阶IO模型以及多路转接技术(selectpoll和epoll)及其代码验证

五种高阶IO模型以及多路转接技术(selectpoll和epoll)及其代码验证