【Linux高级IO(二)】初识epoll

发布于:2025-04-09 ⋅ 阅读:(40) ⋅ 点赞:(0)

目录

1、epoll的接口

2、epoll原理

3、epoll工作方式


1、epoll的接口

#include <sys/epoll.h>

1、int  epoll_create(int  size) :创建epoll模型

返回值是一个文件描述符,创建一个struct file结构体,指向epoll模型,返回的是在进程文件描述符表中的一个fd,通过这个fd就能找到epoll模型。

2、int epoll_wait (int epfd,  struct epoll_event  *events,  int  maxevents,  int  timeout)

监听文件描述符上的事件并返回就绪的事件列表

是在关联就绪队列

作用:事件等待:epoll_wait可以让程序在不使用忙等待的情况下,高效地等待多个文件描述符上的 I/O 事件。通过设置合适的timeout参数,程序可以根据需求进行阻塞等待或非阻塞等待。事件收集:一旦有文件描述符就绪,epoll_wait会将这些就绪事件的详细信息填充到events数组中。events数组的每个元素是一个struct epoll_event结构体,包含了事件的类型(如EPOLLIN表示可读,EPOLLOUT表示可写)以及与之关联的数据(通常是文件描述符)。函数返回值表示就绪事件的数量,通过遍历events数组,程序可以获取每个就绪文件描述符的具体信息,并进行相应的处理  。高效处理大量并发连接:在处理大量并发连接的场景中,如网络服务器,epoll_wait的优势尤为明显。相比于传统的selectpoll函数,epoll使用了事件驱动的机制,采用红黑树和链表的数据结构,使得在处理大量文件描述符时,其时间复杂度为 O (1),而selectpoll的时间复杂度为 O (n)。这意味着epoll_wait可以更高效地处理大量并发连接,减少 CPU 资源的消耗。

第一个参数:epoll_create 的返回值

第二个:它是一个指向 struct epoll_event 结构体数组的指针。当 epoll_wait 检测到有事件发生时,会将这些事件的相关信息填充到这个数组中。 每个 struct epoll_event 结构体包含了事件的类型和与之关联的数据(通常是文件描述符)

第三个: events 数组的最大长度,也就是 epoll_wait 最多能返回的事件数量

第四个:

  • 当 timeout 为 -1 时,epoll_wait 会一直阻塞,直到有事件发生才会返回。
  • 当 timeout 为 0 时,epoll_wait 会立即返回,即使没有事件发生。
  • 当 timeout 为一个正整数时,epoll_wait 会在该时间内阻塞等待事件发生,如果在超时时间内没有事件发生,函数将返回 0

返回值:已经就绪的fd的个数

3、int  epoll_ctl(int  epfd,  int  op,  int  fd,  struct  epoll_event  *event)

新增一个文件描述符,及修改其要关心的事件

是在对红黑树增删改

第一个:epoll_create 的返回值

第二个:选项

第三、四个:哪个文件描述符,上的哪些事件

struct epoll_event结构:

 struct epoll_event ev;

 ev.events = event;

 ev.data.fd = sock;

 n = epoll_ctl(_epfd,oper,sock,&ev);

event可以使用几个宏的集合:

2、epoll原理

OS在硬件层面,怎么知道网卡上有数据呢? 硬件中断

1、在内部维护一颗红黑树

struct rb_node

{
        int   fd;//内核要关心的fd

        uint32_t   event;// 要关心的事件

        //链接字段

}

2、就绪队列(双链表)某个节点的事件就绪了,就将他链入就绪对列中。

struct   list_node

{
        int  fd;//已经就绪的fd

        uint32_t   event; //已经就绪的事件

}

3、OS内部提供回调函数  底层网卡有数据就绪了,自动调用回调

1、向上交付   2、将数据交付给tcp的接收缓冲区    3、查找rb_tree, 看fd,并且看有没有关心EPOLLIN或者EPOLLOUT     4、如果有,构建就绪节点,插入到就绪队列中

