目录
一、高级IO
1.1 概念
1.2 五种IO模型
1.3 小结
二、多路转接的实用派
2.1 epoll 接口
2.1.1 epoll_create
2.1.2 epoll_ctl
2.1.3 epoll_wait
2.2 epoll 底层原理
2.2.1 epoll_ctl
2.2.2 epoll_wait
2.2.3 epoll_create
三、 epoll 类的编写
3.1 类的框架
3.1.1 私有成员
3.1.2 构造函数
3.1.3 析构函数
3.2 类的执行 Loop
3.2.1 Loop 框架
3.2.2 handlerEvent
四、epoll 的优点
一、高级IO
1.1 概念
了解了网络通信相关的知识后,我们也许能直到,通信的本质就是IO,通信的核心是在两个或多个设备之间传输数据,这与计算机系统中的输入输出操作类似。
 当通信时使用接收端口例如 recv 时,系统等待网络数据包从远程服务器通过网络传输到本地机器,数据包从网卡的硬件缓冲区复制到系统内存中的应用程序缓冲区;当文件读取时,系统等待磁盘将所请求的数据读取到磁盘缓冲区中,数据从磁盘缓冲区复制到系统内存中的用户空间。
 所以换种说法,IO = 等待 + 拷贝
那么如何提高IO的效率呢?
当缩小了等待的时间后,IO的效率就会提高。 
1.2 五种IO模型
然后,从一个钓鱼的实例引入今天的主题:
将上面的例子抽象成通信IO:
水池:OS内部缓冲区
水桶:用户缓冲区
鱼:数据
鱼竿:文件描述符
上面的五个人物分别对应了五种IO模型:

 其中,前四个人都属于同步IO,即只要参与了IO的过程,那就是同步IO。田七将钓鱼的工作交给了另一个人,并没有参与IO,所以是异步IO。
