04 | Swoole 源码分析之 epoll 多路复用模块

news2024/12/24 2:37:04

首发原文链接:Swoole 源码分析之 epoll 多路复用模块

大家好,我是码农先森。

引言

在传统的IO模型中,每个IO操作都需要创建一个单独的线程或进程来处理,这样的操作会导致系统资源的大量消耗和管理开销。

而IO多路复用技术通过使用少量的线程或进程同时监视多个IO事件,能够更高效地处理大量的IO操作,从而提高系统的性能和资源利用率。

在IO多路复用的技术中尤其突出的是 epoll 技术,它是解决 C10K 问题的利器。

Swoole 中的多路复用

多路复用技术可以说是贯穿了整个 Swoole,同时也是 Swoole 为什么是高性能通信框架的根本原因。

Swoole 最重要的协程模块就是利用的 IO 多路复用事件循环技术,这也是与 Go 语言中协程不同的本质原因。

下面我们来一起看下 Swoole 中是如何实现 epoll 多路复用技术的。

这是创建 eoll 实例的方法,其中的 Reactor 是一个线程对象。

// 创建一个 epoll 实例,为其分配事件数组,并设置相关的 reactor 对象属性
// swoole-src/src/reactor/epoll.cc:71
ReactorEpoll::ReactorEpoll(Reactor *_reactor, int max_events) : ReactorImpl(_reactor) {
    // 创建一个 epoll 实例
    epfd_ = epoll_create(512);
    // 检查 epoll 创建是否成功
    if (!ready()) {
        swoole_sys_warning("epoll_create failed");
        return;
    }

    // epoll_event 结构体数组分配内存
    // 用于存储注册到 epoll 实例上的事件
    events_ = new struct epoll_event[max_events];
    // 设置最大事件数量
    reactor_->max_event_num = max_events;
    // native_handle 设置为 epoll 实例
    reactor_->native_handle = epfd_;
}

这个方法是向 epoll 事件循环中添加一个客户端的连接对象,用于监听。

// 向 epoll 事件循环中添加一个 socket,并为其设置特定的事件监听
// swoole-src/src/reactor/epoll.cc:94
int ReactorEpoll::add(Socket *socket, int events) {
    // 定义 epoll_event 结构体实例 e
    struct epoll_event e;
    // 设置事件类型
    e.events = get_events(events);
    // 设置 socket 指针,在 epoll 触发事件时,可以找到对应的 socket
    e.data.ptr = socket;

    // 添加事件到 epoll
    if (epoll_ctl(epfd_, EPOLL_CTL_ADD, socket->fd, &e) < 0) {
        swoole_sys_warning(
            "failed to add events[fd=%d#%d, type=%d, events=%d]", socket->fd, reactor_->id, socket->fd_type, events);
        return SW_ERR;
    }

    // 在 Reactor 中添加 socket
    // 为了在 Reactor 内部进行管理和跟踪
    reactor_->_add(socket, events);
    swoole_trace_log(
        SW_TRACE_EVENT, "add events[fd=%d#%d, type=%d, events=%d]", socket->fd, reactor_->id, socket->fd_type, events);

    return SW_OK;
}

这个方法是从 epoll 事件循环中移除一个客户端连接对象。

// 从 epoll 事件循环中删除一个 socket
// swoole-src/src/reactor/epoll.cc:113
int ReactorEpoll::del(Socket *_socket) {
    // 检查 socket 是否已被移除
    if (_socket->removed) {
        swoole_error_log(SW_LOG_WARNING,
                         SW_ERROR_EVENT_SOCKET_REMOVED,
                         "failed to delete events[fd=%d, fd_type=%d], it has already been removed",
                         _socket->fd, _socket->fd_type);
        return SW_ERR;
    }
    
    // 使用 epoll_ctl 函数从 epoll 的文件描述符 epfd_ 中删除 socket
    if (epoll_ctl(epfd_, EPOLL_CTL_DEL, _socket->fd, nullptr) < 0) {
        after_removal_failure(_socket);
        if (errno != EBADF && errno != ENOENT) {
            return SW_ERR;
        }
    }

    swoole_trace_log(SW_TRACE_REACTOR, "remove event[reactor_id=%d|fd=%d]", reactor_->id, _socket->fd);
    // 从 Reactor 中删除该 socket
    reactor_->_del(_socket);

    return SW_OK;
}

这个方法是用于修改一个已经在 epoll 事件循环中的客户端连接对象。

