通俗易懂的epoll

Posted 世_生

tags:

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

目录

理解epoll工作原理

  • 每一个epoll对象都有eventepoll结构体
  • epoll底层是一颗红黑数来管理文件描述符中的事件。
  • 而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法
  • 当事件发生时,回调方法会拷贝一份该节点到一个队列中,该队列的用双链表实现的。
  • 在epoll中没一个事件都会建立节点(epitem结构体)


例子:我们在编写套接字编程的代码时,要把监听套接字放在epoll中来,让epoll帮我们来进行等待连接到来,连接到来事件叫做读事件。
这时候我们通过调用函数把监听套接字放入到红黑树中,Linux内核会对每一个事件对象创建一个epintm结构体。
判断也没有事件只需要通过rdllist是否为空。

struct epitem 
 struct rb_node rbn;//红黑树节点 
 struct list_head rdllink;//双向链表节点 
 struct epoll_filefd ffd; //事件句柄信息 
 struct eventpoll *ep; //指向其所属的eventpoll对象 
 struct epoll_event event; //期待发生的事件类型 

epoll的两种工作模式

LT:水平触发,不断的提醒你有事件到来,直到你把事件全部执行完

ET:边缘触发:只提醒你一次有事件到来,如果不执行,就要等下一次事件到来。

ET模式下:要采用非阻塞的方式进行读操作。且要不断的读,直到读完。
如果不采用非阻塞方式读取,则可能会阻塞住。

如何使用epoll

创建句柄
int epoll_create(int size);
返回一个整数,是一个文件描述符。
使用:int epfd=epoll_create(256);


添加事件到红黑树中
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
op:是一个宏
EPOLL_CTL_ADD:添加
EPOLL_CTL_MOD:修改
EPOLL_CTL_DEL:删除

struct epoll_event
	uint32_t     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(写事件)。
返回值:成功返回0,失败返回-1

//拿出事件
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
timeout:轮询检查的事件
返回值:成功返回事件到达的数量。
使用:
	int epfd=epoll_create(256);
	
	struct epoll_event item;
	item.data.fd=sock;     //添加套接字
	item.events=EPOLLIN   //只关心读时间
	epoll_ctl(epfd,EPOLL_CTL_ADD,sock,&item);//添加事件到红黑树
	
	struct events eve[64];
	int num=epoll_wait(epfd,eve,64,1000);
	//有事件到来,会把事件的节点拷贝到eve数组中,
	//我们只需要遍历数组就可以进行读或者写。
	for(int i=0;i<num;i++)
		if(eve[i].events & EPOLLIN)
			int sock=eve[i].data.fd;
			开始进行读操作(进行读操作时要区分是连接到来,还只是进行读取)
		
		else if(eve[i].event& EPOLLOUT)
			int sock=eve[i].data.fd;
			开始进行写操作
		
	

epoll的优点

  • 接口使用方便: 虽然拆分成了三个函数, 但是反而使用起来更方便高效. 不需要每次循环都设置关注的文件描述符, 也做到了输入输出参数分离开
  • 数据拷贝轻量: 只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中, 这个操作并不频繁(而select/poll都是每次循环都要进行拷贝)
  • 事件回调机制: 避免使用遍历, 而是使用回调函数的方式, 将就绪的文件描述符结构加入到就绪队列中,
  • epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪. 这个操作时间复杂度O(1). 即使文件描述符数目很多, 效率也不会受到影响.
  • 没有数量限制: 文件描述符数目无上限

使用epoll实现一个服务器

									sock.hpp//创建套接字
#pragma once
#include<iostream>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<unistd.h>
#include<cstring>
#include"log.hpp"

#define LONG 5

using namespace std;
class Socket
    public:
        static void Sock(int& listen_sock)
        
            listen_sock=socket(AF_INET,SOCK_STREAM,0);
            if(listen_sock<0)
                LOG(ERROR,"socket error...");
                exit(1);
            
            LOG(INFO,"socket success...");
        
        static void Bind(int listen_sock,int port)
        
            struct sockaddr_in own;
            memset(&own,0,sizeof(own));
            own.sin_family=AF_INET;
            own.sin_port=htons(port);
            own.sin_addr.s_addr=INADDR_ANY;
            if(bind(listen_sock,(struct sockaddr*)&own,sizeof(own))<0)
                LOG(error,"bind error...");
                exit(2);
            
            LOG(INFO,"bind sucess...");
        
        static void Listen(int listen_sock)
        
            if(listen(listen_sock,LONG)<0)
                LOG(error,"listen error...");
                exit(3);
            
            LOG(INFO,"listen succsee...");
        
;
									reactor.hpp
#pragma once
#include<iostream>
#include<string>
#include<unordered_map>
#include<unistd.h>
#include<sys/epoll.h>
#include"log.hpp"
using namespace std;
#define MAX_NUM 64

class Reactor;
class EventItem;

typedef int(*callback_t)(EventItem *);

每一个事件都要对应一个这个结构体
class EventItem

    public:
        //定义回调函数
        callback_t recv_handler;
        callback_t send_handler;
        //sock
        int sock;
        //缓存
        string inbuffer;
        string outbuffer;
        //回指向Reactor
        Reactor* R;
    public:
        EventItem():sock(0),R(nullptr),recv_handler(nullptr),send_handler(nullptr)
        
		
		//注册回调函数
        void MakeCallBack(callback_t _recv,callback_t _send)
        
            recv_handler=_recv;
            send_handler=_send;
        

        ~EventItem()