阻塞 IO:在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。阻塞 IO 是最常见的 IO 模型。
非阻塞 IO:如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回 EWOULDBLOCK 错误码。非阻塞 IO 往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对 CPU 来说是较大的浪费,一般只有特定场景下才使用。
信号驱动 IO:内核将数据准备好的时候,使用 SIGIO 信号通知应用程序进行 IO 操作。
IO 多路转接: 虽然从流程图上看起来和阻塞 IO 类似。实际上最核心在于 IO 多路转接能够同时等待多个文件描述符的就绪状态。
异步 IO: 由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。
1.3 小结
任何 IO 过程中,都包含两个步骤。第一是等待,第二是拷贝。而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间。让 IO 更高效,最核心的办法就是让等待的时间尽量少。
二、多路转接的实用派
上面介绍的五人中,只有赵六真正实现了减少等待的时间,所以在IO中可以使用多路转接以达到高校IO,这里我们要介绍的就是多路转接中的实用派 —— epoll 。它相比之前说的 select ,改进了不少,是目前很多厂商使用多路转接都会用的方法。被公认为 Linux2.6 下性能最好的多路IO就绪通知方法。
2.1 epoll 接口
这里先简单认识一下 epoll 的接口, 2.2 会深入将有关 epoll 的底层逻辑,可以直接跳转到 2.2 来了解 epoll 的底层,届时会有图解,配合图解来理解 epooll 接口。
2.1.1 epoll_create
int epoll_create(int size);创建一个 epoll 的句柄
• 自从 linux2.6.8 之后, size 参数是被忽略的.
• 用完之后, 必须调用 close()关闭.
2.1.2 epoll_ctl
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);epoll 的事件注册函数
• 它不同于 select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型
• 第一个参数是 epoll_create()的返回值(epoll 的句柄)
• 第二个参数表示动作, 用三个宏来表示
• 第三个参数是需要监听的 fd
• 第四个参数是告诉内核需要监听什么事
struct epoll_event 结构如下,这里简单认识一下,后面会具体来讲:
events 可以是以下几个宏的集合:
• EPOLLIN : 表示对应的文件描述符可以读 (包括对端 SOCKET 正常关闭);
• EPOLLOUT : 表示对应的文件描述符可以写;
• EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
• EPOLLERR : 表示对应的文件描述符发生错误;
• EPOLLHUP : 表示对应的文件描述符被挂断;
• EPOLLET : 将 EPOLL 设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的(这两种触发后面会讲);
• EPOLLONESHOT: 只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个 socket 的话,需要再次把这个 socket 加入到 EPOLL 队列里。
2.1.3 epoll_wait
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);收集在 epoll 监控的事件中已经发送的事件
• 参数 events 是分配好的 epoll_event 结构体数组。
• epoll 将会把发生的事件赋值到 events 数组中 (events 不可以是空指针,内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存)。
• maxevents 告知内核这个 events 有多大, 这个 maxevents 的值不能大于创建
epoll_create() 时的 size.
• 参数 timeout 是超时时间 (毫秒, 0 会立即返回, -1 是永久阻塞).
• 如果函数调用成功, 返回对应 I/O 上已准备好的文件描述符数目, 如返回 0 表示已超时, 返回小于 0 表示函数失败.
2.2 epoll 底层原理
2.2.1 epoll_ctl
在创建、使用epoll时,OS会先为我们在内部创建一棵红黑树,这颗红黑树以文件描述符作为key值,每个节点包含的信息大致如下图所示,红黑树的含义是内核要关心哪些 fd 的哪些事件,也就是节点中的前两个信息:
short events 即:
与红黑树相关的系统调用为 epoll_create :
epfd:暂时还不关心。
op:当用户使用 ADD 时,epoll_ctl 使用用户传入的文件描述符与事件创建新节点,并将其插入到红黑树中;当用户使用DEL时, epoll_event 不需要传入参数,仅需要传入 fd ,系统就会将该节点从红黑树中删除。
*event:表示用户需要系统帮助关心文件描述符的事件,如 EPOLLIN、EPOKKOUT 分别表示关心读事件与写事件。
2.2.2 epoll_wait
操作系统内部除了一棵红黑树,还会维护一个双向链表形式的就绪队列:
其中,当红黑树中有 fd 就绪时,就会将其添加到就绪队列中:
这时,就可以使用 epoll_wait ,它可以将就绪队列中的 fd 添加到用户传入的数组中:
 此时,作为应用层,检测有没有事件就绪的时间复杂度为O(1),相比于 select 确实进步了很多。
 同时,因为就绪队列中已就绪的 fd 严格按照数组下标放入数组,所以以后在遍历的时候,也只需要遍历 epoll_wait 的返回值个数,而不需要遍历整个传入的数组,这点相较于 select 也是进步
