高级IO——poll和epoll

3.2poll

​ 由于select使用前要重置fd集,并且存在等待fd有上限等问题,所以使用poll修正select;

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
//返回值和select一样,0表示超时没有fd就绪,大于0表示有n个fd就绪,小于0表示出错;
//timeout表示一次最多等待的时间,单位是微秒;0表示非阻塞,-1表示阻塞,其他就是正常等待;
//fds是pollfd结构体指针,内部有文件描述符,请求事件,返回事件;这个结构将输入和输出事件进行了分类;
//nfds表示pollfd结构体的数量;

struct pollfd {
    int   fd;         /* file descriptor */
    short events;     /* requested events */ //关心事件,读事件就设置POLLIN,写事件设置为POLLOUT
    short revents;    /* returned events */ //也是使用的位图结构表示事件就绪就绪
};

​ poll和select一样,都是进行关心fd的等待和派发就绪事件,只不过优化了重置fd_set和能等待的fd有限的问题;

​ 如下事件都是宏,并且pollfd结构中的event是位图结构(16个比特位),来维护辅助关心fd和派发就绪事件;

本质就是通过输入输出事件分离解决fd_set重置,由用户维护结构体数组的大小,解决fd有限的问题;

在这里插入图片描述
在这里插入图片描述

3.2.1代码实现

#include <iostream>
#include <string>
#include <unistd.h>
#include <poll.h>
#include "Socket.hpp"

class pollserver
{

    public:
    pollserver(const uint16_t port = defaultport) : port_(port)
    {
        for (int i = 0; i < fd_num_max; i++)
        {
            event_fds_[i].fd = defaultsockfd;
            event_fds_[i].events = no_event;
            event_fds_[i].revents = no_event;
        }
    }
    ~pollserver()
    {
        listensock_.Close();
    }

    public:
    bool init()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
        return true;
    }
    void run()
    {
        event_fds_[0].fd = listensock_.Fd();
        event_fds_[0].events = POLLIN;
        int timeout = 5000;
        for (;;)
        {
            int n = poll(event_fds_, fd_num_max, timeout);
            switch (n)
            {
                case 0:
                    std::cout << "time out, timeout: " << timeout << std::endl;
                    break;
                case -1:
                    std::cerr << "poll err" << std::endl;
                    break;
                default:
                    // 表示有事件就绪
                    std::cout << "get a new link..." << std::endl;
                    dispatcher(); // 事件派发
                    break;
            }
        }
    }
    void print()
    {
        for (int i = 0; i < fd_num_max; i++)
        {
            if (event_fds_[i].fd == defaultsockfd)
                continue;
            std::cout << event_fds_[i].fd << " ";
        }
        std::cout << std::endl;
    }
    void accepter()
    {
        // 事件就绪,获取新连接
        std::string clientip;
        uint16_t clientport = 0;
        int sockfd = listensock_.Accept(&clientip, &clientport); // 此时accept就不会阻塞了,因为事件已经就绪了
        if (sockfd < 0)
        {
            return;
        }
        lg(Info, "accept success, clientip: %s, clientport: %d, sockfd: %d", clientip.c_str(), clientport, sockfd);
        int j = 1;
        for (; j < fd_num_max; j++)
        {
            if (event_fds_[j].fd == defaultsockfd)
            {
                event_fds_[j].fd = sockfd;
                event_fds_[j].events = POLLIN;
                event_fds_[j].revents = no_event;
                print();
                break;
            }
        }
        if (j == fd_num_max)
        {
            lg(Warning, "server is full, close current fd: %d", sockfd);
            close(sockfd);
            return;
            // 扩容逻辑
        }
    }
    void recvr(int fd, int i)
    {
        // 其他文件描述符就绪,此处是读文件描述符就绪了
        char buff[4096];
        ssize_t n = read(fd, buff, sizeof(buff) - 1);
        if (n > 0)
        {
            buff[n] = 0;
            std::cout << buff << std::endl;
        }
        else if (n == 0)
        {
            std::cout << "client close fd, me too" << std::endl;
            close(fd);
            event_fds_[i].fd = defaultsockfd;
        }
        else
        {
            std::cerr << "read err" << std::endl;
            close(fd);
            event_fds_[i].fd = defaultsockfd;
        }
    }
    void dispatcher()
    {
        for (int i = 0; i < fd_num_max; i++)
        {
            int fd = event_fds_[i].fd;
            if (fd == defaultsockfd)
                continue;
            if (event_fds_[i].revents & POLLIN)
            {
                if (fd == listensock_.Fd())
                {
                    accepter();
                }
                else
                {
                    recvr(fd, i);
                }
            }
        }
    }

    private:
    Sock listensock_;
    uint16_t port_;
    static const uint16_t defaultport = 8080;
    static const int fd_num_max = 64;
    static const short no_event = 0;
    int defaultsockfd = -1; // 因为0下标也被使用;
    struct pollfd event_fds_[fd_num_max];
};

