Linux学习记录——사십삼 高级IO(4)--- Epoll型服务器(1)

这篇具有很好参考价值的文章主要介绍了Linux学习记录——사십삼 高级IO(4)--- Epoll型服务器(1)。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。


1、理解Epoll和对应接口

poll依然需要OS去遍历所有fd。一个进程去多个特定的文件中等待,只要有一个就绪,就使用select/poll系统调用,让操作系统把所有文件遍历一遍,哪些就绪就加上哪些fd,再返回。一旦文件太多了,遍历效率就显而易见地低。epoll是为处理大批量句柄而作了改进的poll,句柄就是访问某种资源时标识这个资源的东西,比如C语言中的FILE结构体,文件描述符等。不过select/poll并不是没有用处,一些老型操作系统并不支持epoll,就得使用poll或者select。epoll是在Linux内核2.5.44时引入的,到现在为止都是Linux中最高效的多路转接IO方案。

epoll有3个接口。

Linux学习记录——사십삼 高级IO(4)--- Epoll型服务器(1),Linux学习,服务器,linux,学习

size是一个被忽略的参数,只要大于0就行。如果成功,返回一个epoll文件描述符,在系统内部创建一些数据结构,帮助进行已就绪的fd的管理,暂且叫做epoll模型,失败返回-1。不用这个epoll文件描述符后要close(epollfd)。

创建后,用户要告诉内核,应当关心哪个文件描述符上的哪个事件是否就绪,select通过一个位图结构fd_set来实现,poll通过poll_fd来实现的。另外,内核要告诉用户,关心的哪些fd上的哪些事件event已经就绪了。epoll还有两个接口去做这两个事。

Linux学习记录——사십삼 高级IO(4)--- Epoll型服务器(1),Linux学习,服务器,linux,学习

epfd就是创建函数的返回值;op表示想做什么,有3个值,EPOLL_ADD,EPOLL_MOD,EPOLL_DEL,分别是添加、修改、删除;fd表示哪一个fd,event表示这个fd上的哪个事件要被关心。

Linux学习记录——사십삼 高级IO(4)--- Epoll型服务器(1),Linux学习,服务器,linux,学习

进行等待的接口。返回值和select,poll接口一样,就绪的fd数量;timeout的作用和poll一样,输入型参数,单位是毫秒ms,为0表示非阻塞,小于0表示阻塞,大于0poll在这段时间内阻塞等待,如果一直没有事件就绪,那么超过时间就返回0;中间两个参数是输出型参数,操作系统通过这两个告知用户就绪的fd上就绪的事件event。

Linux学习记录——사십삼 高级IO(4)--- Epoll型服务器(1),Linux学习,服务器,linux,学习

events是一个32位整数,用户输入的是关心的事件,返回时操作系统通过这个整数来告诉用户哪些fd的events事件就绪了;data的类型是一个联合体,通常会使用prt或者fd。events有几种取值:

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

上面的就是宏。这里只关心EPOLLIN和EPOLLOUT。

TCP报头中6个标记位中有一个代表PSH,用来提示对方应用层立刻从接收缓冲区读取数据。但PSH并不一定能让应用层读取数据,它的催促是让套接字观察的fd对应的文件里的数据处于就绪状态。

操作系统可以把数据从应用层拷贝到缓冲区,然后将数据交给网卡。当网卡收到数据后,网卡会发送硬件中断,操作系统通过查看中断向量表,知道发来的中断号是网卡的,所以就知道网卡有了数据。select/poll都是在软件层面去检测是否有数据的。

CPU有对应的寄存器,寄存器是二进制序列,是一种存储单元,由硬件电路构成。数据拷贝到CPU的硬件本质是利用高低电频对CPU内的寄存器进行充放电,让CPU的寄存器变成和内存一样的值。CPU和所有外设之间都有针脚间接相连。发送中断就像是某个外设产生电流,从和它间接相连的针脚向寄存器充电,把数据放到寄存器中。之后网卡就可以发送中断号让CPU拷贝数据到内存了。所以数据是可以从外设拷贝到内存的。

