Linux相关概念和易错知识点(48)(epoll的底层原理、epoll的工作模式、反应堆模式)

基于Linux的C++轻量级web服务器/webserver/httpserver——epoller详解 Epoller的介绍 epoll是I/O多路复用技术的一种,目前常用的I/O多路复用技术有:epoll、select、poll。 I/O多路复用技术可以让服务器同时监听多个客户端发起的socket连接。epoll相较于select和poll,由于其底层的红黑树,不仅可以知道是否有socket通信状态发生了改变,还能够知道有几个socket发生了状态改变。不需要在程序中使用循环遍历所有的socket连接,确定到底是哪一个socket状态发生了改变。这一特点大大提高了服务器监听的效率,也让epoll成为了目前I 阅读详情

1.epoll的底层原理

(1)网络原理引导(上层向下层注册服务)

当服务端和客户端建立连接之后,服务端通信专用的fd对应的struct sock里面就有发送队列和接收队列。我们将该连接放入select和poll中之后,Linux内核就会遍历我们关心的fd和事件,其中就会看我们关心的通信fd里面的sock,看里面的接收队列、发送队列上是否有数据。

但操作系统遍历所有我们关心的fd和事件效率低下,于是我们需要借助一个底层回调机制。

网络部分,当接收队列sk_buff中有数据之后,该套接字会自动调用某个函数指针对应的方法Callback(),这个默认方法是空的(即函数指针指向NULL)。 所以在默认的情况下,数据就绪时,当套接字发现该Callback()方法为空时,就不进行调用,如果该方法不为空就调用。与此同时,套接字在上层有一个接口SetCallback(func_t f),上层可以通过调用这个接口传入一个函数指针,这个指针会被赋值在Callback()方法上,于是后续每当数据就绪时,套接字就会自动去调用这个Callback(),而这个Callback()是上层通过接口传过去的,这就叫上层向下层注册服务!

我们传过去的Callback()方法就是回调函数,即下层执行我们传过去的上层的函数,这个上层函数是我们自定义的,也就是说只要sk_buff收到了数据,它就会自动调用我们的接口,我们想让它干什么,它就干什么!

因此,只要下层提供回调接口,允许我们传函数指针,那么上层就可以向底层注册我们想要的机制。 比如说我们可以注册这个机制为:OS给目标进程发送SIGIO,同时进程捕获该信号,当系统找到PCB修改位图结构的pending标志位之后,进程切换到内核态执行相应的捕获函数handler(),这个handler()就可以打印一条数据,cout << “有数据就绪了!”,这就是信号驱动式IO的核心依赖,也是epoll模型底层的重要组成部分。

(2)红黑树和就绪队列

当我们创建一个epoll模型时,OS会在内核创建一颗红黑树,这颗红黑树一开始是没有节点的,但可以新增节点,这个节点可理解为pair<fd, struct epoll_event>。 因此,我们用户ctl设置的所有关心的fd和事件都会以fd为key被保存在一颗红黑树中。

除此之外还有个就绪队列ready_queue,这个队列里面的每个节点保存着已就绪的fd和事件。因此一旦有事件就绪,就绪队列就会被链入新节点。

红黑树描述了内核要为用户关心哪些fd上的哪些事件(用户到内核),而就绪队列是内核告诉用户哪些fd上的哪些事件就绪了(内核到用户)。ctl接口的本质是在修改红黑树(用户到内核),wait是在从就绪队列拿出数据(内核到用户)。

但是,OS怎么知道文件有事件就绪呢?红黑树的节点管我就绪队列什么事?红黑树跟就绪队列怎么联系在一起?

(3)基于回调机制的节点迁移

当用户调用ctl函数时,底层会在红黑树中添加对应的结点(用户到内核)。但除此之外,系统底层还会在我们关心的fd对应的struct file中调用一个SetCallback(func_t f)的接口,设置回调函数。默认情况下这个回调函数是NULL,不会执行,但调用ctl之后,上层注册了这个方法。因此,每当这个fd有事件就绪时,该fd就会主动去调用这个Callback(),而这个方法就是:把该节点从红黑树迁移到就绪队列!

所以,如果有数据就绪了,调用Callback(),判断是读事件就绪还是写事件就绪,再把对应fd和对应事件作为就绪节点插入就绪队列。 由于这个函数是回调的,我们只需要负责向红黑树中插入节点,注册回调方法,之后就由回调机制在底层自动完成从红黑树到就绪队列的迁移。

我们把节点的迁移这个回调机制、红黑树、就绪队列这一整套解决方案叫做epoll模型!我们调用create创建epoll模型,本质上就是创建红黑树和就绪队列,维护回调机制。其中最核心的就是利用了回调机制实现类似信号驱动IO的节点迁移。

回调函数就是指被当成参数传递的函数,在这里就是指手动传递的Callback()。相当于留了个电话,后续一方可以拿着这个电话和对方进行联系。

(4)从文件角度看epoll模型

我们需要以struct file的private_data为起点,理解epoll模型的创建过程。

如何理解epoll模型?