3.2.2poll优缺点

优点:

​ 解决了每次使用select都要重置fd_set的问题,另外将接口限制了关心fd数量的大小转变为了用户决定(实际上还是受到了硬件的限制);

缺点:

​ 没有解决输入输出型参数的多次拷贝和遍历,效率还是受到了影响;

3.3epoll

​ epoll主要解决poll要遍历大量文件描述符的问题;select和poll只是遍历文件描述符表;

​ 是inux内核2.6之后公认的最高效的多路转接方案,memcache、redis、ngnix等都使用了epoll;

3.3.1epoll接口认识

#include <sys/epoll.h>
//1.创建一个epoll模型
int epoll_create(int size);//参数只要大于0即可;
//epoll_create()  creates  an epoll(7) instance.  Since Linux 2.6.8, the size argument is ignored, but must be greater than zero;

//2.获取就绪事件
int epoll_wait(int epfd, struct epoll_event *events,
               int maxevents, int timeout);
//第一个参数是创建epoll模型的返回值;
//第2-3个参数是将已经返回的就绪的fd和事件返回;
//第四个参数是毫秒和poll一样;
//返回值和poll一样,表示成功就绪的fd个数;
events可以是以下几个宏的集合:
EPOLLIN : 表示对应的文件描述符可以读 (包括对端SOCKET正常关闭); 
EPOLLOUT : 表示对应的文件描述符可以写;
EPOLLPRI : 表示对应的文件描述符有紧急的数据可读 (这里应该表示有带外数据到来);
EPOLLERR : 表示对应的文件描述符发生错误;
EPOLLHUP : 表示对应的文件描述符被挂断;
EPOLLET : 将EPOLL设为边缘触发(Edge Triggered)模式, 这是相对于水平触发(Level Triggered)来说的.
EPOLLONESHOT:只监听一次事件, 当监听完这次事件之后, 如果还需要继续监听这个socket的话, 需要
再次把这个socket加入到EPOLL队列里

typedef union epoll_data {
    void    *ptr;
    int      fd;
    uint32_t u32;
    uint64_t u64;
} epoll_data_t;

struct epoll_event {
    uint32_t     events;    /* Epoll events */
    epoll_data_t data;      /* User data variable */
};

//3.将fd设置进epoll内
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//op选项如下
EPOLL_CTL_ADD
EPOLL_CTL_MOD
EPOLL_CTL_DEL,最后一个参数置为nullptr
//第3-4个参数是文件描述符和对应的事件;

3.3.2epoll模型原理

​ 为了解决通过遍历大量的文件描述符来检测事件是否就绪的问题,操作系统内部维护了一个红黑树和就绪队列;

struct rb_node
{
    int fd;
    uint32_t event;//要关心的事件
    //还包括大量的连接字段;
}
struct queue_node
{
    int fd;
    uint32_t event;//就绪的事件
}

​ 当事件就绪了,操作系统会是使用硬件中断的方式将网卡的数据拷贝至网卡驱动层,然后网卡驱动层会自动调用回调函数,将数据进行向上交付并且交付到tcp的接收队列,然后查找红黑树,查找fd和对应关心的事件,之后就构建就绪队列节点,添加到队列里;

​ 创建epoll模型就是,创建一个红黑树,就绪队列,还有注册底层的回调机制;操作系统为了管理epoll模型,创建了专门的struct结构来进行维护;

​ epoll_create就是创建文件对象和epoll模型并将epoll模型添加到文件对象内,就可以通过fd查找到epoll模型;

​ epoll_ctl就是增加,删除和修改红黑树的节点,而查找是由底层的回调机制来实现;