;

class Reactor

    private:
    //epoll句柄
    int epfd;
    //使用哈希容器来一一对应
    unordered_map<int,EventItem> mp;

    public:
    
    Reactor()
    void InitReactor()
    
        epfd=epoll_create(256);
        if(epfd<0)
            LOG(ERROR,"epoll_create error...");
            exit(5);
        
        LOG(INFO,"epoll_create success..."+to_string(epfd));
    
	
	添加事件到红黑树中
    void AddToEpoll(int sock,uint32_t ev,const EventItem& item)
    
        struct epoll_event event;
        event.events=0;
        event.events|=ev;
        event.data.fd=sock;

        string s=to_string(sock);
        if(epoll_ctl(epfd,EPOLL_CTL_ADD,sock,&event)<0)
            LOG(ERROR,"epoll_ctl add error...sock:"+s);
        
        else
            mp.insert(sock,item);
            LOG(INFO,"epoll_ctl add success...sock:"+s);
        

    

	删除红黑树中的事件
    void RevokeEpoll(int sock)
    
        if(epoll_ctl(epfd,EPOLL_CTL_DEL,sock,nullptr)<0)
            string s=to_string(sock);
            LOG(ERROR,"epoll_ctl del error...sock:"+s);
        
        删除哈希中的映射
        mp.erase(sock);
    
    void Assignment(int timeout)
    
        struct epoll_event revs[MAX_NUM];
        int num=epoll_wait(epfd,revs,MAX_NUM,timeout);
        
        事件到来轮询recv中的节点
        for(int i=0;i<num;i++)
            int sock=revs[i].data.fd;
            uint32_t mask=revs[i].events;
            if(mask & EPOLLIN)
                if(mp[sock].recv_handler)
                    mp[sock].recv_handler(&mp[sock]);调用回调函数进行读
            
            else if(mask & EPOLLOUT)
                if(mp[sock].send_handler)
                    mp[sock].send_handler(&mp[sock]);调用回调函数进行写
            
        
    
    ~Reactor()
    
        if(epfd>=0)
            close(epfd);
    
;
									insertface.hpp
#pragma once
#include<iostream>
#include<cstring>
#include<sys/types.h>
#include<sys/socket.h>
#include"reactor.hpp"
#include"util.hpp"


int recver(EventItem* eve);

连接到来
int accepter(EventItem* eve)

    struct sockaddr_in oth;
    socklen_t len=sizeof(oth);
    int sock=accept(eve->sock,(struct sockaddr*)&oth,&len);
    if(sock<0)

    
    else
        SelNonBlock(sock);

        EventItem item;
        item.sock=sock;
        item.R=eve->R;

        item.MakeCallBack(recver,nullptr);
        Reactor* ptr=eve->R;
        ptr->AddToEpoll(sock,EPOLLIN|EPOLLET,item);
    


只写了连接到来的接口,读没有写
int recver(EventItem* eve)



									log.hpp//日志,方便知道走到哪里了
#pragma once
#include<iostream>
#include<string>
#include<time.h>

using namespace std;

#define INFO 1
#define ERROR 2

采用宏来调用,具有可读性
#define LOG(str1,str2) log(#str1,str2,__FILE__,__LINE__)

void log(string str1,string str2,string str3,int line)

    cout<<"["<<str1<<"]"<<"["<<time(nullptr)<<"]"<<"["<<str2<<"]"<<"["<<str3<<"]"<<"["<<line<<"]"<<endl;

									util.hpp
#pragma once
#include<iostream>
#include<unistd.h>
#include<fcntl.h>

设置非阻塞
void SelNonBlock(int sock)

     int fl = fcntl(sock, F_GETFL);
        fcntl(sock, F_SETFL, fl | O_NONBLOCK);

									server.cc
#include<iostream>
#include<cstdlib>
#include"sock.hpp"
#include"reactor.hpp"
#include"insertface.hpp"
#include"util.hpp"
int main(int argc,char* argv[])

    if(argc!=2)
        exit(4);
    
    int port=atoi(argv[1]);
    int listen_sock=-1;
    
    创建套接字
    Socket::Sock(listen_sock);
    Socket::Bind(listen_sock,port);
    Socket::Listen(listen_sock);
	
	创建Reactor,并初始化
    Reactor Re;
    Re.InitReactor();
	
	创建监听套接字对应的结构
    EventItem item;
    item.sock=listen_sock;
    item.R=&Re;
	
	把监听套接字设置成非阻塞
    SelNonBlock(listen_sock);
    
	注册回调函数,我这里把读注册了accept,没有注册写
    item.MakeCallBack(accepter,nullptr);
	
	添加到红黑树中,采用ET模式
    Re.AddToEpoll(listen_sock,EPOLLIN|EPOLLET,item);

    int timeout=1000;

	一直调用检查事件是否到来
    while(true)
        Re.Assignment(timeout);
    



整个的精髓是,把 epoll 和	读写分开了,采用了回调函数的方法,只不要管reactor.hpp,只需要在insertface.hpp中添加函数就行。

以上是关于通俗易懂的epoll的主要内容,如果未能解决你的问题,请参考以下文章

通俗易懂的epoll

通俗易懂,一篇文章带你认识Kafka

select,poll,epoll最简单的解释

通俗易懂:冒泡排序

通俗易懂的JUC源码剖析-CompletionService

控制反转和依赖注入的理解(通俗易懂)