用户层往下是系统调用层,再往下是操作系统,再往下就是传输层及以下了。当用户层创建epoll时,OS会维护一个红黑树,开始时只有一个根节点,并且epoll还会创建一个就绪队列,为空。红黑树的节点是结构体,里面有fd,有事件event,整个红黑树就是用户告诉OS,要关心哪些fd,以及fd上的哪些事件。所以可以看出epoll_ctl本质是对这个红黑树进行增删改,比如要删,就传对应的fd,事件设为nullptr/NULL,那就是对红黑树某个节点的删除。fd决定节点是红还是黑,左节点还是右节点,插入到哪里。内核中,一个数据结构对象,既可以属于红黑树,也可以属于另一个结构。

红黑树上只有某个fd上有对应的事件发生了,那么就把这个fd的节点接入到就绪队列中,队列只保存已经准备好的fd && 对应的event。队列每一个元素也可以是一个结构体,只取红黑树中已就绪节点里面的值来填充。epoll_wait接口中间两个参数就是从就绪队列中拿取节点,这个接口只看就绪队列,可以以时间复杂度为O(1)的方式来检测事件就绪,也就是队列是否为空。

节点放入队列实际不是将一个节点内容拷贝到队列节点里,而是红黑树节点也是队列节点,节点就是一个结构体,结构体里可以放入表示已经就绪的事件,放入红黑树相关指针信息,放入队列相关指针信息,建立起队列就是用这个队列相关的指针去指向下一个节点。

当数据就绪时,操作系统通过网卡,经过网络协议栈,拷贝到每个文件的文件缓冲区中。每个节点都有回调机制,假设每个文件结构体都有一个变量,如果没设置回调,就置为空,每次操作系统拷贝数据到缓冲区后就去判断一下这个变量,为空就退出,不为空就调用回调函数,回调函数做的工作就是把红黑树上已就绪的节点放到就绪队列中。

红黑树,就绪队列,回调机制这三个整体就是epoll模型,所以epoll_create使用时就是创建了这些,从操作系统内部到系统调用形成了一个体系。红黑树就像select/poll中的数组,但epoll这里核心的维护交由系统来做,不让用户去做。

为什么epoll_create要返回就绪fd的个数,以及另外两个接口还需要用这个数字?整个机制是由系统做的,接口是由进程调用的,进程在运行时,会创建task_struct指向文件描述符表files_struct,表里有一个数组,类型是struct file,012默认被占用,当创建epoll模型,操作系统也创建了一个struct file,里面有个指针指向epoll模型,这个struct file就在调用epoll接口的进程的文件描述符表中。用户,进程,task_struct,files_struct,struct file,这是一整个路线。通过epoll_create的返回值,也就是另外两个接口的参数epfd,两个接口就可以找到进程维护的文件描述符表,进而找到struct file,然后找到epoll模型,就可以对红黑树,就绪队列进行操作了。

epoll的红黑树比数组更有效率;也不需要底层在线性遍历所有节点;上层也不需要遍历节点只需要查看就绪队列;用户只需要调用接口就可以操作整个体系。

2、简单实现

Main.cc

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

int main()
{
    std::unique_ptr<EpollServer> svr(new EpollServer());
    svr->InitServer();
    svr->Start();
    return 0;
}

Makefile

epollserver:Main.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f epollserver

EpollServer.hpp中先写基础的

#pragma once

#include <iostream>
#include <string>
#include "Sock.hpp"
#include "log.hpp"

const static int gport = 8888;

class EpollServer
{
public:
    EpollServer(uint16_t port = gport) : port_(port)
    {}

    void InitServer()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
    }

    void Start()
    {
        while(true)
        {
            sleep(3);
        }
    }

    ~EpollServer()
    {}
private:
    uint16_t port_;
    Sock listensock_;
};

现在还不能Accept,因为还不知道底层是否有文件就绪,如果没有,整个服务器就得阻塞了。epoll这里的思路就是把自己的权利交给epoll。要将listensock添加到epoll中,不过得先有epoll模型。

创建一个Epoll.hpp

#pragma once

#include <iostream>
#include <string>
#include <sys/epoll.h>

static const  int defaultepfd = -1;

class Epoller
{
public:
    Epoller():epfd_(defaultepfd)
    {}