如上图,当创建一个epoll模型时其实是创建了一个struct file和struct eventpoll,该file的private_data和我们的struct eventpoll进行关联,因此创建epoll模型返回的其实是一个fd,Linux网络内核和struct file也是这样关联的!Linux下一切皆文件,这也是C风格多态的体现!

在理解epoll模型的构成之后,我们也能理解,ctl和wait都需要fd是因为需要通过fd找到struct file,进而找到struct eventpoll,之后再向红黑树插入 / 修改节点或是从就绪队列取出节点。 所谓wait只会去检测就绪队列里面有没有节点,而不会去主动轮询红黑树里面的fd和事件。

但系统具体是怎么做到把红黑树节点迁移到就绪队列呢?

在图中我们可知红黑树的节点类型是epitem,在内核中,一个节点不是只能属于一个数据结构,利用内核链表的原理,我们添加链接信息就相当于在其它队列激活了该节点。 因此,红黑树的节点也可以是就绪队列的节点,我们可以直接设置指针以激活就绪队列中的节点,使其链接到另一个链表中。

注意,一个节点就绪后不会从红黑树移除,所谓迁移指的就是激活在就绪队列中的节点。能修改红黑树的只有用户的ctl函数。

(5)epoll模型效率分析

epoll为什么高效?

每一个socket里面的sock都可以注册接口,所以用户仅需注册方法 (当sk_buff有数据时,激活在就绪队列上的节点) 即可,OS就能在数据就绪时自己执行回调函数驱动完成节点迁移到就绪队列,OS不再需要遍历了,仅需检测有没有事件就绪,时间复杂度为O(1)。 当有事件就绪的话,就绪队列不为空,这时wait会把就绪的事件和fd作为输出型参数拷贝给用户,这个时间复杂度是O(N),不过这个开销是避免不了的。

为什么用红黑树?

红黑树相当于select里面的辅助数组(包含所有关心的fd和事件,用户到内核),调用ctl就是管理这个辅助数组。而红黑树近似平衡,搜索复杂度相比数组更均衡,很适合。

接口层面的高效如何体现?

如果有事件就绪了,epoll_wait通过输出型参数返回一个数组,这个数组里面节点都是合法的且就绪的,我们直接遍历数组的0 ~ ret - 1即可。红黑树里面没就绪fd和事件都不在就绪队列里面,也就不会出现在输出型参数的数组里。

(6)DEL注意事项

当关闭通信fd时,我们要先调用ctl函数,再close文件,顺序不能调换。因为我们必须要保证节点的fd在移出红黑树前随时都是合法的,调用ctl前系统会去判断我们想要移除的fd是否合法,如果我们先close文件,那这个fd就一定不合法,就会出现泄漏问题。

2.epoll的工作模式

(1)水平触发(LT)

select、poll、epoll默认的工作模式是水平触发(LT)。LT模式下,只要有数据没被取完,隔段时间就一直通知,直到数据被取完,不管这个数据是旧的还是新来的。

因此,一旦收到数据(事件就绪),节点就会一直激活在就绪队列中,只有读完全部数据后该节点才会被链出队列,在此之前该节点一直在就绪队列中处于激活状态,上层一调用wait就会通过就绪队列被通知一次。

(2)边缘触发(ET)

还有个工作模式边缘触发(ET)。当新收到数据时就通知对方一次,并且仅当有新数据到来时才会再通知一次。

因此,一旦数据就绪,OS就会在就绪队列激活节点,上层一旦调用wait发现到了该就绪节点,该节点立马从就绪队列移除,后面再wait就不会找到刚才的这个节点了,除非又有新的数据就绪,否则后面永远不再通知。

设置ET通过EPOLLET按位或在关心事件上即可,通过位图传过去。

(3)LT和ET的效率分析

ET相对更高效,因为首先ET不做重复通知,在相同的时间能通知的事件数量更多,相比而言LT会做很多重复的通知。

其次,ET模式的规则是一旦通知,数据必须取完,后面就不再通知,用户再也无法通过wait读取到该次数据。这是强硬的规定,所以这也是ET模式倒逼上层一次性读取完数据。这种倒逼上层快速读取数据也会进一步增加数据传输效率。 以TCP为例,当就绪时,由于上层必须将数据从接收缓冲区取完,这样的话缓冲区空闲位置就及时增大,ACK给对方通知的将会是一个更大的滑动窗口,对方就可能发送更多数据。这样网络吞吐量就会变大,提高IO效率。

那代价是什么?

ET模式一旦要读取,就必须取完,那问题是我们怎么保证取完了?循环读取!这个时候我们就可以结合非阻塞IO来处理了,直到读取到返回值为-1,根据errno判断为数据为就绪才停止。当然我们也可以根据直到实际读到的数据 < 小于期望数据判断,就像有人找你借钱,你借了一次下次他还要找你,直到你再也掏不出他想要的钱的数量才会停止。在阻塞IO下,我们也可以一直读直到阻塞。

