Linux高级IO——多路转接之epoll

本章代码Gitee地址:EpollServer

文章目录

    • [1. epoll接口](#1. epoll接口)
      • [1.1 epoll_create](#1.1 epoll_create)
      • [1.2 epoll_wait](#1.2 epoll_wait)
      • [1.3 epoll_ctl](#1.3 epoll_ctl)
    • [2. epoll原理](#2. epoll原理)
    • [3. epoll_server](#3. epoll_server)
    • [4. epoll两种工作模式](#4. epoll两种工作模式)

1. epoll接口

1.1 epoll_create

cpp 复制代码
#include <sys/epoll.h>
int epoll_create(int size);

参数int size理论上可以随便写(已废弃)

返回值:

  • 成功返回一个文件描述符
  • 失败返回-1

1.2 epoll_wait

cpp 复制代码
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

参数:

  • int epfdepoll_create的返回值

  • struct epoll_event *events, int maxevents:用户及缓冲区,返回已经就绪的文件描述符和事件

    cpp 复制代码
    typedef union epoll_data {
        void    *ptr;
        int      fd;
        uint32_t u32;
        uint64_t u64;
    } epoll_data_t;
    
    struct epoll_event {
        uint32_t     events;    //位图传递
        epoll_data_t data;      //
    };
  • int timeout:超时时间,单位是毫秒,0为非阻塞,-1为阻塞式

返回值:已经就绪的文件描述符的个数

1.3 epoll_ctl

cpp 复制代码
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

参数:

  • int epfdepoll_create的返回值

  • int op

    cpp 复制代码
    EPOLL_CTL_ADD	//增添
    EPOLL_CTL_MOD	//修改
    EPOLL_CTL_DEL	//删除
  • int fd, struct epoll_event *event:哪个文件描述符上的哪个事件

selectpoll都是用数组维护的,需要用户进行管理

2. epoll原理

网卡是外设,当硬件就绪之后,会以硬件中断的方式来告诉操作系统,将网卡的数据读到网卡驱动上,而操作系统读数据是从文件缓冲区读取数据。

所以为了支持epoll,操作系统支持三种机制:

  1. 内核会维护一颗红黑树,红黑树节点里面包含:

    cpp 复制代码
    struct rb_node
    {
        int fd;	//内核要关系的文件描述符
        uint32_t event;	//要关系的事件	位图形式
        //...
    }
  2. 此外还会维护一个就绪队列,一旦红黑树上有节点就绪,此时就会将该节点链入到队列当中

cpp 复制代码
struct list_node
{
    int fd;	//已就绪的文件描述符
    uint32_t event;	//已就绪的事件
    //...
}
  1. 操作系统的底层网卡,是允许操作系统注册一些回调机制。

    操作系统内部提供一个回调函数,网卡以中断的方式将数据搬到了网卡驱动层,驱动层当中有数据就绪了,那么数据链路层就会自动调用对应的回调函数。

    这个回调函数要做的就是:

    • 向上交付
    • 数据到来解包交到tcp接收队列
    • 查找rb_tree->fd
    • 构建就绪节点,插入就绪队列

以上三套机制,就叫做epoll模型

Linux一切接文件,strcut file指针指向这个epoll模型,然后将struct file对象添加到进程文件描述符表里面,所以epoll的返回值是一个文件描述符。

++epoll优势:++

  1. 检测就绪时间复杂度为O(1),判断队列是否为空

    获取就绪队列时间复杂度O(n)

  2. fdevent没有上限,所以的文件描述符和关系的事件都是由红黑树管理的,这颗红黑树多大,操作系统决定

如何看待这颗红黑树?

selectpoll都需要辅助数组,数组用户维护,而这颗红黑树就相当于之前我们自己维护的数组,只不过在epoll里面是由系统管理

  1. epoll_wait返回值表示有多少事件就绪,将就绪的节点一个一个弹出,依次放入数组,就绪事件是连续的

3. epoll_server

cpp 复制代码
#include<iostream>
#include<memory>
#include<string>
#include"Socket.hpp"
#include"Log.hpp"
#include"Epoller.hpp"
#include"Nocopy.hpp"

uint32_t EVENT_IN = (EPOLLIN);
uint32_t EVENT_OUT = (EPOLLOUT);

class EpollServer : public Nocopy
{
    static const int defaultnum = 64;   //默认一次性最多获取64个事件
public:
    EpollServer(uint16_t port)
    :_port(port),
    _listensock_ptr(new MySocket()),
    _epoller_ptr(new Epoller())
    {}


    void Init()
    {
        //创建套接字
        _listensock_ptr->Socket();
        //绑定套接字
        _listensock_ptr->Bind(_port);
        //监听套接字
        _listensock_ptr->Listen();
        
        log(Info, "create listen socket success: %d", _listensock_ptr->Getfd());
    }

    void Accepter()
    {
        // 获取新链接
        std::string clientip;
        uint16_t clientport;
        int sock = _listensock_ptr->Accept(&clientip, &clientport);
        if (sock > 0)
        {
            // 不能直接读取,获取连接不代表发送了数据
            
            // 让epoll去关心
            _epoller_ptr->EpollerCtl(EPOLL_CTL_ADD, sock, EVENT_IN);
            log(Info, "get a new link, clientip: %s, clientport: %d", clientip.c_str(), clientport);

        }
    }

    void Recver(int fd)
    {
        // 读事件就绪
        char buffer[1024];
        ssize_t n = read(fd, buffer, sizeof(buffer) - 1);   //BUG
        if (n > 0)
        {
            buffer[n] = 0;
            std::cout << "get a message: " << buffer << std::endl;

            //返回
            std::string echo_str = "server echo $";
            echo_str += buffer;
            write(fd, echo_str.c_str(), echo_str.size());
        }
        else if (n == 0)
        {
            log(Info, "client quit, me too, close fd:%d", fd);
            //从epoll当中移除   删除红黑树节点
            _epoller_ptr->EpollerCtl(EPOLL_CTL_DEL, fd, 0);
            close(fd);  //细节  先移除再关闭
        }
        else
        {
            log(Warning, " read error, close fd:%d", fd);
        }
    }

    void Dispatcher(struct epoll_event revs[], int num)
    {
        for(int i = 0; i < num; i++)
        {
            uint32_t events = revs[i].events;
            int fd = revs[i].data.fd;

            if(events & EVENT_IN)
            {
                //读事件就绪
                if(fd == _listensock_ptr->Getfd())
                {
                    Accepter();
                }
                else
                {
                    //其他事件就绪
                    Recver(fd);
                }
            }
            else if(events & EVENT_OUT)
            {
                //写事件就绪

            }
        }
    }

    void Start()
    {
        //listensock套接字添加进epoll当中
        //listensock和它关心的事件    本质上添加到内核epoll模型的rb_tree里面
        _epoller_ptr->EpollerCtl(EPOLL_CTL_ADD, _listensock_ptr->Getfd(), EVENT_IN);    //关心读事件

        struct epoll_event revs[defaultnum];    //存放就绪的事件
        for(; ;)
        {
            //epoll只负责等待
            int n = _epoller_ptr->EpollerWait(revs, defaultnum);
            if(n > 0)
            {
                //有事件就绪
                log(Debug, "event happend, fd is : %d", revs[0].data.fd);
                //提取就绪事件   epoll_wait返回值会返回就绪的事件数量
                //如果数量大于定义的大小, 下次再捞
                Dispatcher(revs, n);  
            }
            else if(n == 0)
            {
                log(Info, "time out...");
            }
            else
            {
                log(Error, "epoll_wait error");
            }
        }
    }

    ~EpollServer()
    {
        _listensock_ptr->Close();
    }
private:
    std::shared_ptr<MySocket> _listensock_ptr;
    std::shared_ptr<Epoller> _epoller_ptr;
    //MySocket _listensock;
    uint16_t _port;
    //Epoller _epoller;
};

4. epoll两种工作模式

++LT模式:++

epoll默认工作模式是LT(Level Triggered水平触发)模式

当事件到来时,如果上层一直不取走,底层会一直通知

selectpoll采用的也是LT模式

++EL模式:++

EL(Edge Triggered边缘触发)模式是当数据变化的时候,才会通知一次

数据从无到有,从少到多

打个比方:

快递员A(LT模式)送快递的时候,如果客户一直不取,他就一直打电话,说你的快递到了,签收一下;

快递员B(ET模式)送快递的时候,只通知一次,然后就放在驿站了;如果之后又有快递到了,则又通知一次;

快递员A在一个小时只能,可能只能通知到几个客户;而快递员B在一个小时之内可以通知多个客户

ET不止通知效率高于LTIO效率也高于LT

由于ET只通知一次,所以就倒逼上层,每次都要把本轮数据全部取走

  • 如何知道本轮数据全部取完?

    比如说,我们有550g的大米,每天要吃100g,前5天正常,到第6天的时候,原本是要吃50g大米的,可是只能吃50g了,这就说明大米没有了

    也就是说当需要读取的目标数据大于实际读取的数据的时候,就表明数据已经全部取走。

    这就需要我们循环读取数据,直到读取出错为止,可是fd是默认是阻塞的,所以在ET模式下,所有的fd要设置成非阻塞Non_block,如果不设置,程序会一直阻塞住

    每次都能取走全部的数据,接收缓冲区就有空间了,这样tcp就能给对方通知更大的窗口,然后对方就可以给我们发送更多的数据
    ET是否一定比LT高效?

LT所有的fd设置成non_block非阻塞,然后循环读取,这就个ET类似了

这里所谓的通知一次和每次通知,本质上其实是向就绪队列添加一次还是每次都添加

相关推荐
龙鸣丿32 分钟前
Linux基础学习笔记
linux·笔记·学习
耶啵奶膘2 小时前
uniapp-是否删除
linux·前端·uni-app
_.Switch3 小时前
高级Python自动化运维:容器安全与网络策略的深度解析
运维·网络·python·安全·自动化·devops
2401_850410833 小时前
文件系统和日志管理
linux·运维·服务器
JokerSZ.3 小时前
【基于LSM的ELF文件安全模块设计】参考
运维·网络·安全
XMYX-04 小时前
使用 SSH 蜜罐提升安全性和记录攻击活动
linux·ssh
芯盾时代4 小时前
数字身份发展趋势前瞻:身份韧性与安全
运维·安全·网络安全·密码学·信息与通信
心灵彼岸-诗和远方5 小时前
DevOps业务价值流:架构设计最佳实践
运维·产品经理·devops
一只哒布刘5 小时前
NFS服务器
运维·服务器
苹果醋35 小时前
Java8->Java19的初步探索
java·运维·spring boot·mysql·nginx