用户只需要从就绪队列中获取就绪节点即可!!

上面三套就是epoll模型。

Epoller.hpp

#pragma once

#include "nocopy.hpp"
#include <cerrno>
#include <sys/epoll.h>
#include <unistd.h>
#include <string.h>
#include "Log.hpp"

class Epoller : public nocopy //类Epoller继承自nocopy类,类Epoller对象不能被拷贝和复值
{
    static const int size = 128;
public:
    Epoller()
    {
        _epfd = epoll_create(size); //创建epoll模型 返回一个文件描述符
        if(_epfd == -1)
        {
            lg(Error,"epoll_create error: %s", strerror(errno));
        }
        else
        {
            lg(Info,"epoll_create success: %d", _epfd);
        }
    }
    int EpollerWait(struct epoll_event revents[], int num)  //传一个数组和数组大小就ok
    {
        int n = epoll_wait(_epfd,revents, num, _timeout);//等待epoll实例_epfd上的事件发生,将发生的事件存储在revents数组中,
        //最多存储num个事件 ,超时事件为_timeout毫秒  返回事件发生的数量
        //---解释---第二个参数是指向结构体数组的指针 当检测到有事件发生时,就会将事件的信息:fd、事件类型填充到结构体中
        return n;
    }
    int EpollerUpdate(int oper,int sock,uint32_t event) //修改epoll实例上的文件描述符的监听事件
    {
        int n = 0;
        if(oper == EPOLL_CTL_DEL)
        {
            n = epoll_ctl(_epfd, oper, sock, nullptr);//从epoll实例中删除指定文件描述符sock
            if(n != 0)
            {
                lg(Error, "epoll_ctl delete error!");
            }
        
        }
        else
        {
            struct epoll_event ev;
            ev.events = event;
            ev.data.fd = sock;

            n = epoll_ctl(_epfd,oper,sock,&ev);//通过新增调用 向红黑树中新增节点 让节点和底层队列产生关联
            if(n != 0)
            {
                lg(Error, "epoll_ctl error!");
            }
        }
        return 0;
    }
    ~Epoller()
    {
        if(_epfd >= 0)
        close(_epfd);
    }
private:
    int _epfd;
    int _timeout{3000};
};

EpollerServer.hpp

#pragma once

#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
#include <memory>
#include "Socket.hpp"
#include "Epoller.hpp"
#include "Log.hpp"
#include "nocopy.hpp"

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

class EpollServer : public nocopy
{
    static const int num = 64;
public:
    EpollServer(uint16_t port)
    :_port(port),
    _listsocket_ptr(new Sock()), 
    _epller_ptr(new Epoller())
    {}