但单进程不敢阻塞,所以ET下都会以非阻塞IO + 错误码那套方法处理。LT模式一般不要求设置非阻塞IO,通知了读一下,通知了读一下,这也是所谓的ET模式约束带来的处理方式不同。

LT + 非阻塞 + 循环读 = ET?那要ET干什么?效率有区别吗?

其实LT + 非阻塞 + 循环读和ET的效率没有本质区别,但是ET是强制要求,而前者是一种自己驱动的,考验开发者的水平和素养的。所以LT是基本使用的,没有强硬的规定,相对而言比较均衡。而ET专门为高吞吐量IO设计的,ET相对而言代码更复杂。两者使用的选择取决于各自运用的场景。

当遇到多连接,且多连接中只有一部分比较活跃时,比较适合使用多路转接(epoll、poll、select都是多路转接,推荐epoll),这种IO模型能够把所有文件描述符聚合在一起,尽可能减少进程线程消耗。这就引入了基于epoll的反应堆模式。

3.反应堆模式

反应堆模式属于并发编程与I/O多路复用领域的架构模式,并不是传统的设计模式,这点我们要清楚。下面我从代码角度上来逐层搭建基于ET模式的反应堆,我们要理解每个模块的作用和耦合方式,而非代码非关键部分的细节。

(1)Epoller的封装

我用单例模式封装epoll模型及其基本操作。相当于后续用户的所有fd和事件的关心操作都是调用该epoller的接口实现的,添加到单例中唯一的epoll模型中。这可以说是反应堆中最底层、最核心的模块。

#pragma once

#include <iostream>
#include <sys/epoll.h>
#include "myLog.hpp"
#include "myCommon.hpp"

// epoll模块,对epoll的封装,提供接口给reactor使用,后续事件的等待、增删都是对该模块函数的调用
namespace myEpollerModule
{
    using namespace myLogModule;
    using namespace myCommonModule;

    // 强枚举类型
    enum class myCtrl
    {
        ADD = EPOLL_CTL_ADD,
        DEL = EPOLL_CTL_DEL,
        MOD = EPOLL_CTL_MOD
    };

    // reactor想要wait事件、ctrl事件时,都是面对epoller进行操作的,而不是一个裸的epoll
    class myEpoller
    {
        // 单例,不允许被拷贝,赋值
        myEpoller(const myEpoller &) = delete;
        myEpoller &operator=(const myEpoller &) = delete;

        myEpoller()
        {
            _epfd = epoll_create(1); // 参数随便填,已经没用了
            if (_epfd < 0)
            {
                LOG(LogLevel::FATAL) << "epoll模型创建失败";
                exit(static_cast<int>(myError::EPOLL_CREATE_ERROR));
            }
            LOG(LogLevel::INFO) << "epoll模型创建成功";
        }

        ~myEpoller()
        {
            if (_epfd >= 0)
                close(_epfd);
        }

    public:
        static myEpoller *GetInstance()
        {
            if (_epoller == nullptr)
                _epoller = new myEpoller(); // 默认执行构造函数,当第一次调用获取单例之后,整个epoll模型就已经就绪了
            return _epoller;
        }

        // 调用wait接口,epoller从就绪队列取出就绪事件并写入revs中,并且返回就绪事件的个数,返回值不会超过num
        int Wait(struct epoll_event *revs, int num, int timeout)
        {
            // nfds不会超过num
            int nfds = epoll_wait(_epfd, revs, num, timeout); // revs是输出型参数,调用方调用wait后可以直接拿到就绪的事件和fd,结合返回值全部取出
            if (nfds < 0)
                LOG(LogLevel::ERROR) << "epoll模型等待就绪事件失败";
            return nfds;
        }

        bool Ctrl(int fd, uint32_t events, myCtrl op)
        {
            // 创建一个epoll_event结构体,并将自己关心的fd和事件写进去
            struct epoll_event ev;
            ev.events = events;
            ev.data.fd = fd;

            // 调用epoll_ctl接口,将自己关心的fd和事件写入到epoll模型的红黑树中并向fd注册服务,使得后续数据准备就绪后会自动添加到就绪队列中
            int n = epoll_ctl(_epfd, static_cast<int>(op), fd, &ev); // op有不同类型,对应不同操作,add、del、mod
            if (n < 0)
            {
                // 操作失败,针对不同操作类型返回错误信息
                switch (op)
                {
                case myCtrl::ADD:
                    LOG(LogLevel::INFO) << "epoll模型添加fd完成";
                    break;
                case myCtrl::DEL:
                    LOG(LogLevel::INFO) << "epoll模型删除fd完成";
                    break;
                case myCtrl::MOD:
                    LOG(LogLevel::INFO) << "epoll模型修改fd完成";
                    break;
                }
                return false;
            }
            return true;
        }

    private:
        int _epfd = -1; // epoll的文件描述符

        // 采用懒汉模式,创建reactor对象的时候再创建这个Epoller对象
        static myEpoller *_epoller; // 这里就用指针即可,没必要用智能指针,因为单例已经通过接口限制实现了
    };