// 修改一个已经存在于 epoll 事件循环中的 socket 的事件监听类型
// swoole-src/src/reactor/epoll.cc:134
int ReactorEpoll::set(Socket *socket, int events) {
    // 定义 epoll_event 结构体实例 e
    struct epoll_event e;
    // 设置事件类型
    e.events = get_events(events);
    // 设置 socket 指针,在 epoll 触发事件时,可以找到对应的 socket
    e.data.ptr = socket;

    // 使用 epoll_ctl 函数修改 epoll 文件描述符 epfd_ 中对应 socket 的事件
    int ret = epoll_ctl(epfd_, EPOLL_CTL_MOD, socket->fd, &e);
    if (ret < 0) {
        swoole_sys_warning(
            "failed to set events[fd=%d#%d, type=%d, events=%d]", socket->fd, reactor_->id, socket->fd_type, events);
        return SW_ERR;
    }

    swoole_trace_log(SW_TRACE_EVENT, "set event[reactor_id=%d, fd=%d, events=%d]", reactor_->id, socket->fd, events);
    // 在 Reactor 内部进行相应的设置
    reactor_->_set(socket, events);

    return SW_OK;
}

这个方法是 epoll 事件循环环节中最重要的一点,开始等待 Socket IO事件的触发,并且调用对应的处理函数。

// swoole-src/src/reactor/epoll.cc:153
int ReactorEpoll::wait(struct timeval *timeo) {
    // 声明事件对象 event、Reactor 处理对象 handler
    Event event;
    ReactorHandler handler;
    int i, n, ret;

    // reactor 对象 ID 和 最大事件数量
    int reactor_id = reactor_->id;
    int max_event_num = reactor_->max_event_num;

    // 用于设置超时时间,如果 timeout_msec 为 0,则根据传入的 timeo 参数设置超时时间
    if (reactor_->timeout_msec == 0) {
        if (timeo == nullptr) {
            reactor_->timeout_msec = -1;
        } else {
            reactor_->timeout_msec = timeo->tv_sec * 1000 + timeo->tv_usec / 1000;
        }
    }
	
	// 在进入事件循环之前调用 before_wait 方法,表示准备开始等待事件
    reactor_->before_wait();

    while (reactor_->running) {
        // 如果定义了 onBegin 回调函数,则调用它来执行相应的操作
        if (reactor_->onBegin != nullptr) {
            reactor_->onBegin(reactor_);
        }

        // 调用 epoll_wait 函数获取就绪事件的数量
        n = epoll_wait(epfd_, events_, max_event_num, reactor_->get_timeout_msec());
        if (n < 0) {
            // 如果出现错误且不捕获错误,则打印错误信息并返回错误码
            if (!reactor_->catch_error()) {
                swoole_sys_warning("[Reactor#%d] epoll_wait failed", reactor_id);
                return SW_ERR;
            } else {
                goto _continue;
            }
        } else if (n == 0) {
            // 如果返回的就绪事件数为 0,则执行结束回调函数并继续下一轮循环。
            reactor_->execute_end_callbacks(true);
            SW_REACTOR_CONTINUE;
        }
        
        for (i = 0; i < n; i++) {
            // 在处理每个就绪事件时,将事件相关信息保存在event对象中
            event.reactor_id = reactor_id;
            event.socket = (Socket *) events_[i].data.ptr;
            event.type = event.socket->fd_type;
            event.fd = event.socket->fd;

            // 如果事件类型是 EPOLLRDHUP、EPOLLERR 或 EPOLLHUP 之一,则设置 event_hup 标志为 1。
            if (events_[i].events & (EPOLLRDHUP | EPOLLERR | EPOLLHUP)) {
                event.socket->event_hup = 1;
            }

            // 检查是否存在可读事件且套接字未被移除
            // read 如果是可读事件(EPOLLIN),则调用相应的读事件处理器
            if ((events_[i].events & EPOLLIN) && !event.socket->removed) {
                handler = reactor_->get_handler(SW_EVENT_READ, event.type);
                ret = handler(reactor_, &event);
                if (ret < 0) {
                    swoole_sys_warning("EPOLLIN handle failed. fd=%d", event.fd);
                }
            }
            
            // 检查是否存在可写事件且套接字未被移除
            // write 如果是可写事件(EPOLLOUT),则调用相应的写事件处理器。
            if ((events_[i].events & EPOLLOUT) && !event.socket->removed) {
                handler = reactor_->get_handler(SW_EVENT_WRITE, event.type);
                ret = handler(reactor_, &event);
                if (ret < 0) {
                    swoole_sys_warning("EPOLLOUT handle failed. fd=%d", event.fd);
                }
            }

            // error 如果是错误事件(EPOLLRDHUP、EPOLLERR、EPOLLHUP),则调用相应的错误事件处理器。
            if ((events_[i].events & (EPOLLRDHUP | EPOLLERR | EPOLLHUP)) && !event.socket->removed) {
                // ignore ERR and HUP, because event is already processed at IN and OUT handler.
                if ((events_[i].events & EPOLLIN) || (events_[i].events & EPOLLOUT)) {
                    continue;
                }
                handler = reactor_->get_error_handler(event.type);
                ret = handler(reactor_, &event);
                if (ret < 0) {
                    swoole_sys_warning("EPOLLERR handle failed. fd=%d", event.fd);
                }
            }

            // 在处理完事件后,检查是否需要执行一次性事件的删除操作
            if (!event.socket->removed && (event.socket->events & SW_EVENT_ONCE)) {
                reactor_->_del(event.socket);
            }
        }

    _continue:
        // 在事件循环中执行回调函数并继续下一轮循环
        reactor_->execute_end_callbacks(false);
        SW_REACTOR_CONTINUE;
    }
    return 0;
}