    void Init()
    {
        _listsocket_ptr->Socket();//创建套接字
        _listsocket_ptr->Bind(_port);//将套接字绑定到指定的端口
        _listsocket_ptr->Listen();//将套接字设置为监听状态

        lg(Info, "create listen socket success: %d\n", _listsocket_ptr->Fd());
    }
    void Accepter() //处理新的客户端连接
    {
       //获取新连接
       std::string clientip;
       uint16_t clientport;
       int sock = _listsocket_ptr->Accept(&clientip,&clientport);
       if(sock > 0)
       {
        //不需要阻塞  因为这些是已经就绪的
        //但是不能立马读  建立连接了并不代表有数据
        //这个文件描述符上到底有没有新数据到来,只有epoll最清楚
        _epller_ptr->EpollerUpdate(EPOLL_CTL_ADD, sock, EVENT_IN);

       }
    }
    void Recver(int fd)
    {
        char buffer[1024];
        ssize_t n = read(fd, buffer, sizeof(buffer) -1);
        if(n > 0)//读取成功
        {
          buffer[n] = 0; //在缓冲区末尾加字符串结束符
          std::cout << "get a message:" << buffer << std::endl;
          //write
          std::string echo_str = "server echo $";
          echo_str += buffer;
          write(fd,echo_str.c_str(), echo_str.size());
        }
        else if(n == 0) //读取长度为0,客户端已经将连接关闭了
        {
          _epller_ptr->EpollerUpdate(EPOLL_CTL_DEL,fd,0);//从epoll中移除时必须保证这是一个合法的文件描述符 所以先移除再关
          close(fd); 
          
        }
        else
        {
          _epller_ptr->EpollerUpdate(EPOLL_CTL_DEL,fd,0);//从epoll中移除时必须保证这是一个合法的文件描述符 所以先移除再关
          close(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) //当前事件是可读事件
        {
          //将监听套接字的文件描述符添加到 epoll 实例中,并关注其可读事件(EPOLLIN)。
          //当有新的客户端连接请求到达时,监听套接字就会变得可读,
          //epoll 会检测到这个事件并将其作为就绪事件返回给服务器
          if(fd == _listsocket_ptr->Fd())//当前文件描述符是监听套接字的描述符,表示有新的客户端连接请求
          {
            //服务器根据文件描述符判断是监听套接字可读是,就知道有新连接到来
            //当有新连接到来,监听套接字就会变的可读,epoll就会检测到监听套接字上的可读事件,并将其作为一个就绪事件返回给服务器
             Accepter(); //处理新的客户端连接
          }
          else //普通客户端套接字有数据可读
          {
            //其他fd上面的普通读取事件就绪
            Recver(fd); //处理客户端发送的数据
          }
        }
      }
    }
    void Start()
    {
        //将listensock添加到epoll中,  就是listensock和他关心的事件,添加到内核epoll模型中的rb_tree(红黑树)中
        _epller_ptr->EpollerUpdate(EPOLL_CTL_ADD, _listsocket_ptr->Fd(), EVENT_IN);
        //要让epoll去等listen套接字 要让套接字增多
        //epoll只负责在IO模型中进行等待

        //---解释---调用Epoller类的EpollerUpdate方法,将监听套接字添加到epoll实例中,并关注可读事件
        struct epoll_event revs[num];//---解释---存储epoll_wait函数返回的就绪事件
        for(;;)
        {
          int n = _epller_ptr->EpollerWait(revs, num); //不断调用epoll_wait函数等待事件发生
          if(n > 0)
          {
            //有事件就绪
            Dispatcher(revs,n); //事件派发
          }
          else if(n == 0)
          {
            lg(Info, "time out ...");
          }
          else
          {
            lg(Error, "epll wait error");
          }

        }
    }
    ~EpollServer()
    {
        _listsocket_ptr->Close();    
        
    }
private:
    std::shared_ptr<Sock> _listsocket_ptr;
    std::shared_ptr<Epoller> _epller_ptr;
    uint16_t _port;
};

3、epoll工作方式

LT:水平触发Level Triggered

ET:边缘触发Edge Triggered : 数据/连接,从无到有,从有到多,变化的时候,才会通知我们一次。

epoll默认模式:LT模式(有事件到来,上层不取走,就一直通知你)

ET的通知效率高。不仅如此,ET的IO效率也更高。

边缘触发会倒逼程序员,在每次通知的时候把数据全取走,循环读取,直到读取出错。所有的fd必须是非阻塞的。当读取不成功就会出错,而不是一直阻塞等待。(面试题)   这意味着tcp会向对方通告一个更大的窗口,从而从概率上让对方一次给我发送更多的数据!!

其实LT也可以将所有的fd设置成为non_block,然后循环读取。在通知第一次的时候,就全部读取,不就和ET一样了。

向就绪队列里添加一次,还是一直添加(这就是ET和LT的区别)

上面的代码存在问题:read如何保证一次读完完整的报文。  没读完的数据在缓冲区中,如果想着下一次再去读,缓冲区已经被清空或者覆盖了。

下一节代码Reactor解决这个问题


网站公告

今日签到

点亮在社区的每一天
去签到