    myEpoller *myEpoller::_epoller = nullptr;
}

(2)Reactor和Connection


其中 using connection_t = std::shared_ptr<myConnection>;

反应堆模块包含我们刚才封装的epoller单例,可见的是reactor模块直接和epoller模块进行交互。并且reactor模块也包含connection_t,可见reactor模块也直接和用户连接交互,是一个中间层。

既然反应堆直接和epoller交互,那么reactor也承担向epoll模型添加connection_t事件,设置ET模式的责任,代码如下:

// 拷贝构造connection_t,connection_t是智能指针
void AddConnection(connection_t conn) // 在reactor看来所有的fd、事件都是connection,向epoll模型写的都是根据connection来的
{
    std::unordered_map<int, connection_t>::iterator it = _connections.find(conn->GetSockfd()); // 每个connection代表一个连接,里面就有要关心的fd和事件,所以我们需要根据这个fd到哈希表中去找
    if (it == _connections.end())                                                              // 说明这个事件还没有出现过,说明是新连接,需要添加到epoll模型中
    {
        // 先将connection添加到哈希表中
        _connections.insert(std::make_pair(conn->GetSockfd(), conn));
        // 再将connection添加到epoll模型中
        _epoller->Ctrl(conn->GetSockfd(), conn->GetEvents(), myCtrl::ADD);
        // 每个进入reactor的connection都要设置回指指针,这样能够在connection中通过回指指针添加新的connection到epoll中
        conn->SetReactor(this); // 设置回指指针,设置的信息会同步在哈希表中,因为conn是一个智能指针,指向的是同一块资源
        LOG(LogLevel::INFO) << "新连接添加到反应堆中,epoller模块已接管,连接信息为" << conn->PrintAddr() << ",分配的fd为" << conn->GetSockfd();
    }
    else
        LOG(LogLevel::WARNING) << "连接信息" << conn->PrintAddr() << "已存在反应堆中,无需重复添加";
}

我们暂时先对reactor理解到这里,我们先处理connection的理解。reactor面向的都是一个一个的connection。

通过这个可知,每个connection都回指向反应堆,这就意味着通过回指指针,每个connection有权力修改自己或者其它connection在epoll模型中的所关心的事件,因为通过回指指针我们可以调用reactor模块里面的函数,而这些函数可以跟epoller交互。

connection是一个基类,其子类可以是Listener类或者IOService类,分别代表不同种类的connection实现,这套继承体系我们要理解。

Listener模块中一个重要的接口如下(建立连接):

// 有新连接了
void Recver() override
{
    // ET模式下,反复读取,因为就绪事件只会通知一次
    while (true)
    {
        myInetAddr peer_addr;
        int peer_errno = 0;
        int sockfd = _listensock->Accept(&peer_addr, &peer_errno); // 调用accept函数,获取新连接的fd和客户端的IP和端口号
        if (sockfd > 0)                                            // 获取的和客户端沟通的服务fd,这个fd应该创建connection对象并被插入到反应堆中
        {
            LOG(LogLevel::INFO) << " accept到一个新连接并分配fd:" << sockfd;

            std::shared_ptr<myIOService> conn = std::make_shared<myIOService>(sockfd); // 创建一个新的connection对象,这个对象就是和客户端沟通的服务对象
            conn->RegisterCallback(HandlerRequest); // 注册服务
            conn->SetAddr(peer_addr); // 设置客户端的IP和端口号
            // 插入到Epoll模型中,利用回指指针
            GetReactor()->AddConnection(conn); // 把这个connection对象插入到反应堆中
        }
        else // 读取失败,做异常处理
        {
            if (peer_errno == EAGAIN || peer_errno == EWOULDBLOCK)
            {
                LOG(LogLevel::INFO) << "已经accept所有的连接";
                std::cout << "当前缓冲区:" << _inbuffer;
                break;
            }
            else if (peer_errno == EINTR)
            {
                LOG(LogLevel::INFO) << "连接被信号中断,重新连接";
                continue;
            }
            else
            {
                LOG(LogLevel::WARNING) << "accepte错误,退出连接";
                break;
            }
        }
    }
}

我们可以验证,在connection继承体系下,我们利用基类的回指指针就可以将创建的连接的connection插入到epoll模型中。

这时,我们再回到reactor中,理解其Loop函数

void LoopOnce(int timeout)
{
    // 调用epoll模型的wait接口,等待就绪事件
    int nfds = _epoller->Wait(_revs, event_num, timeout);
    Dispatcher(nfds); // 这个时候就绪的事件已经写到_revs里面了,根据返回值nfds就能对里面的事件进行完全的梳理
}

