Linux--epoll--0328

epoll是Linux提供的一种高效处理大量文件描述符的机制,通过epoll_create创建句柄,epoll_ctl管理关注的事件,epoll_wait获取就绪事件。epoll的工作原理涉及到红黑树和就绪队列,通过回调函数处理就绪事件,提高了效率。文章还展示了如何对epoll进行封装和在服务器中的使用。
摘要由CSDN通过智能技术生成

1. epoll概念

epoll是为处理大量句柄而做了改进的poll

句柄是一个用来标识对象或者项目的标识符,可以用来描述窗体、文件等

2. epoll的相关系统调用

epoll_create

int epoll_create(int size);

创建一个epoll的句柄。

size大小可以随意传入,是一个被废弃的参数

 epoll_ctl

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

第一个参数是epoll_create()的返回值(epoll的句柄).
第二个参数表示动作,用三个宏来表示.
第三个参数是需要监听的fd.
第四个参数是告诉内核需要监听什么事。

第二个参数的取值:
EPOLL_CTL_ADD :注册新的fd到epfd中;
EPOLL_CTL_MOD :修改已经注册的fd的监听事件;
EPOLL_CTL_DEL :从epfd中删除一个fd;

返回值

成功返回0

struct epoll_event

struct epoll_event
{
    uint32_t events;
    epoll_data_t date;
};

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

 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表示函数失败

细节一:如果底层有很多就绪的sock,revs承装不下,怎么办?

没事,一次拿不完。下次接着拿。

 细节二:关于epoll_wait的返回值问题

返回就绪的个数。有几个fd上的事件就绪,就返回几,epoll返回的时候,会将所有就绪的event按照顺序放入到revs数组中,一共有返回值个。

3. epoll工作原理

先回答两个问题:操作系统怎么知道网卡里面有数据了?操作系统怎么知道键盘有用户输入了呢?

硬件中断!在操作系统中存在一个中断向量表,其中对应着不同硬件中断对应的处理方法。当用户在键盘输入时,电信号会转变为一个具体的数字放在寄存器中,OS就会通过这个数字在中断向量表里查询对应的处理方法,从而将网卡中的数据读到操作系统中。

在epoll_creat时OS首先在内核创建一个epoll模型,该模型有一个红黑树的结构,一些回调方法,和一个就绪队列。 当用户需要epoll去关心某些文件时,OS会在红黑树中添加节点,节点里面至少包括了文件描述符和关心的事件类型等信息。然后OS会自动检测这些事件资源是否就绪,如果就绪,会通过设置的一些回调方法自动处理,并会创建一个新的就绪节点。随后将这个节点挂到就绪队列。

 再谈epoll的三个调用接口

epoll_create 就是构建红黑树、就绪队列、建立回调机制这三个过程。即创建一个epoll模型。

epoll_ctl 就是修改底层红黑树(增删改)

epoll_wait 就是从就绪队列拿就绪的资源。

epoll_create返回值为fd的原因

首先在进程中,会有task_struct结构体,里面有文件描述符表,这些函数的返回值就是建立epoll模型的文件描述符。OS可以通过这个文件描述符找到对应的文件,该文件会中有一个指针,就指向epoll模型。

3.1 epoll的优化

1.红黑树是k,v结构的,key值刚好可以由文件描述符充当,所以用户后续拿走就绪事件时,也会按照一定的顺序。

2.用户只需要设置关系,获取结果即可,不用关心任何对fd和event的管理细节。

3.在底层中使用红黑树代替了数组。并且不再通过遍历一遍遍的看资源是否就绪,而是资源就绪时,OS会采用回调函数直接帮我们处理。

4.底层只要有fd就绪,OS会自动构建节点,然后连入到就绪队列中。上层只要不断的从就绪队列中将数据拿走,就完成了获取就绪事件的任务。发现,内核或者用户都会操作这个epoll模型,也就是说epoll模型是临界资源,本质上就是一个生产者消费者模型。在epoll中已经保证了线程安全。

5.如果就绪队列里没有就绪时间。可以自由的设置阻塞/非阻塞/自定义等待时间。

4.epoll的使用

注:本博文只实现了对于读入的处理方法

4.1 对epoll的封装

#pragma once
#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
class Epoll
{
public:
    static const int gsize=256;
    static int CreateEpoll()
    {
        int epfd=epoll_create(gsize);
        if(epfd>0) return epfd;
        else
        {
            exit(5);
        }
    }
    //对哪个epoll模型 什么操作 哪个文件 事件是什么EPPLLIN?EPOLLOUT?
    static bool CtlEpoll(int epfd,int operator,int sock,uint32_t events)
    {
        struct epoll_event ev;
        ev.events=events;
        ev.data.fd=sock;
        int n=epoll_ctl(epfd,operator,sock,&ev);
        return n==0;
    }
    static int WaitEpoll(int epfd,struct epoll_event revs[],int num,int timeout)
    {
        return epoll_wait(epfd,revs,num,timeout);//返回就绪的个数
    }
};

4.2 调用逻辑

我们把方法暴露给外面 可以通过传入不同的参数指针 对结果进行不同的处理。

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

using namespace std;
using namespace ns_epoll;

void change(std::string request)
{
    //完成业务逻辑
    std::cout << "change : " << request << std::endl;
}

int main()
{
    unique_ptr<EpollServer> epoll_server(new EpollServer(change));
    epoll_server->Start();

    return 0;
}

4.3 EpollServer.hpp

4.3.1 成员介绍及构造和析构