​ epoll_wait就是将就绪队列的就绪数据全部放到用户缓冲区的结构体数组内;

​ 其实回调机制是可以和每一个节点对应,就不需要查找更高效;

3.3.3epoll优势

​ 1.检测就绪是O(1);当队列为空时,说明不就绪,而select和poll需要遍历一遍;而获取就绪的时间复杂度是O(n);

​ 2.fd和event没有上限;

​ 3.epoll不需要用户自己维护数组,底层操作系统底层维护了一个红黑树;

​ 4.返回值表示有n个fd就绪,因为是将就绪队列的节点一定是就绪的fd和事件,所以就绪队列的节点拷贝到结构体数组,数组中一定是连续的有效值,不需要像select和poll需要遍历查找过滤掉无效部分;

​ 5.对于就绪队列的数据,如果上层满了就下一批再取;

​ 6.一个文件描述符所关心的事件是否就绪,本质上就是是否添加到了就绪队列;当底层有数据但是上层可能太忙了没时间去取,就需要通知对方尽快将数据取走;即当事件就绪了,就将文件描述符及就绪事件链入到就绪队列,如果接收缓冲区满了,使得对方不能发送了,这时候对方也会发送一个PSH,让上层赶紧拿走接收缓冲区的数据,在底层强制调用回调机制,将就绪事件和对应的fd链入到就绪队列;PSH标志位的本质就是:读时间就绪;PSH仅仅是说明数据已经准备好了;

3.3.4代码实现

​ 1.注意设置fd及关心事件时用一个epoll_event结构,就绪事件处理是使用的epoll_event数组;

​ 2.在设置时将epoll_event.data.fd = sock,是为了将用户级数据返回;

​ 3.当写端关闭了文件描述符之后,读端也应该关闭,要注意要保证文件描述符是合法的,所以要在删除关注文件描述符后再进行关闭文件描述符;

1.epoll封装
class nocopy
{
public:
    nocopy();
    nocopy(const nocopy &np) = delete;
    nocopy &operator=(const nocopy &np) = delete;
};

#include <sys/epoll.h>
#include <cstring>
#include "nocopy.hpp"
#include "Log.hpp"

class epoll : public nocopy
{
public:
    epoll()
    {
        epfd_ = epoll_create(128);
        if (epfd_ < 0)
        {
            lg(Fatal, "epoll_create error, errno: %d, errstr: %s", errno, strerror(errno));
        }
        lg(Info, "epoll_create success, epfd: %d", epfd_);
    }
    int Wait(epoll_event event[], int size)
    {
        int n = epoll_wait(epfd_, event, size, timeout_);
        return n;
    }
    int Update(int oper, int sockfd, uint32_t event)
    {
        int n = 0;
        if (oper == EPOLL_CTL_DEL)
        {
            n = epoll_ctl(epfd_, oper, sockfd, nullptr);
            if (n < 0)
            {
                lg(Error, "epoll_ctl_del error, errno: %d, errstr: %s", errno, strerror(errno));
            }
        }
        else
        {
            epoll_event ev;
            ev.events = event;
            ev.data.fd = sockfd;

            n = epoll_ctl(epfd_, oper, sockfd, &ev);
            if (n < 0)
            {
                lg(Error, "epoll_ctl error, errno: %d, errstr: %s", errno, strerror(errno));
            }
        }
        return n;
    }
    ~epoll()
    {
        if (epfd_ > 0)
        {
            close(epfd_);
        }
    }

private:
    int epfd_;
    static const int timeout_ = 3000;
};
2.epollserver简单封装
#include <iostream>
#include <memory>
#include "Log.hpp"
#include "Socket.hpp"
#include "epoll.hpp"

uint32_t event_in = (EPOLLIN);
uint32_t event_out = (EPOLLOUT);
static const uint16_t defaultport = 8080;

