秒懂Linux之多路转接 epoll

fe594ea5bf754ddbb223a54d8fb1e7bc.gif

目录

多路转接poll

多路转接epoll

epoll_create

epoll_ctl

epoll_wait

epoll 的优点

epoll 使用示例: 检测标准输入输出

handlerevent函数

epoll 工作方式

理解 ET 模式和非阻塞文件描述符

全部代码


多路转接poll

在学习epoll之前,简单讲讲poll。poll跟select很相似且改善了select的一些不足之处。

这里开始有一点疑惑,为什么select在参数设置上会是输入输出混合,而在Poll这里就不是了呢?

经过相关的资料查看明白了select在调用之前,你将感兴趣的文件描述符放入对应的集合中(输入),然后在返回的时候它是在原集合中修改当前已就绪的事件(输出)。

而在poll就实现了输入输出分离,输入希望监听fd的事件类型,输出已就绪的事件。

总结:Poll改良了select中集合大小有限制的缺点以及实现输入输出分离,不用每次都手动设置。

poll并不是我们本次学习的重点,下面我们来谈谈epoll~

———————————————————————————————————————————

多路转接epoll

按照 man 手册的说法: 是为处理大批量句柄而作了改进的 poll.就目前所学来看, epoll应该是我目前接触到性能最好的多路转接~

epoll_create

int epoll_create(int size);
创建一个 epoll 的句柄并返回一个文件描述符,按我的理解就类似创建了一处大空间,然后我们在里面放东西。

size可以忽略,调用完之后要记得用close关闭。

epoll_ctl

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); //epoll 的事件注册函数.

用户告诉内核,你要帮助我关心哪些fd上的哪些事件  

epfd就是epoll_create返回的文件描述符

op就是该注册函数要进行的动作:

  • EPOLL_CTL_ADD:注册新的 fd 到 epfd 中;
  • EPOLL_CTL_MOD:修改已经注册的 fd 的监听事件;
  • EPOLL_CTL_DEL:从 epfd 中删除一个 fd;

fd与event就是该动作要执行到目标文件描述符的特定事件中

events 可以是以下几个宏的集合:
  • EPOLLIN : 表示对应的文件描述符可以读 (包括对端 SOCKET 正常关闭);
  • EPOLLOUT : 表示对应的文件描述符可以写;
  • EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
  • EPOLLERR : 表示对应的文件描述符发生错误;
  • EPOLLHUP : 表示对应的文件描述符被挂断;
  • EPOLLET : 将 EPOLL 设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
  • EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个 socket 的话, 需要再次把这个 socket 加入到 EPOLL 队列里.

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 表示函数失败.
struct epoll_event * events 有点特殊,或者说是Linux中的数据结构有点特殊。
我们使用epoll_ctl注册事件的时候可以把整体当成是红黑树:

有需要监视的事件就会被加入到红黑树中。

我们使用epoll_wait返回时,会把已就绪的事件从就绪队列中提取出来

神奇的是红黑树和队列用的都是同样的节点,这归根于Linux数据结构的特性了。我们也可以这么理解,就绪队列其实就是在红黑树上扒拉已就绪的事件~

那么就绪队列又是谁往里面放数据的呢?——已注册事件通过硬件中断提醒OS已就绪,所有添加到 epoll 中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法.最后形成新的就绪节点加入到就绪队列中~

epoll 的优点

  • 没有数量限制,文件描述符无上限
  • 做到了输入输出参数分离
  • 做到轻量拷贝,不像select需要循环遍历加入fd,epoll只需要epoll_ctl操作即可。
  • select需要遍历去寻找已就绪的事件,而epoll是利用回调的机制自动把就绪事件加入到就绪队列中。

epoll 使用示例检测标准输入输出

基本框架

Main.cc

#include "EpollServer.hpp"
#include <memory>

// ./epollserver port
int main(int argc,char *argv[])
{   
    if(argc!=2)
    {
        std::cout<<"输入格式错误"<<std::endl;
    }
    uint16_t port = std::stoi(argv[1]);
    EnableScreen();
    
    std::unique_ptr <EpollServer> svr = std::make_unique<EpollServer>(port);
    svr->Loop();

    return 0;
}

 epollserver

#pragma once
#include <iostream>
#include <string>
#include <memory>
#include <sys/epoll.h>
#include "InetAddr.hpp"
#include "Socket.hpp"
#include "Log.hpp"

using namespace socket_ns;

class EpollServer
{
    const static int gnum = 64;

public:
    EpollServer(uint16_t port = 8888)
        : _port(port),
          _listensocket(std::make_unique<TcpSocket>()),
          _epfd(-1)
    {
        // 1.创建listensocket
        InetAddr addr("0", _port);
        _listensocket->BuildListenSocket(addr);

        // 2.创建epoll
        _epfd = epoll_create(128);
        if (_epfd < 0)
        {
            LOG(FATAL, "epoll_create error\n");
            exit(5);
        }
        LOG(DEBUG, "epoll_create success, epfd: %d\n", _epfd);

        // 3.加入事件:最开始的listensock,其关心事件:读事件
        struct epoll_event ev;
        ev.events = EPOLLIN;
        ev.data.fd = _listensocket->SockFd();
        epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensocket->SockFd(), &ev);
    }

    ~EpollServer()
    {
        _listensocket->Close();
        if (_epfd >= 0)
        {
            close(_epfd);
        }
    }

    void Loop()
    {
        int timeout = 1000;
        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 have event ready...\n");
                break;
            }
        }
    }