void Dispatcher(int nfds)
{
    for (int i = 0; i < nfds; i++)
    {
        // 取出就绪的fd
        int sockfd = _revs[i].data.fd;

        // 取出就绪的事件,主要是用于异常处理转换
        uint32_t events = _revs[i].events;
        // 根据就绪事件的类型进行处理
        if ((events & EPOLLERR) | (events & EPOLLHUP))
            events = (EPOLLIN | EPOLLOUT); // 首先将异常事件转为读写事件,读写错误后再进行进一步处理

        // 异常处理后有可能文件就被关了,所以要先判断sockfd是否合法。可以预见的是异常处理都会在Recver()里面被处理
        if ((events & EPOLLIN) && isConnectionInReactor(sockfd))
            _connections[sockfd]->Recver(); // 该connection触发了读事件,这个connection里面会有相关的处理方式,调用即可。对反应堆来说,一切连接都叫connection
        if ((events & EPOLLOUT) && isConnectionInReactor(sockfd))
            _connections[sockfd]->Sender();
    }
}

void PrintConnections()
{
    std::cout << std::endl
              << "当前反应堆中连接信息如下:" << std::endl;
    for (auto &conn : _connections)
        std::cout << "fd:" << conn.first << ",连接信息为" << conn.second->PrintAddr() << std::endl;
    std::cout << std::endl;
}

// 启动反应堆后就进入这个函数
void Loop()
{
    int timeout = 1000 * 10; // 10s 称为一次超时
    while (_isrunning)
    {
        // 事件管理
        LoopOnce(timeout);
        // 管理信息打印
        PrintConnections();
        // 超时管理等其它模块均可在这里拓展
    }
}

这和之前poll和select的代码及其相似,通过派发器,reactor会将就绪的事件派发给不同的connection,而这些connection就是Listener或者IOService模块,不同模块会重写自己的Sender(),Recver(),Excepter()函数,实现多态。

这时我们也就能理解IOservice里面函数的作用了

void Sender() override
{
    while(true) // 一直写直到失败
    {
        int n = send(GetSockfd(), _outbuffer.c_str(), _outbuffer.size(), 0);
        if(n > 0)
            EraseOutBuffer(n);
        if(n == 0) 
            break;
        else
        {
            if(errno == EAGAIN || errno == EWOULDBLOCK) // 缓冲区写满了
                break;
            else if(errno == EINTR)// 被信号打断,重新写
                continue; 
            else
            {
                Excepter(); // 读写错误转为异常处理
                return;
            }
        }
    }

    // 走到这里的代码不是异常错误,也不是信号错误,而是缓冲区写满了或者发送完毕了
    if(!isOutBufferEmpty()) // 说明是对方缓冲区写满了
        GetReactor()->ModConnection(GetSockfd(), true, true); // 再次添加对读写事件的关心
    else // 说明发送完毕了,outbuffer里面已经没有需要再发送的数据了,所以不再需要关心写事件了
        GetReactor()->ModConnection(GetSockfd(), true, false); // 不再关心写事件
}

void Recver() override
{
    LOG(LogLevel::INFO) << "fd:" << GetSockfd() << ",触发读事件";
    while(true) // 一直读直到失败
    {
        char buffer[size] = { 0 };
        int n = recv(GetSockfd(), buffer, size - 1, 0); // 非阻塞
        if(n > 0)
        {
            buffer[n] = 0; // 补全字符串的\0
            AppendInBuffer(buffer);
        }
        else if(n == 0)
        {
            Excepter();
            break;
        }
        else
        {
            if(errno == EAGAIN || errno == EWOULDBLOCK)
                break; // 缓冲区读完了
            else if(errno == EINTR)
                continue; // 被信号打断,重新读
            else
            {
                Excepter(); // 读写错误转为异常处理
                return;
            }
        }
    }

    // 走到这里的代码不是异常错误,也不是信号错误,而是缓冲区读完了或者对方关闭了连接
    std::cout << "接收到的消息为:" << std::endl << _inbuffer;
    std::string result;
    if(_fun)
        result = _fun(_inbuffer); // 调用上层注册的方法,得到处理后的结果
    LOG(LogLevel::INFO) << "处理后的结果为:" << result;
    // 添加到应答中
    AppendOutBuffer(result);
    if(_inbuffer == "QUIT")
    {
        _inbuffer.clear(); // 去掉前4个字符
        LOG(LogLevel::INFO) << "客户端请求退出";
    }
    
    // 触发写事件,自动处理缓冲区的发送任务
    if(!isOutBufferEmpty())
        GetReactor()->ModConnection(GetSockfd(), true, true); // 把应答的结果写回给客户端,添加写事件之后自动触发写过去
}

void Excepter() override
{
    LOG(LogLevel::INFO) << "触发异常处理,sockfd为" << GetSockfd();
    // 触发异常处理,将该连接从反应堆中移除
    GetReactor()->DelConnection(GetSockfd()); // 移除之后connection自动调用析构函数,自动close文件
}

在这里,我们能看到服务端的connection,只要数据发送完毕,之后就设置该fd取消对写事件的关心。读完毕并处理数据完之后,需要发回客户端,写的时候再打开对写事件的关心,写事件处理完之后继续关闭写事件关心,打开读事件关心。一切IO的触发都是交给通过回指指针,交由reactor所管理的epoll模型来派发的,并不是说收到数据处理之后直接调用Sender()写回结果,而是设置到epoller中,由epoller统一调用。