总结

  1. epoll 在内部使用了红黑树的数据结构,红黑树是一个高效的数据结构。
  2. epoll 是解决 C10K 问题的利器,不仅是在 Swoole 中被应用,在很多的高性能服务中也有应用,例如:Nginx 服务等。
  3. Swoole 被称为高性能通信框架的关键原因,就是采用了 epoll 多路复用技术。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1558210.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

第十四届蓝桥杯JavaA组省赛真题 - 棋盘

解题思路&#xff1a; 暴力 棋盘类题目取反操作&#xff1a; f[a][b]^1; 或者f[a][b] 1 - f[a][b]; import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner scan new Scanner(System.in);int n scan.nextInt();int m scan.nex…

(南京观海微电子)——GOA介绍

GOA是Gate on Array的简写&#xff0c;简单可以理解为gate IC集成在玻璃上了&#xff0c;面板就可以不用gate ic了&#xff0c;是一种低成本的设计&#xff0c;窄边框面板大多数都用了GOA技术。还有一些公司叫GIP&#xff08;Gate in Panel&#xff09;&#xff0c;GDM等等。 …

已上线项目,突然有一天网站虽进得去,但是接口拿不到数据,作为前端的你如何排查问题?

在开始写这篇博客之前,想说几句题外话哈,虽然自己的粉丝不多,但自己每篇博客都是用心在写,可能后面会针对部分文章开启只有VIP才能访问,原因你们也懂得(▽),无非是想赚点外块呗,不过主要现在也是知识付费时代,毕竟自己写出的东西也是本人亲身经历着,也是具有一定的价值…

针对pycharm打开新项目需要重新下载tensorflow的问题解决

目录 一、前提 二、原因 三、解决办法 一、前提 下载包之前&#xff0c;已经打开了&#xff0c;某个项目。 比如&#xff1a;我先打开了下面这个项目&#xff1a; 然后在terminal使用pip命令下载&#xff1a; 如果是这种情况&#xff0c;你下载的这个包一般都只能用在这一个…

【已解决】centos执行Hangfire任务时间隔八个小时

参数说明&#xff1a; centos系统是7.9版本的 hangfire是Hangfire 1.8.7 centos系统的目前设置时区是cts的东八时区&#xff0c;截图如下 原因&#xff1a; hangfire任务设定每天执行时间东八实际偏移的执行时间 比如&#xff1a;你设定的是早上6点执行东八实际下午两点执行 …

【web自动化】selenium的使用(二)

目录 复选框操作多层框架操作下拉框操作弹窗操作 复选框操作 示例&#xff1a;批量操作对象&#xff0c;比如将页面上所有的checkbox 都勾上 方法&#xff1a;先用标签选择器选择页面上所有的input&#xff0c;然后从中过滤出属性type是checkbox的 并勾选 List<WebElemen…

Debian linux版本下运行的openmediavault网盘 千兆网卡升级万兆

一、适用场景 1、使用vmware ESXi虚拟化平台运行多种不同应用服务器时&#xff0c;其中网盘服务器采用开源的openmediavault搭建&#xff1b; 2、将老专业服务器升级千兆网为万兆网&#xff1b; 3、需要转移的数据量大的企业或用户&#xff1b; 4、从服务器到服务器的数据转移…

Mac上的Gatekeeper系统跟运行时保护

文章目录 问题&#xff1a;无法打开“xxx.xxx”&#xff0c;因为无法验证开发者。macOS无法验证此App是否包含恶意软件。如何解决&#xff1f; 参考资料门禁运行时保护 问题&#xff1a;无法打开“xxx.xxx”&#xff0c;因为无法验证开发者。macOS无法验证此App是否包含恶意软件…

vue3+vite模版框架 tabs右键刷新时丢失路由参数