上面说了操作系统会自主把已就绪的 fd 添加到就绪队列中,这里其实设置了一个回调函数,当有 fd 就绪时,就会调用该回调函数,完成回调方法,所以 epoll 模型就是由这三部分组成(两种数据结构 + 一种函数调用):
2.2.3 epoll_create
Linux 一切皆文件,它将 epoll 模型也归结成了文件,在调用 epoll_create 时,操作系统会在文件描述符表中创建一个文件描述符,它指向底层的 epoll 模型!
这也就是为什么 epoll_create 返回一个文件描述符,而 epoll_ctl 与 epoll_wait 都需要传入一个文件描述符。它们都需要该文件描述符才能找到底层的 epoll 模型。此外, task_struct 虽然创建了 epoll 模型,而 epoll 管理的就是它表中的文件描述符。
三、 epoll 类的编写
3.1 类的框架
对于 epoll_server ,我们还是从 epoll_echo 入手,理解 epoll 模型的调用与使用。同时,在 epoll_server 中,有用到相关的头文件,下面不再重复,可以去 select_server 中找到,链接如下:Linux网络之多路转接——老派的select-CSDN博客
3.1.1 私有成员
epoll 与 select 相似,但它们都是对报文进行多路转接,所以与之前编写的 TCP 协议一样,都是需要端口号与 listen 套接字。除此之外,通过之前对 epoll_create 的讲解, OS 底层创建的 epoll 模型其实也是文件,所以需要设置一个文件描述符标识 epoll 模型,以便于后面使用 epoll 模型时,OS 可以根据该 fd 找到 epoll 模型。最后,在 epoll_wait 中也介绍到, OS 帮助我们将就绪队列中的文件描述符递交给应用层,这里就需要我们定义一个结构体数组,为 epoll_wait 提供输入参数。
#include <sys/epoll.h>
#include <memory>
#include "Log.hpp"
#include "Socket.hpp"
using namespace socket_ns;
class EpollServer
{
    static const int gnum = 64;
public:
private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensock;
    int _epfd;
    struct epoll_event _revs[gnum];
};3.1.2 构造函数
在构造函数中,首先是对端口号的初始化;其次,需要对 listen 套接字进行创建与初始化;然后,需要创建 epoll 模型,并对 epfd 进行初始化;最后,将监听套接字添加到 epoll 实例,并设置关心读事件。
对于最后将 listensock 添加到 epoll 中的解释:监听套接字的主要作用是接受新的客户端连接。当一个新的客户端尝试连接服务器时,监听套接字会变为可读状态(即有新连接到达),可以确保在有新的连接到达时,epoll 会通知程序,触发相应的处理逻辑(如调用 accept 接受新连接)。
除此之外,因为创建 epoll 模型时可能会出错,所以这里将 Socket.hpp 中的错误原因新增了一条
enum
{
    SOCKET_ERROR = 1,
    BIND_ERROR,
    LISTEN_ERROR,
    USAGE_ERROR,
    EPCREATE_ERROR
};
class EpollServer
{
    static const int gnum = 64;
public:
    EpollServer(uint16_t port) : _port(port), _listensock(std::make_unique<TcpSocket>()), _epfd(-1)
    {
        // 1. 创建listensock
        InetAddr addr("0", _port);
        _listensock->BuildListenSocket(addr);
        // 2. 创建epoll模型
        _epfd = ::epoll_create(128);
        if (_epfd < 0)
        {
            LOG(FATAL, "epoll_create error\n");
            exit(EPCREATE_ERROR);
        }
        LOG(DEBUG, "epoll_create success, epfd: %d\n", _epfd);
        // 3. 将监听套接字添加到 epoll 实例
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = _listensock->SockFd(); 
        epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock->SockFd(), &ev);
    }
private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensock;
    int _epfd;
    struct epoll_event _revs[gnum];
};3.1.3 析构函数
在用完 epoll 模型后,必须将指向 epoll 模型的文件描述符关闭,同时将监听套接字也关闭。
    ~EpollServer()
    {
        _listensock->Close();
        if (_epfd >= 0)
            ::close(_epfd);
    }3.2 类的执行 Loop
3.2.1 Loop 框架
上面介绍 epoll_wait 时,已经介绍过,使用 epll_wait 后,我们直接传入结构体数组就可以得到底层中已就绪的套接字,相比于 select 反复的遍历,不断的更新,可以说简化了非常多:
可以看到, epoll 调用一下 epoll_wait 就可以完成 select 中对于标识 fd 数组的类成员 _fd_array 的反复遍历。随后再根据 epoll_wait 的返回值(添加到传参数组的 fd 个数)来确认是否执行成功。
因为 epoll_wait 的返回值比较特殊,它返回的是添加到传参数组的 fd 个数,而且就绪队列会将 fd 严格按照数组下标大小添加到数组中,所以在使用具体的处理函数时,只需要遍历其返回值个数次即可。所以这里向处理函数传入其返回值,便于处理函数遍历数组。
    void Loop()
    {
        int timeout = -1;
        while (true)
        {
            int n = ::epoll_wait(_epfd, _revs, gnum, timeout);
            switch (n)
            {
            case 0:
                LOG(DEBUG, "epoll_wait timeout...\n");
                break;
            case -1:
                LOG(DEBUG, "epoll_wait failed...\n");
                break;
            default:
                LOG(DEBUG, "epoll_wait haved event ready..., n : %d\n", n);
                handlerEvent(n);
                break;
            }
        }
    }
3.2.2 handlerEvent
handlerEvent 主要有三层:
 第一层:遍历类成员中 struct epoll_event _revs[gnum] 的前 n (epoll_wait的返回值) 个元素
 第二层:数组元素中的事件 & 关心事件(EPOLLIN | EPOLLOUT | other...) —> 判断其是否为关心时间就绪的 fd
 第三层:判断 fd 是否为 listensock 。若是,... ;若不是, ...