(3)IOService注册服务

这个IOService要处理怎样的任务呢?由上层决定,我们仅需提供设置接口,在收到数据后调用该接口处理即可。

这不就是epoll的底层原理中上层向下层注册服务的方法吗?设计思想是一致的。

于是,IOService就是一个负责接收数据、处理数据的模块,而处理数据的方法和该模块解耦,我们上层可以传入各种各样的处理方法,得到相应的结果。

(4)反应堆模式设计结构

(5)对不同模块之间耦合方式的理解

反应堆模式毫无疑问采用了模块化设计,但它如何让不同的模块之间耦合起来非常值得我们学习。

①上层向下层注册服务

这在epoll模型的底层实现,信号驱动式IO的底层实现上已经见过。

在反应堆模式中,IOService对应的connection负责和客户端通信,Recver()主要负责三件事:接收数据、处理数据、激活写事件。 其中处理数据就是调用一个默认为空的fun(),并且IOService对外提供一个设置fun的接口,上层可以通过这个接口传入任何函数,而下层都会去实现,这个就是上层向下层注册服务。 通过这种方式,我们实现了处理数据操作和处理方法之间的接耦,将IOService变得更加通用了。

②回指指针影响下层

Listener模块调用Recver()获取到连接之后怎么办?创建IOService,但怎么将这个IOService写透到下层的服务中呢,即让Reactor模块的epoller关心该connection的事件?

回指指针!connection里面有着指向reactor的回指指针,当Listener得到连接并创建IOService之后,它直接用基类的回指指针指向下层,进入下层,并调用下层reactor里面的成员函数,将新连接写透到epoll模型中。

其实在反应堆模式中,最底层是Epoller,reactor只能算第二层,那reactor怎么将关心的fd和事件写入到epoll模型中呢?调用epoller对外提供的成员函数。但问题是怎么调用的呢?
本质上也是个回指指针!

③对上下层模块耦合的理解

前面两个例子中,似乎都是上层影响下层。其实在我们的模块化设计中,上下层关系一定存在,那么在上下层进行耦合的过程中,下层影响上层是最轻松的,而上层影响下层是困难的。 因为上层天然受到下层类的影响,上层可以通过调用接口修改下层类的一些属性,而下层的属性改变会直接影响上层。从另一个角度上说,上层也影响了下层,但目的是不一样的,影响到了下层是手段,目的是影响上层,这个我们要区分开。 但上层想要影响下层很困难,回指指针和上层向下层注册服务就是其中的解决方案,第一次接触时我们其实难以接受。

用一个形象的例子就能理解了,房子是由地基和上层建筑构成的。房子塌了,地基还在,对于地基而言,它能独立存在,它不依赖上层建筑,就算没有上层建筑,地基一样好好的。而上层建筑却依赖地基,地基随便一动,上层建筑就会受到影响。

【 ‌infrastructure】【数据中心】【AI infra】第十篇 智能计算数据中心解决方案集成测试和交付知识体系02 编号类型领域模块子模块学科知识点及详细数学建模与数值设计(现象—根因—模型—流程—策略—极端)关联知识/标准/论文/工业实践9.96拥塞/通信栈智算互联内核态socket与控制面gRPC/brpc分离内核协议栈/上下文切换/控制带宽操作系统/分布式系统现象:万卡训练用gRPC做参数服务器或brpc做配置下发,小消息P99达数百μs,控制事件拥塞导致集合通信启动慢。根因:内核TCP栈路径为应用→socket→skb→协议栈→软中断→NIC,小消息系统调用与上下文切换占比高;gRPC基于HTTP/2多路复用但仍 阅读详情

相关推荐

《跨界杂谈》华为印象(五):高度集权的矩阵式架构

高度集权的矩阵式架构 “高度集权的矩阵式架构”不是官方术语。是笔者的一位同事非常经典的总结。解释这个句话还是有点困难的,笔者入职华为三年也没彻底搞明白,在这里只能尝试性的解释一下。 矩阵 我们先看一下“矩阵”。华为有两套部门职能体系,华为内部术语,叫实线部门和虚线部门。由实线与虚线部门交叉构成矩阵。大部分(但不是所有)华为员工都由两个部门(两个领导)进行管理。对员工而言,所谓的实线部

《云计算架构技术与实践》 2996

epoll原理详解及epoll反应堆模型

设想一个场景:有100万用户同时与一个进程保持着TCP连接,而每一时刻只有几十个或几百个TCP连接是活跃的(接收TCP包),也就是说在每一时刻进程只需要处理这100万连接中的一小部分连接。那么,如何才能高效的处理这种场景呢?进程是否在每次询问操作系统收集有事件发生的TCP连接时,把这100万个连接告诉操作系统,然后由操作系统找出其中有事件发生的几百个连接呢?实际上,在Linux2.4版本以前,那时...

LiuenshengUp的博客 1133

SAP物料主数据表结构全解析:ABAP开发与MM模块必知必会