问题&#xff1a; 标题栏的tabs的右键&#xff1a;刷新时&#xff0c;没有保存上一个页面传递过来的参数 分析&#xff1a; TagView.vue刷新事件 function refreshSelectedTag(view: TagView) {console.log(|--执行刷新, view)tagsViewStore.delCachedView(view);const {full…

IDEA的使用(概念,安装,配置,)以及什么是字符集,模版

目录 Intellij IDEA IDE的概念 IntelliJ IDEA的安装 IntelliJ IDEA的使用 基本配置 JDK配置 创建Module 基本用法 字体配置 主题配置 字符集 设置IDEA默认字符集 注释模板 字符集 字符集简介 常见字符集 Intellij IDEA 我们不可能一直使用记事本之类变成&#…

Win7共享文件夹无法访问怎么办,win7共享文件夹访问不了

在win7系统中,用户可以通过局域网建立共享,然后将一些文件夹进行共享,方便用户访问。“共享文件”一般常用于办公室,某位同事的计算机用来和其它计算机间相互分享的文件夹。然而有时候需要访问共享文件时,却出现无法访问的情况,要怎么解决这样的问题呢?接下来,小编带来…

3D转换1111

1.三维坐标系 1.3D位移: translate3d(x,y,z)  translform:translateX(100px)&#xff1a;仅仅是在x轴上移动  translform:translateY(100px)&#xff1a;仅仅是在Y轴上移动  translform:translateZ(100px)&#xff1a;仅仅是在Z轴上移动&#xff08;注意&#xff1…

linux yum install jdk如何查找安装目录并配置环境变量

Linux服务已安装jdk1.8&#xff0c;此时需要在该服务器部署的应用要求依赖openJDK11&#xff0c;可按照以下命令进行安装openJDK11。 搜索查看安装包&#xff1a;yum search java-11-openjdk 如果能查找到安装包&#xff0c;执行以下安装命令进行安装。 安装openjdk11&#x…

互联网、因特网、万维网的区别

互联网 internet&#xff1a;凡是能彼此通信的设备组成的网络就叫互联网&#xff0c;即使只有两台计算机&#xff0c;无论以何种技术使其彼此通信&#xff0c;都叫互联网。所以&#xff0c;根据互联网的覆盖规模可以分为&#xff1a; 局域网&#xff08;Local Area Network&am…

Java接口实战:模拟咖啡制作、订购与消费完整流程(day14)

定义接口&#xff1a; // 咖啡制作接口 interface CoffeeMaker { Coffee makeCoffee(String type); } // 咖啡店接口 interface CoffeeShop { void orderCoffee(String type, CoffeeConsumer consumer); } // 咖啡消费者接口 interface CoffeeConsumer { void …

预处理详解(一) -- 预定义符号与#define定义

目录 一. 预定义符号二. #define1.#define定义常量2.#define定义宏3.带有副作用的宏参数4.宏替换的规则5.宏和函数的对比 一. 预定义符号 %s _ _FILE_ _ //文件 %s _ _ DATE_ _ //日期 %s _ _ TIME_ _ //时间 %d _ _ LINE_ _ //行号 %d _ _ STDC_ _ //如果编译器支持 ANSI C,那…

python基础——异常捕获【try-except、else、finally】

&#x1f4dd;前言&#xff1a; 这篇文章主要介绍一下python基础中的异常处理&#xff1a; 1&#xff0c;异常 2&#xff0c;异常的捕获 3&#xff0c;finally语句 &#x1f3ac;个人简介&#xff1a;努力学习ing &#x1f4cb;个人专栏&#xff1a;C语言入门基础以及python入门…

[图解]DDD领域驱动设计伪创新-通用语言01

0 00:00:01,420 --> 00:00:04,110 今天我们来说一个 1 00:00:04,390 --> 00:00:07,450 领域驱动设计的伪创新&#xff0c;通用语言 2 00:00:12,780 --> 00:00:13,960 通用语言 3 00:00:14,460 --> 00:00:14,830 4 00:00:14,830 --> 00:00:15,940 英文叫 5 0…

Python | Leetcode Python题解之第2题两数相加

题目&#xff1a; 题解&#xff1a; # Definition for singly-linked list. # class ListNode: # def __init__(self, val0, nextNone): # self.val val # self.next next class Solution:def addTwoNumbers(self, l1: Optional[ListNode], l2: Optiona…

阿里云服务器经济型e实例特点、适用场景介绍和问题解答

阿里云服务器ECS经济型e系列是阿里云面向个人开发者、学生、小微企业&#xff0c;在中小型网站建设、开发测试、轻量级应用等场景推出的全新入门级云服务器&#xff0c;CPU处理器采用Intel Xeon Platinum架构处理器&#xff0c;支持1:1、1:2、1:4多种处理器内存配比&#xff0c…