    ~Epoller()
    {}
private:
    int epfd_;
};

完善一下Epoll模型,并初始化和析构

Epoll.hpp

#pragma once

#include <iostream>
#include <string>
#include <cstdlib>
#include <sys/epoll.h>
#include "err.hpp"
#include "log.hpp"

static const  int defaultepfd = -1;
static const int gsize = 128;

class Epoller
{
public:
    Epoller():epfd_(defaultepfd)
    {}

    void Create()
    {
        epfd_ = epoll_create(gsize);
        if(epfd_ < 0)
        {
            logMessage(Fatal, "epoll_create error, code: %d, errstring: %s", errno, strerror(errno));
            exit(EPOLL_CREAT_ERR);//err.hpp里加上这个错误
        }
    }

    int Fd()
    {
        return epfd_;
    }

    void Close()
    {
        if(epfd_ != defaultepfd) close(epfd_);
    }

    ~Epoller()
    {}
private:
    int epfd_;
};

EpollServer.hpp

#pragma once

#include "Epoll.hpp"
#include "Sock.hpp"
#include "log.hpp"

const static int gport = 8888;

class EpollServer
{
public:
    EpollServer(uint16_t port = gport) : port_(port)
    {}

    void InitServer()
    {
        listensock_.Socket();
        listensock_.Bind(port_);
        listensock_.Listen();
        epoller_.Create();
        logMessage(Debug, "init server success");
    }

    void Start()
    {
        //1、将listensock添加到epoll中,要先有epoll模型
        while(true)
        {
            sleep(3);
        }
    }

    ~EpollServer()
    {
        listensock_.Close();
        epoller_.Close();
    }
private:
    uint16_t port_;
    Sock listensock_;
    Epoller epoller_;
};

接下来关注事件。

Epoll.hpp

    //用户告诉内核要关心哪些事件
    bool AddEvent(int fd, uint32_t events)
    {
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = fd;//fd就是就绪的文件描述符
        int n = epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ev);
        if(n < 0)
        {
            logMessage(Fatal, "epoll_ctl error, code: %d, errstring: %s", errno, strerror(errno));
            return false;
        }
        return true;
    }

EpollServer.hpp

    void Start()
    {
        //1、将listensock添加到epoll中,要先有epoll模型
        bool r = epoller_.AddEvent(listensock_.Fd(), EPOLLIN);//只关心读事件
        assert(r);//可以做别的判断
        (void)r;
        while(true)
        {
            ;
        }
    }

然后就可以在循环中获取事件了,使用wait。从队列里拿数据这个过程是线性拷贝的,因为系统不相信用户,所以要定义一个struct epoll_event类型的数组来接收。以及wait接口中的events参数里,由于拷贝的缘故,数据是从左到右连续有效的,而返回值 - 1就是当前最后一个有效的下标。

EpollServer.hpp

    void Start()
    {
        //1、将listensock添加到epoll中,要先有epoll模型
        bool r = epoller_.AddEvent(listensock_.Fd(), EPOLLIN);//只关心读事件
        assert(r);//可以做别的判断
        (void)r;
        struct epoll_event revs_[gnum];
        int timeout = 1000;
        while(true)
        {
            int n = epoller_.Wait(revs_, gnum, timeout);
            switch (n)
            {
            case 0:
                logMessage(Debug, "timeout...");
                break;
            case -1:
                logMessage(Warning, "epoll_wait failed");
                break;
            default:
                logMessage(Debug, "有%d个事件就绪了", n);
                HandlerEvents(n);//一定有数据就绪
                break;
            }
        }
    }

    void HandlerEvents(int num)
    {
        for(int i = 0; i < num; i++)
        {
            int fd = revs_[i].data.fd;
            uint32_t events = revs_[i].events;
            logMessage(Debug, "当前正在处理%d上的%s", fd, (events&EPOLLIN) ? "EPOLLIN" : "OTHER");
            if(events & EPOLLIN)//判断读事件就绪
            {
                if (fd == listensock_.Fd())
                {
                    // 1、新连接到来
                    std::string clientip;
                    uint16_t clientport;
                    int sock = listensock_.Accept(&clientip, &clientport);
                    if (sock < 0)
                        continue;
                    logMessage(Debug, "%s:%d 已经连上服务器了", clientip.c_str(), clientport);
                    // 还不能recv,即使有了连接但也不知道有没有数据
                    // 只有epoll知道具体情况,所以将sock添加到epoll中
                    bool r = epoller_.AddEvent(sock, EPOLLIN);
                    assert(r);
                    (void)r;
                }
                else // 2、读事件
                {
                    char buffer[1024];
                    ssize_t s = recv(fd, buffer, sizeof(buffer) - 1, 0);
                    if (s > 0)
                    {
                        buffer[s - 1] = 0;//对打印格式
                        buffer[s - 2] = 0;//做一下调整
                        std::string echo = buffer;
                        echo += " [epoll server echo]\r\n";
                        std::cout << "client# " << echo << std::endl;
                        send(fd, echo.c_str(), echo.size(), 0);
                    }
                    else
                    {
                        if (s == 0)
                            logMessage(Info, "client quit ...");
                        else
                            logMessage(Warning, "recv error, client quit...");
                        close(fd);
                        //将文件描述符移除
                        //在处理异常的时候,fd必须合法才能被处理
                        epoller_.DelEvent(fd);
                    }
                }
            }
        }
    }

