文章目录
- epoll历史
- epoll的接口
- epoll_create
- epoll_wait
- epoll_ctl
- epoll原理
- 代码实验
前面的内容介绍了select多路转接,也分析了其利弊,后面用poll改良了select,解决了部分的缺点,但是对于一些核心的缺点还是不能保证,比如说遍历等,于是有了epoll,epoll是目前来说在多路转接版块使用最广泛,最好用的一种多路转接,所以本篇我们就重点分析一下epoll
epoll历史
epoll是后面才发展出来的,在最开始进行内核设计的时候,其实没有增加像epoll这样的内容,但是在后面的不断更新学习中,不断地对于效率有了更加高的要求,所以就诞生出了epoll这样的多路转接方案,而这个方案实际上和前面的select还有poll已经完全不一样了,因此下面对于epoll进行介绍
epoll的接口
epoll_create
这个接口就是创建一个epoll模型,并且会返回的也是一个文件描述符,具体的原理后面会谈
epoll_wait
这个接口是用来拿取数据的,第一个参数epfd就是前面创建模型时的文件返回值,通过这个文件描述符来找到这个epoll模型,后面的两个参数主要是用来返回的是已经就绪的fd和event,而最后一个参数则是超时时间,这里也不多赘述,和前面一样,具体是如何拿取的,后面会结合原理继续谈
在这当中存在一个epoll_event的结构体,看一下这个结构体的内容:
首先在这个结构体中包含一个32位的位图events,这个当中的内容是用来传递标记位的,第二个参数是一个新的结构体,而这个新的结构体本质上是联合,说明可以选择使用上面的任意一个字段来保持,比如说可以是用户级的数据,或者是一个事件等等,这里结合原理再说
epoll_ctl
这个函数是用来对于epoll模型做出对应的修改的,第一个参数是创建epoll模型的返回值
第二个参数是op操作,可以对于模型进行增删改,具体的选项如下
第三个参数fd是对于特定的某个fd指向的文件,而第四个参数表示的是某个特定的事件
epoll原理
先画出下面的这个原理图
对于底层的硬件设备来说,我们这里只关心网卡,在这个网卡上面会存在一个网卡驱动,然后是操作系统和各种的系统调用的接口,而在我们之前进行select和poll的时候,对于遍历的这个操作,本质上就是去查看这个文件描述符对应的某个资源和数据是否已经就绪了,如果没有就绪就让继续进行阻塞等待,此时这个进程就会被挂起到等待队列当中,而在底层操作系统进行定期的唤醒和调度的时候,轮到这个进程了,那么就继续对于这个内容进行检测对应的文件描述符,这就意味着,操作系统需要主动的去检查这个内容到底有没有就绪,相当于是把操作系统绑定在了这里
而在后来,系统的这种模式就发生了一定的变化
首先我们要明确的概念是,对于网卡来说,它就是一个外设,从纯硬件的角度来讲,对应的数据会先进入数据链路层,然后从数据链路层再一系列的向上交付,然后再进行解包等等,那在硬件层面上,操作系统会通过中断来知道资源有没有就绪,然后资源就被操作系统读取上去了
所以操作系统为了便于进行检测,就在内部维护了一颗红黑树,而在这个红黑树的节点当中,第一个就是对应的需要进行多路转接的文件描述符,其次是需要关心的事件,比如说有读事件,写事件,异常事件这些事件,整体上是采用了一个位图来进行描述表示,还有就是一些连接字段
其次,操作系统还会维护一个就绪队列,在这个就绪队列当中维护的节点其实和红黑树维护的节点基本相同,可以理解为这个节点既在红黑树当中,也在就绪队列当中,在就绪队列当中的节点必然是这个事件已经就绪了,那么可以理解为就是在红黑树当中有一个文件描述符3,并且这个3号文件关心的是写事件,而当这个写事件就绪的时候,就把这个节点从红黑树中转移到就绪队列当中,那么就有了新的节点入队列,后续就可以取出来了
最后还有一种机制,那就是在操作系统中会设置一些回调函数,这些回调函数是做什么的呢?比如当网卡用中断的方式把数据转移到了网卡驱动层,那么一旦网卡驱动层有数据了,那么这个数据链路层就会自动调用对应的callback回调,可以理解为是要进行向上交付,其次还可以理解为是数据来了,要进行解包,然后把这个解包的数据放到TCP的文件缓冲区当中让它进行解析等等,它还可以查找红黑树的一些信息,比如说用文件描述符作为键值进行关联,借助红黑树就可以查找到所关心的事件有没有就绪等等,如果查到了,就说明这事件已经就绪了,那么就构建节点插入到就绪队列当中
所以在操作系统当中,在使用epoll的时候,它会使用一些回调函数到底层,底层的资源就绪的时候,就进行硬件中断,然后交付给操作系统,然后就把数据放在了就绪队列当中,之后对于用户来说直接从就绪队列当中去拿就可以了
那在操作系统中是如何管理这个epoll模型的?从create接口就能看出来,本质上其实是用一个文件来进行管理的,所以下面的话题就是,对照着这个内容,再次看epoll的接口
代码实验
下面就用上面的原理,实现一份简单的epoll代码
首先在epoll的类中要包含有对应的一个epollfd和对应的timeout,还有对应的三个接口,创建epoll模型,进行epoll模型中获取消息队列的内容,修改epoll模型当中的字段,也就是修改红黑树:
class Epoller
{
public:Epoller(){}~Epoller(){}int EpollerWait(struct epoll_event* revents, int num){}int EpollerUpdate(int op, int sock, uint32_t enent){}
private:int _epfd;int _timeout;
};
如上就搭建出了一个比较基础版本的epoll,那么下面对于这当中的字段进行填充
#pragma once#include "Log.hpp"
#include <cerrno>
#include <cstring>
#include <sys/epoll.h>class Epoller
{static const int size = 128;
public:Epoller(){_epfd = epoll_create(size);if(_epfd == -1)lg(Error, "epoll create error : %s", strerror(errno));elselg(Info, "epoll create success : %d", _epfd);}// 获取指定fd下的revents字段,并返回有多少是就绪的int EpollerWait(struct epoll_event* revents, int num){int n = epoll_wait(_epfd, revents, num, -1);return n;}// 更新指定sock下的事件int EpollerUpdate(int op, int sock, uint32_t event){int n = 0;// 如果是删除操作,就置空即可if(op == EPOLL_CTL_DEL){n = epoll_ctl(_epfd, op, sock, nullptr);if(n)lg(Error, "epoll delete error");}// 如果是其他的操作,需要填写对应的字段else{struct epoll_event ev;ev.events = event;ev.data.fd = sock;n = epoll_ctl(_epfd, op, sock, &ev);if(n)lg(Error, "epoll ctl error");}return n;}~Epoller(){if(_epfd > 0)close(_epfd);}
private:int _epfd;int _timeout; // 没有使用这个字段
};
有了对于上述内容的封装,就可以填写对于epoll服务器的内容了,主要包括初始化,把listensock填写到epoll当中,然后再对其进行一系列操作即可
#pragma once#include <iostream>
#include <memory>
#include <sys/epoll.h>
#include "Socket.hpp"
#include "Log.hpp"
#include "Epoller.hpp"
using namespace std;class EpollServer
{
public:static const int num = 64;EpollServer(uint16_t port): _port(port), _listensocket_ptr(new Sock()), _epoller_ptr(new Epoller()){}void Init(){_listensocket_ptr->Socket();_listensocket_ptr->Bind(_port);_listensocket_ptr->Listen();lg(Info, "create socket success: %d", _listensocket_ptr->Fd());}// 处理链接void Accepter(){// 当获取了新链接之后,就准备进行读取的事件等待了string clientip;uint16_t clientport;int sock = _listensocket_ptr->Accept(&clientip, &clientport);if (sock > 0){// 如果想要读取,就继续加入到epoll的事件中进行等待,当就绪后会提醒的_epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, sock, EPOLLIN);lg(Info, "get a new link, sock is %d", sock);}}// 对于接受到可以读取的事件提醒后,就可以直接进行读取了void Recver(int fd){// demochar buffer[1024];ssize_t n = read(fd, buffer, sizeof(buffer) - 1); // bug?if (n > 0){buffer[n] = 0;cout << "get a messge: " << buffer << endl;string echo_str = "server echo $ ";echo_str += buffer;write(fd, echo_str.c_str(), echo_str.size());}else if (n == 0){lg(Info, "client quit, me too, close fd is : %d", fd);_epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);close(fd);}else{lg(Warning, "recv error: fd is : %d", fd);_epoller_ptr->EpollerUpdate(EPOLL_CTL_DEL, fd, 0);close(fd);}}void Dispatcher(struct epoll_event *revs, int num){// 和select一样,遍历fd,然后区分是listen的请求还是read的请求for (int i = 0; i < num; i++){uint32_t events = revs[i].events;int fd = revs[i].data.fd;// 我们这里只关心读事件if (events & EPOLLIN){// 如果是listensocket的事件,就调用listensocket的处理方式if (fd == _listensocket_ptr->Fd()){Accepter();}// 如果是读事件,就调用读事件对应的处理方式else{Recver(fd);}}// 其他的事件暂时不关心else{}}}void Start(){// 把listensocket添加到epoll当中_epoller_ptr->EpollerUpdate(EPOLL_CTL_ADD, _listensocket_ptr->Fd(), EPOLLIN);struct epoll_event revs[num];for (;;){// 等待epoll返回就绪的信息int n = _epoller_ptr->EpollerWait(revs, num);// 如果有事件就绪了就去处理if (n > 0){lg(Debug, "event happened, fd is : %d", revs[0].data.fd);Dispatcher(revs, n);}else if (n == 0){lg(Info, "time out");}else{lg(Error, "epoll wait error");}}}~EpollServer(){_listensocket_ptr->Close();}private:shared_ptr<Sock> _listensocket_ptr;shared_ptr<Epoller> _epoller_ptr;uint16_t _port;
};
经过测试这是可行的:
至此就完成了一个比较基础的epoll实现策略