首先,遍历类成员中 struct epoll_event _revs[gnum] 的前 n (epoll_wait的返回值) 个元素
    void handlerEvent(int num)
    {
        for (int i = 0; i < num; i++)
        {
            
        }
    }其次,取出数组元素中的事件,并 & 关心事件(EPOLLIN | EPOLLOUT | other...)
    void handlerEvent(int num)
    {
        for (int i = 0; i < num; i++)
        {
            //取出元素中的事件状态
            uint32_t revents = _revs[i].events;
            int sockfd = _revs[i].data.fd;
        }
    }    void handlerEvent(int num)
    {
        for (int i = 0; i < num; i++)
        {
            uint32_t revents = _revs[i].events;
            int sockfd = _revs[i].data.fd;
            // & 关心事件
            if (revents & EPOLLIN)
            {
                
            }
        }
    }最后,根据是否为 listen 套接字进行相对应的操作,其实因为之前我们将 listen 套接字设置为了关心读状态,所以这里可能与 echo_server 关心的事件有冲突,当需要执行另外某种操作时,可能不再存在这一步。
当套接字为监听套接字时,就可以对到来的请求进行 accept ,正式完成三次握手;当套接字不是监听套接字时,就可以根据服务端的目的执行其他操作。
    void handlerEvent(int num)
    {
        for (int i = 0; i < num; i++)
        {
            uint32_t revents = _revs[i].events;
            int sockfd = _revs[i].data.fd;
            // & 关心事件
            if (revents & EPOLLIN)
            {
                //监听套接字->执行accept,接收到来的"客户端"
                if (sockfd == _listensock->SockFd())
                {
                    InetAddr clientaddr;
                    int newfd = _listensock->Accepter(&clientaddr); 
                    if (newfd < 0)
                        continue;
                    struct epoll_event ev;
                    ev.events = EPOLLIN;
                    ev.data.fd = newfd;
                    epoll_ctl(_epfd, EPOLL_CTL_ADD, newfd, &ev);
                    LOG(DEBUG, "_listensock ready, accept done, epoll_ctl done, newfd is: %d\n", newfd);
                }
                //其他套接字->执行echo_server
                else
                {
                    char buffer[1024];
                    ssize_t n = ::recv(sockfd, buffer, sizeof(buffer), 0);
                    if (n > 0)
                    {
                        LOG(DEBUG, "normal fd %d ready, recv begin...\n", sockfd);
                        buffer[n] = 0;
                        std::cout << "client say# " << buffer << std::endl;
                        std::string echo_string = "server echo# ";
                        echo_string += buffer;
                        ::send(sockfd, echo_string.c_str(), echo_string.size(), 0);
                    }
                    else if (n == 0)
                    {
                        LOG(DEBUG, "normal fd %d close, me too!\n", sockfd);
                        ::epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr); 
                        ::close(sockfd);
                    }
                    else
                    {
                        ::epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr); 
                        ::close(sockfd);
                    }
                }
            }
        }
    }四、epoll 的优点
- 接口使用方便: 虽然拆分成了三个函数,但是反而使用起来更方便高效。不需要每次循环都设置关注的文件描述符,也做到了输入输出参数分离
- 数据拷贝轻量:只在合适的时候调用 EPOLL_CTL_ADD 将文件描述符结构拷贝到内核中,这个操作并不频繁 (而 select/poll 都是每次循环都要进行拷贝)
- 事件回调机制:避免使用遍历,而是使用回调函数的方式,将就绪的文件描述符结构加入到就绪队列中,epoll_wait 返回直接访问就绪队列就知道哪些文件描述符就绪。这个操作时间复杂度 O(1)。即使文件描述符数目很多,效率也不会受到影响。
- 没有数量限制: 文件描述符数目无上限。
但是, epoll 解决不了数据拷贝的问题,这是 select/poll/epoll 都具有的特点。





![[Meachines] [Easy] Postman redis未授权访问-SSH公钥注入+RSA私钥解密+Webmin-RCE权限提升](https://img-blog.csdnimg.cn/img_convert/2f3e67ab9d315ea98434a574c9fe6cb7.jpeg)





![二十天刷leetcode【hot100】算法- day1[后端golang]](https://i-blog.csdnimg.cn/direct/247599f2ca1040459a629782695463fb.jpeg)