本文深入解析SAP物料主数据的核心表结构,涵盖MARA、MAKT、MARC、MARD及MVKE等关键表。详细阐述了物料主数据在ABAP开发和MM模块中的存储逻辑与关联关系,帮助开发者和顾问高效处理数据查询、增强功能及业务故障排查,是掌握SAP物料管理基础的必读指南。

weixin_29216957的博客 382

epoll反应堆

epoll还有一种更高级的使用方法,那就是借鉴封装的思想,简单的说就是当某个事情发生了,自动的去处理这个事情。这样的思想对我们的编码来说就是设置回调,将文件描述符,对应的事件,和事件产生时的处理函数封装到一起,这样当某个文件描述符的事件发生了,回调函数会自动被触发,这就是所谓的反应堆思想。 从我们之前对epoll的使用上如何去支持反应堆呢?需要重新再认识一下struct epoll_event中的epoll_data_t结构体: typedef union epoll_data { void

weixin_42374938的博客 1025

基于禾赛32线雷达和Xsens MTi-G-710 组合导航模块,松灵Scout mini 底盘实现 Cartographer 3D 建图过程

地下车库 Cartongrapher 3D建图

yhy_yhy13053117的博客 3940

从CentOS迁移到openEuler 24.03 LTS:手把手教你部署Java+MySQL+Redis+Nginx全栈环境

本文详细指导如何从CentOS迁移到openEuler 24.03 LTS,并部署Java+MySQL+Redis+Nginx全栈环境。涵盖系统配置、软件安装、性能优化及环境验证,助力企业级用户高效完成服务器迁移与部署。

weixin_26938645的博客 389

linux网络编程(六)epoll反应堆

【代码】linux网络编程(六)epoll反应堆。

da.的博客 552

语言模型推理能力与计算表达力深度解析

语言模型的推理能力本质上是一种知识迁移与泛化应用的过程,其核心在于Transformer架构中的自注意力机制和前馈网络的协同作用。计算表达力作为衡量模型能力的关键指标,涉及函数逼近、组合泛化和上下文理解等多个维度。随着模型规模的扩大,涌现能力现象揭示了参数规模与推理能力之间的非线性关系。在实际应用中,通过提示工程和微调策略可以有效激活和优化模型的推理表现。当前研究正探索模块化架构和世界模型整合等方向,以突破长程推理和因果关系建模等前沿挑战。本文深入探讨了语言模型推理泛化的技术原理与实践方法,为理解计算表达力

weixin_42521032的博客 343

网络编程高并发

mac地址6个字节 48位标识网卡的id,理论上这个id全球唯一mac地址一般用来标识主机的id,这个id是物理地址,不会改变。IP地址(ipv4)4个字节 32位如果是ipv6 16个字节 128位IP地址标识主机的ID,这个id是虚拟的是会改变的。IP地址是一种 Internet上的主机编址方式,也称为网际协议地址。IP地址是组成:使用32bit,由{子网 ID,主机 ID}两部分组成。子网ID:IP地址中由子网掩码中1覆盖的连续位。

m0_69093426的博客 1404

Java代码变更影响分析:从静态解析到动态调用图的实战全链路

本文介绍了Java代码提交影响分析的核心原理与实现方法。主要内容包括:1)静态分析技术,通过JavaParser解析AST构建调用图,记录类与方法间的调用关系;2)动态分析技术,使用ByteBuddy进行字节码增强,捕获运行时调用路径;3)JCCI工具实现,结合javalang、unidiff等技术分析代码改动影响,并将结果存储到SQLite数据库。文章提供了完整的代码示例,展示了从AST解析、动态调用跟踪到实际工具应用的完整技术栈,为代码变更影响分析提供了实用解决方案。

墨夶的博客 1252

2、硬件工程师笔试面试汇总(2025版本)

本文摘要:文章系统梳理了硬件工程师笔试面试中的电子元器件知识要点,涵盖电阻、电容、电感、二极管、三极管、MOS管等器件的工作原理、选型要点和应用场景。详细解析了UART、I2C、SPI等通信协议的特点及实现方式,并探讨了电源管理、信号处理、PCB设计等核心问题。针对常见电路如放大电路、滤波电路、逻辑电路等提供了深入分析,同时介绍了示波器使用、单片机系统设计等实用技能。文章还包含器件封装、电平转换、时序分析等工程实践内容,并附有相关真题解析和仿真实验指导,为硬件工程师提供全面的技术参考。

XU157303764的博客 9011

epoll 反应堆模型(Libevent库核心思想)

epoll 反应堆模型是从 libevent 库里面抽取的核心代码

weixin_43200943的博客 1200

33、密码学中的选择密文攻击与Cramer - Shoup加密方案

本文详细介绍了密码学中选择密文攻击的背景及其对Cramer-Shoup公钥加密方案的影响。该方案在决策Diffie-Hellman假设和抗碰撞哈希函数的基础上,能够有效抵御自适应选择密文攻击,并且其安全性无需依赖随机预言机模型。文章还分析了Cramer-Shoup方案的设计原理、加密与解密过程,以及其在标准假设下的安全性证明方法。此外,通过一系列相关练习,帮助读者进一步理解密码学概念如密文不可区分性、ElGamal加密、Goldwasser-Micali方案等。最后,文章展望了密码学的未来发展趋势,包括量子