注意当我们listensock的初始化进行完成后,不能直接accept(),因为不知道什么时候底层连接就绪,所以一定要直接把listensock添加到epoll里面,让它帮我们等待。

#ifndef __EPOLL_SERVER_HPP__
#define __EPOLL_SERVER_HPP__

#include <iostream>
#include <string>
#include <functional>

#include "Epoll.hpp"
#include "Sock.hpp"
#include "log.hpp"
#include <cassert>
namespace ns_epoll
{
    static const int default_num=8080;//默认的端口号
    const static int gnum=64;//创建的可以存放就绪事件数组的最大长度
    using std::cout;
    using std::cin;
    using std::string;
    class EpollServer
    {
    public:

        using func_t = std::function<void(std::string)>;
        //将对于数据的使用暴露给外面 传进来什么 就怎么操作
        EpollServer(func_t HandlerRequset,const uint16_t& port=default_num)
            :_port(port)
            ,_HandlerRequset(HandlerRequset)
            ,_revs_num(gnum)
        {
            //0.由于在私有成员中添加了一个获得就绪时间的数组指针
            //需要开辟空间 以方便以后使用
            _revs=new struct epoll_event[_revs_num];

            //1. 创建_listensock
            _listensock=Sock::Socket();
            Sock::Bind(_listensock,_port);
            Sock::Listen(_listensock);
            //2. 创建epoll模型
            _epfd=Epoll::CreateEpoll();
            logMessage(DEBUG, "init success, listensock: %d, epfd: %d", _listensock, _epfd);

            //3.将_listensock加入epoll
            if (!Epoll::CtlEpoll(_epfd, EPOLL_CTL_ADD, _listensock, EPOLLIN)) exit(6);
            logMessage(DEBUG, "add listensock to epoll success.");

        }
        ~EpollServer()
        {
            if(_listensock>=0) close(_listensock);
            if(_epfd>=0) close(_epfd);
            if(_revs) delete[] _revs;
        }
    private:
        int _listensock;
        int _epfd;
        uint16_t _port;
        struct epoll_event *_revs;//用于一次性获取多个就绪事件的数组
        int _revs_num;//获得就绪数组的大小
        func_t _HandlerRequset;//使用的方法是什么
    };
}

#endif

4.3.2 服务器启动和添加文件到epoll逻辑

Start中对于LooOnce的封装其实没什么用,就是把单次循环和死循环拆开了。

当程序刚开始,epoll中只有_listensock是关心的。如果它就绪,说明连接完成。可以调用accept()去获取这个就绪事件了。当然随着程序运行,肯定会获得越来越多的套接字,所以我们设定方法去调用不同的处理方法。

         void Start()
        {
            int timeout = -1;
            //阻塞式等待
            while(true)
            {
                LoopOnce(timeout);
            }
        }

        //循环一次
        void LoopOnce(int timeout)
        {
            int n = Epoll::WaitEpoll(_epfd, _revs, _revs_num, timeout);
            //if(n == _revs_num) //扩容
            switch (n)
            {
            case 0:
                logMessage(DEBUG, "timeout..."); // 3, 4
                break;
            case -1:
                logMessage(WARNING, "epoll wait error: %s", strerror(errno));
                break;
            default:
                // 等待成功
                logMessage(DEBUG, "get a event");
                HandlerEvents(n);
                break;
            }
        }
        void HandlerEvents(int n)
        {
            assert(n>0);
            //走进来就一定有资源就绪了 看看是哪种事件
            //_revs里面存放着所有的就绪事件 我们挨着处理就可以
            for(int i=0;i<n;i++)
            {
                int sock=_revs[i].data.fd;
                uint32_t revents=_revs[i].events;
                //读时间就绪了
                if(revents & EPOLLIN)
                {
                    if(sock==_listensock)
                    {
                        Acceptr(sock);
                    }
                    else
                    {
                        Recver(sock);
                    }
                }
            }
        }

4.3.3 处理方法的实现

注意

这里Recver()按道理来说是需要制定协议的。这里为了简单测试,就当字符串处理,从而尽可能的减少没有协议带来的影响。

void Acceptr(int listensock)
        {
            std::string clientip;
            uint16_t clientport;
            int sock=Sock::Accept(listensock,&clientip, &clientport);
            if(sock<0) 
            {
                logMessage(WARNING, "accept error!");
                return;
            }
            //不知道什么时候才会发送消息,接着让epoll帮我们等
            if( ! Epoll::CtlEpoll(_epfd,EPOLL_CTL_ADD,sock,EPOLLIN) ) return ;
            logMessage(DEBUG, "add new sock : %d to epoll success", sock);
        }
        void Recver(int sock)
        {
            char buffer[1024];
            ssize_t n=recv(sock,buffer,sizeof(buffer)-1,0);
            if(n>0)
            {
                buffer[n]=0;
                _HandlerRequset(buffer);//用外部传入的方法进行处理数据
            }
            else if(n==0)
            {
                // 1. 先在epoll中去掉对sock的关心
                bool res = Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0);
                assert(res);
                (void)res;
                // 2. 再close文件
                close(sock);
                logMessage(NORMAL, "client %d quit, me too...", sock);
            }
            else
            {
                // 1. 先在epoll中去掉对sock的关心
                bool res = Epoll::CtlEpoll(_epfd, EPOLL_CTL_DEL, sock, 0);
                assert(res);
                (void)res;
                // 2. 在close文件
                close(sock);
                logMessage(NORMAL, "client recv %d error, close error sock", sock);

            }

4.3.4 测试结果

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值