class epollserver : public nocopy
{
public:
    epollserver(uint16_t port = defaultport)
        : listensock_ptr_(new Sock()), epoller_ptr_(new epoll()), port_(port) {}
    void init()
    {
        listensock_ptr_->Socket();
        listensock_ptr_->Bind(port_);
        listensock_ptr_->Listen();
        lg(Info, "create listen socket success, listensockfd: %d", listensock_ptr_->Fd());
    }
    void run()
    {
        epoller_ptr_->Update(EPOLL_CTL_ADD, listensock_ptr_->Fd(), event_in);
        epoll_event event[numsize];
        for (;;)
        {
            int n = epoller_ptr_->Wait(event, numsize);
            if (n > 0)
            {
                // 事件就绪
                lg(Debug, "event happend, fd is: %d", event[0].data.fd);
                dispatcher(event, n); // 事件派发器
            }
            else if (n == 0)
            {
                // 没有事件就绪,超时
                lg(Info, "time out...");
            }
            else
            {
                lg(Error, "Wait errror, errno: %d, errstr: %s", errno, strerror(errno));
            }
        }
    }

    void Accept()
    {
        // 监听套接字就执行监听逻辑
        std::string clientip;
        uint16_t clientport;
        int sockfd = listensock_ptr_->Accept(&clientip, &clientport);
        if (sockfd > 0)
        {
            // 将新的文件描述符添加到epoll中
            epoller_ptr_->Update(EPOLL_CTL_ADD, sockfd, event_in);
            lg(Info, "get a new link, client info: %s, %d", clientip.c_str(), clientport);
        }
        else
        {
            return;
        }
    }

    void Recvr(int fd)
    {
        // 其他文件描述符就绪,此处是读文件描述符就绪了
        char buff[4096];
        ssize_t n = read(fd, buff, sizeof(buff) - 1); // 存在粘包问题
        if (n > 0)
        {
            buff[n] = 0;
            std::cout << buff << std::endl;

            // 构建发送给客户端的数据
            std::string echo_string = "server echo# ";
            echo_string += buff;
            write(fd, echo_string.c_str(), echo_string.size());
        }
        else if (n == 0)
        {
            // 对方将连接关闭了
            std::cout << "client close fd, me too" << std::endl;
            epoller_ptr_->Update(EPOLL_CTL_DEL, fd, 0);
            close(fd);
        }
        else
        {
            std::cerr << "read err" << std::endl;
            epoller_ptr_->Update(EPOLL_CTL_DEL, fd, 0);
            close(fd);
        }
    }

    void dispatcher(epoll_event events[], int size)
    {
        for (int i = 0; i < size; i++)
        {
            uint32_t event = events[i].events;
            int fd = events[i].data.fd;
            if (event & event_in)
            {
                // 读事件就绪了
                if (fd == listensock_ptr_->Fd())
                {
                    Accept(); // 连接管理器
                }
                else
                {
                    // 其他文件描述符就执行读取逻辑
                    Recvr(fd); // 事件处理器
                }
            }
            else if (event & event_out)
            {
                // 写事件就绪了
                // 执行写逻辑
            }
            else
            {
            }
        }
    }
    ~epollserver()
    {
        listensock_ptr_->Close();
    }

private:
    static const int numsize = 64;
    std::shared_ptr<Sock> listensock_ptr_;
    std::shared_ptr<epoll> epoller_ptr_;
    uint16_t port_;
};

3.3.5epoll两种工作模式LT和ET

LT,水平触发(Level Triggered),类似示波器中的波一直为高时才为真;

ET,边缘触发(Edge Triggered),类似示波器中的波由低变高才为真;

​ epoll默认的工作模式为LT模式,即当事件触发时,会一直发送消息,即一直有效,关注的是存在;而ET则是从无到有,从少到多,即变化的时候才会通知一次,关注的是变化;使用ET的时候要注意要将数据一次都处理完,否则就会造成数据的丢失,所以需要循环读取直至读取出错即阻塞,但是不可以阻塞防止服务器挂起,所以需要ET模式的fd都是非阻塞的

​ ET比LT的通知效率要高,因为如果使用LT就会导致一直通知一个fd,其中有很多次的通知是无效的,使得单位时间内的有效通知减少,所以应该使用ET,只是通知了一次,只要保证处理数据时不丢包就行;

​ ET的IO效率更高,因为ET使得数据全被读取,对于TCP会使得发送端的滑动窗口更大,可以发送更多的数据;

​ 其实LT也是可以设置第一次通知就将数据全部拿走,之后就不会通知了,所以效率不一定是ET高,具体看用户代码实现;

总结:LT和ET的区别就是事件就绪,向就绪队列添加多次还是一次;使用ET必须使用非阻塞文件描述符

  • 31
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值