private:
    uint16_t _port;
    std::unique_ptr<Socket> _listensocket;
    int _epfd;
    struct epoll_event revs[gnum];//接收就绪事件数组
};

目前为止都很select的模拟过程一样,闲置时就不断等待就绪事件,而就绪事件到来却不处理时就一直提醒~

handlerevent函数

 void handlerEvent(int num)
    {
        for (int i = 0; i < num; i++)
        {
            // 提取就绪事件
            uint32_t revents = _revs[i].events;
            // 提取就绪事件Fd
            int sockfd = _revs[i].data.fd;

            // 判定情况 只针对读事件
            if (revents & EPOLLIN)
            {
                // 第一个要处理的肯定是listentsocket的读事件
                if (sockfd == _listensocket->SockFd())
                {
                    InetAddr clientaddr;
                    int newfd = _listensocket->Accepter(&clientaddr);
                    if (newfd < 0)
                        continue;
                    // 获取新连接成功 那么我们要立即对newfd进行读写吗?
                    // 不行,无法确定newfd事件是否就绪,加入epoll监视
                    struct epoll_event ev;
                    ev.events = EPOLLIN;
                    ev.data.fd = newfd;
                    epoll_ctl(_epfd, EPOLL_CTL_ADD, newfd, &ev);
                    LOG(DEBUG, "normal fd %d ready, recv begin...\n", sockfd);
                }
                // 正常读事件
                else
                {
                    char buffer[1024];
                    // 这里肯定不会被阻塞,但只能recv一次
                    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);
                        // 这里有两个小细节:
                        // 细节一:读完关闭fd时也要记得及时去除fd在epoll上的标记
                        // 细节二:在去除标记时该fd必须是处于合法的状态(在close之前)
                        epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr);
                        close(sockfd);
                    }
                    else
                    {
                        epoll_ctl(_epfd, EPOLL_CTL_DEL, sockfd, nullptr); // 要删除的fd,必须是合法的!
                        close(sockfd);
                    }
                }
            }
        }
    }

目前我们遇到一个很大的问题:

如何保证recv能读取到完整的数据,从应用层来看可以去制定协议来划分报文边界以保证能读取到完整的报文。
但更大的问题是每次recv都只读一部分,然后事件并不会从就绪队列消失,而是作为未处理再次送达,可是重新回到这里的时候buffer之前的数据早就没了。我个人想到的一种方法就是根据读取到的n大小进行判定,只要>0都作为没读完,直到让n=0为止,通过这个循环让recv慢慢积累读完完整数据。

此话题结束,为的就是引出epoll的工作模式

epoll 工作方式

你正在吃鸡 , 眼看进入了决赛圈 , 你妈饭做好了 , 喊你吃饭的时候有两种方式 :
1. 如果你妈喊你一次 , 你没动 , 那么你妈会继续喊你第二次 , 第三次 ...( 亲妈 ,水平触发)
2. 如果你妈喊你一次 , 你没动 , 你妈就不管你了 ( 后妈 , 边缘触发 )
epoll 2 种工作方式-水平触发(LT)和边缘触发(ET)

epoll默认是水平触发模式,因此在前面的例子中会有读取数据不完整的风险,如果你只处理了一部分那么它只会不厌其烦地慢慢催你全部读完。

而边缘触发模式其实是一种倒逼你一次性全部读完的模式,如果你只读取了一部分那么wait就再也不会返回了 也就是说 , ET 模式下 , 文件描述符上的事件就绪后 , 只有一次处理机会,采用ET模式时你就必须要考虑一次性读取完整数据。

这里可以从wait返回次数来对比出ET性能更高效一些,但这还不是核心。

理解 ET 模式和非阻塞文件描述符

假设这样的场景 : 服务器接收到一个 10k 的请求 , 会向客户端返回一个应答数据 . 如果客户端收不到应答, 不会发送第二个 10k 请求 .

如果服务端写的代码是阻塞式的 read, 并且一次只 read 1k 数据的话 (read 不能保证一
次就把所有的数据都读出来 , 参考 man 手册的说明 , 可能被信号打断 ), 剩下的 9k 数据
就会待在缓冲区中 .

此时由于 epoll ET 模式 , 并不会认为文件描述符读就绪 . epoll_wait就不会再次返回. 剩下的 9k 数据会一直在缓冲区中 . 直到下一次客户端再给服务器写数据 .epoll_wait 才能返回
但是问题来了

所以ET模式性能真正高效的地方其实就是倒逼程序员必须要关注读取问题,确保一次性读完,而这里读完可以采用上面提出的循环读取的方案,但按照ET的特性,即使你缓冲区还有数据可是它再次去调用wait已经不会返回结果了,需要读取的就绪事件没法获取到,recv就这样阻塞了。

我们前面也提到了,epollfd往往默认都是阻塞的,所以我们需要把其变为非阻塞,这样就可以解决上述的全部问题了。

至于解决方案我们在下篇博客为大家进行解析~

全部代码

epoll多路转接 · bec4b5d · 玛丽亚后/keep - Gitee.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值