vscode6remote的博客 100

Epoller

Epoller

qq_53558968的博客 513

华为的C语言编程规范.pdf

华为技术有限公司内部技术规范。本规范制定了编写C语言程序的基本原则、规则和建议。从代码的清晰、简洁、可测试、安全、程序效率、可移植各个方面对C语言编程作出了具体指导。

精雕软件(JDpaint) 5.5 绿色版

精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版精雕软件(JDpaint) 5.5 绿色版

epoll底层原理深究

后端面试之epoll底层原理深究,理解底层数据结构、协议栈与epoll的回调时机

hjlogzw的博客 6215

RU DOS版、EFI版32bit/64bit BIOS常用工具

DOS版、EFI版的RU工具,可用来查看IO、MEM、PCI、SMBIOS等资源。亲测在EFI SHELL下可用。

epoll底层原理

epoll作为linux下高性能网络服务器的必备技术至关重要,java NIO、nginx、redis、skynet和大部分游戏服务器都使用了这一多路复用技术。 epoll是select和poll的增强版本。

田埂、の技术博客 3994

epoll反应堆模型

核心:epoll ET模式 + 非阻塞socket + 轮询 + void *ptr(回调函数)就是原来咱们监听到事件比如说读事件的话,就得自己去写read或者recv去读数据, 现在有了ptr,就不用管了,有了读事件程序自己就帮我调了回调函数,我不用自己写了2.epoll_create 创建监听 红黑树3.返回 epfd – epo11_ct1()向树上添加一个监听fd4.while(1) 循环着做下面的事情 5-105.epoll_wait 监听6.对应监听fd有事件产生。

m0_74795952的博客 1294

Epoll底层原理

Epoll 是 Linux 内核提供的一种 I/O 事件通知机制,通过在用户态和内核态之间建立一个数据结构,使得用户态程序可以在内核态中注册感兴趣的事件,当事件发生时,内核会通知用户态程序。Epoll 采用了“事件驱动”的方式,只在事件发生时才会通知用户态程序,避免了传统 I/O 事件通知机制中因轮询导致的效率低下问题。Epoll 通过使用“事件驱动”的方式,避免了传统 I/O 事件通知机制中因轮询导致的效率低下问题,提高了系统的性能和并发能力。当事件发生时,Epoll 对象会将该事件通知给用户态程序。

qq_31231915的博客 1045

JAVA面试题分享九十二:Epoll 底层原理?

epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本。

之乎者也·的博客 1108

epoll底层实现过程

1  epoll一种网络模式,采用的是 IO多路复用技术(就是可以监控多个文件描述符),相比较于select 和poll是非常快的; 首先看这三个函数: 1 int epoll_create(int size); 2 int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 3 int epoll_wai

fengfeifengfei的专栏 2058

Epoll具体底层流程

/就绪socket队列,epoll_wait在有就绪事件后通过socker->sock->sd_data_ready调用注册的回调函数ep_poll_callback,ep_poll_callback函数会将有就绪的epitem放入rdllist;(2)等数据接收完毕,调用socker->sock->sk_data_ready也就sock_def_readable,这个函数可以找到epoll_ctl注册到socker->sock->wq里等待项qq里的注册函数ep_poll_callback;

yanerhao的专栏 1322

【linux网络编程】epoll反应堆模型

反应堆的理解:加入IO转接之后,有了事件,server才去处理,这里反应堆也是这样,由于网络环境复杂,服务器处理数据之后,可能并不能直接写回去,比如遇到网络繁忙或者对方缓冲区已经满了这种情况,就不能直接写回给客户端。反应堆就是在处理数据之后,监听写事件,能写会客户端了,才去做写回操作。写回之后,再改为监听读事件。如此循环。...

StudyWinter的博客 1339

多路转接之epoll

。

m0_73919451的博客 1630

Linux网络 多路复用epoll && Reactor 反应堆模式

epoll 是 Linux 内核为处理大批量文件描述符而设计的 I/O 事件通知机制,它提供了一种高效的方式,能够同时监控多个文件描述符的事件(如读、写等),并且回调那些就绪的文件描述符。相比于传统的 select 和 poll,epoll 在处理大量并发连接时具有更高的效率,特别适用于高并发服务器的开发。功能:创建一个 epoll 实例,返回一个文件描述符。size 参数已弃用,只需要大于 0 即可。成功:返回epoll文件描述符。失败:返回-1,并设置errno。

2301_79881188的博客 1056
上一篇: Linux相关概念和易错知识点(47)(五种IO模型、多路转接、select、poll、epoll)
下一篇: Linux相关概念和易错知识点(49)(HTTPS、数据摘要、SSL证书、ARP欺骗)
-SGlow-
博客等级 码龄3年 1513粉丝 118原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

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

抵扣说明:

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

余额充值