通俗易懂的epoll

目录

  • 理解epoll工作原理
    • epoll的两种工作模式
  • 如何使用epoll
  • epoll的优点
  • 使用epoll实现一个服务器

理解epoll工作原理

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

通俗易懂的epoll_第1张图片
例子:我们在编写套接字编程的代码时,要把监听套接字放在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
#include
#include
#include
#include
#include
#include
#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
#include
#include
#include
#include
#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
#include
#include
#include
#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
#include
#include

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
#include
#include

设置非阻塞
void SelNonBlock(int sock)
{
     int fl = fcntl(sock, F_GETFL);
        fcntl(sock, F_SETFL, fl | O_NONBLOCK);
}
									server.cc
#include
#include
#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中添加函数就行。

你可能感兴趣的:(Linux,epoll,linux)