Epoll.hpp

    //用户告诉内核要关心哪些事件
    bool AddEvent(int fd, uint32_t events)
    {
        struct epoll_event ev;
        ev.events = events;
        ev.data.fd = fd;//属于用户的数据,epoll底层不对该数据做任何修改,为了给未来就绪返回
        int n = epoll_ctl(epfd_, EPOLL_CTL_ADD, fd, &ev);
        if(n < 0)
        {
            logMessage(Fatal, "epoll_ctl error, code: %d, errstring: %s", errno, strerror(errno));
            return false;
        }
        return true;
    }
    
    bool DelEvent(int fd)
    {
        return epoll_ctl(epfd_, EPOLL_CTL_DEL, fd, nullptr) == 0;
    }

    int Wait(struct epoll_event* revs, int num, int timeout)
    {

        return epoll_wait(epfd_, revs, num, timeout);
    }

读事件处理中,我们目前无法读到一个完整的报文。因为完整报文由应用层协议规定,我们的代码没有应用层协议,所以得自定义一个。

先用回调函数来处理数据

#include <functional>
using func_t = std::function<std::string (std::string)>;

public:
    EpollServer(func_t func, uint16_t port = gport) : func_(func), port_(port)
    {}
private:
    uint16_t port_;
    Sock listensock_;
    Epoller epoller_;
    struct epoll_event revs_[gnum];
    func_t func_;

读事件处理时

                else // 2、读事件
                {
                    char request[1024];
                    ssize_t s = recv(fd, request, sizeof(request) - 1, 0);
                    if (s > 0)
                    {
                        request[s - 1] = 0;//对打印格式
                        request[s - 2] = 0;//做一下调整
                        std::string response = func_(request);
                        send(fd, response.c_str(), response.size(), 0);
                    }
                    else
                    {
                        if (s == 0)
                            logMessage(Info, "client quit ...");
                        else
                            logMessage(Warning, "recv error, client quit...");
                        close(fd);
                        //将文件描述符移除
                        //在处理异常的时候,fd必须合法才能被处理
                        epoller_.DelEvent(fd);
                    }
                }

在Main.cc中传入函数

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

std::string echoServer(std::string r)
{
    std::string resp = r;
    resp += "[echo]\r\n";
    return resp;
}

int main()
{
    std::unique_ptr<EpollServer> svr(new EpollServer(echoServer));
    svr->InitServer();
    svr->Start();
    return 0;
}

下一篇仍然是Epoll代码。

基本版Epoll

结束。文章来源地址https://www.toymoban.com/news/detail-802823.html

到了这里,关于Linux学习记录——사십삼 高级IO(4)--- Epoll型服务器(1)的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • Linux多路IO复用技术——epoll详解与一对多服务器实现

    本文详细介绍了Linux中epoll模型的优化原理和使用方法,以及如何利用epoll模型实现简易的一对多服务器。通过对epoll模型的优化和相关接口的解释,帮助读者理解epoll模型的工作原理和优缺点,同时附带代码实现和图解说明。

    2024年02月05日
    浏览(43)
  • 多路IO—POll函数,epoll服务器开发流程

    \\\"在计算机网络编程中,多路IO技术是非常常见的一种技术。其中,Poll函数和Epoll函数是最为常用的两种多路IO技术。这两种技术可以帮助服务器端处理多个客户端的并发请求,提高了服务器的性能。本文将介绍Poll和Epoll函数的使用方法,并探讨了在服务器开发中使用这两种技

    2024年02月06日
    浏览(42)
  • IO模型之epoll实现服务器客户端收发

     epoll.ser epoll.cri result      

    2024年02月13日
    浏览(56)
  • 【TCP服务器的演变过程】使用IO多路复用器epoll实现TCP服务器

    手把手教你从0开始编写TCP服务器程序,体验开局一块砖,大厦全靠垒。 为了避免篇幅过长使读者感到乏味,对【TCP服务器的开发】进行分阶段实现,一步步进行优化升级。 本节,在上一章节的基础上,将IO多路复用机制select改为更高效的IO多路复用机制epoll,使用epoll管理每

    2024年01月17日
    浏览(70)
  • 多路转接高性能IO服务器|select|poll|epoll|模型详细实现

    那么这里博主先安利一下一些干货满满的专栏啦! Linux专栏 https://blog.csdn.net/yu_cblog/category_11786077.html?spm=1001.2014.3001.5482 操作系统专栏 https://blog.csdn.net/yu_cblog/category_12165502.html?spm=1001.2014.3001.5482 手撕数据结构 https://blog.csdn.net/yu_cblog/category_11490888.html?spm=1001.2014.3001.5482 去仓库获

    2024年02月15日
    浏览(62)
  • 手撕测试tcp服务器效率工具——以epoll和io_uring对比为例

    服务器的性能测试主要包括2部分: 并发量。能容纳多大的连接 效率。在不崩坏的情况下能对报文的处理效率。 本文主要进行效率测试,看看基于epoll模型和io_uring模型的tcp服务器,谁的效率更高。 测试思路 客户端(一个或多个)大量地向服务器发送报文,测试服务器的处理

    2024年01月18日
    浏览(102)
  • 【Linux】高级IO --- 多路转接,select,poll,epoll

    所有通过捷径所获取的快乐,无论是金钱、性还是名望,最终都会给自己带来痛苦 1. 后端服务器最常用的网络IO设计模式其实就是Reactor,也称为反应堆模式,Reactor是单进程,单线程的,但他能够处理多客户端向服务器发起的网络IO请求,正因为他是单执行流,所以他的成本就

    2024年02月09日
    浏览(65)
  • Linux学习记录——사십 高级IO(1)

    其它IO类型的实现在这篇之后的三篇 input,output。调用read或recv接口时,如果对方长时间不向我方接收缓冲区拷贝数据,我们的进程就只能阻塞,这是读取条件不满足。阻塞的时间成本最后会体现在用户上。因此可以说,IO = 等 + 数据拷贝。高效IO则是单位事件内,等的比重越

    2024年01月21日
    浏览(45)
  • Linux网络编程:多路I/O转接服务器(select poll epoll)

    文章目录: 一:select 1.基础API  select函数 思路分析 select优缺点 2.server.c 3.client.c 二:poll 1.基础API  poll函数  poll优缺点 read函数返回值 突破1024 文件描述符限制 2.server.c 3.client.c 三:epoll 1.基础API epoll_create创建   epoll_ctl操作  epoll_wait阻塞 epoll实现多路IO转接思路 epoll优缺点

    2024年02月11日
    浏览(53)
  • 五、Linux C/C++ 对epoll-reactor服务器的百万级高并发实现

    前言:基于epoll的反应堆模式(reactor)的服务器程序,进行百万并发量的连接测试。通过代码优化,以及服务器与客户端的硬件配置优化,达到百万并发。 代码实现 代码实现: 1台服务器:8G运行内存 8核CPU 3台客户端:4G运行内存 4核CPU 这些硬件配置可以通过虚拟机配置。 按照

    2024年02月20日
    浏览(88)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包