I/O多路转接之epoll

news2024/11/30 13:46:41

承接上文 I/O多路转接之poll-CSDN博客

简介

epoll的相关系统调用

epoll底层原理

编写epoll的server

重新回归一下epoll原理,LT,ET

epoll改成ET工作模式 -- 初识(有bug)

epoll初识

按照man手册的说法: 是为处理大批量句柄而作了改进的poll.
它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel 2.5.44)
它几乎具备了之前所说的一切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知方法
 

epoll的相关系统调用

epoll 有3个相关的系统调用,这一点和之前的poll是完全不一样的,还有一点是epoll还是一样只负责等待

epoll_create

int epoll_create(int size);

创建一个epoll的句柄

自从linux2.6.8之后, size参数是被忽略的.
用完之后, 必须调用close()关闭

epoll_ctl

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

epoll的事件注册函数

它不同于select()是在监听事件时告诉内核要监听什么类型的事件, 而是在这里先注册要监听的事件类型;
第一个参数是epoll_create()的返回值(epoll的句柄);
第二个参数表示动作,用三个宏来表示;
第三个参数是需要监听的fd;
第四个参数是告诉内核需要监听什么事;

第二个参数的取值:

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

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

epoll底层原理

当某一进程调用epoll_create方法时, Linux内核会创建一个eventpoll结构体,这个结构体中有两个成员与epoll的使用方式密切相关
 

struct eventpoll{
....
/*红黑树的根节点,这颗树中存储着所有添加到epoll中的需要监控的事件*/
struct rb_root rbr;
/*双链表中则存放着将要通过epoll_wait返回给用户的满足条件的事件*/
struct list_head rdlist;
....
};

1.每一个epoll对象都有一个独立的eventpoll结构体,用于存放通过epoll_ctl方法向epoll对象中添加进来的事件;
2.这些事件都会挂载在红黑树中,如此,重复添加的事件就可以通过红黑树而高效的识别出来(红黑树的插入时间效率是lgn,其中n为树的高度);
3.而所有添加到epoll中的事件都会与设备(网卡)驱动程序建立回调关系,也就是说,当响应的事件发生时会调用这个回调方法;
4.这个回调方法在内核中叫ep_poll_callback,它会将发生的事件添加到rdlist双链表中;
5.在epoll中,对于每一个事件,都会建立一个epitem结构体;

struct epitem{
struct rb_node rbn;//红黑树节点
struct list_head rdllink;//双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
}

当调用epoll_wait检查是否有事件发生时,只需要检查eventpoll对象中的rdlist双链表中是否有epitem元素即可.
如果rdlist不为空,则把发生的事件复制到用户态,同时将事件数量返回给用户. 这个操作的时间复杂度是O(1)

总结一下, epoll的使用过程就是三部曲:

调用epoll_create创建一个epoll句柄;
调用epoll_ctl, 将要监控的文件描述符进行注册;
调用epoll_wait, 等待文件描述符就绪

编写epoll的server

epollServer.cc
#include "epollServer.hpp"
#include <memory>

using namespace std;
using namespace epoll_ns;

static void usage(std::string proc)
{
    std::cerr << "Usage:\n\t" << proc << " port"
              << "\n\n";
}

// 一个简单的回调方法
std::string echo(const std::string &message)
{
    return "I am epollserver, " + message;
}

// ./select_server port
int main(int argc, char *argv[])
{
    if (argc != 2)
    {
        usage(argv[0]);
        exit(USAGE_ERR);
    }
    uint16_t port = atoi(argv[1]);

    unique_ptr<EpollServer> epollsvr(new EpollServer(echo, port));
    epollsvr->initServer();
    epollsvr->start();

    return 0;
}
epollServer.hpp
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <functional>
#include <sys/epoll.h>
#include "err.hpp"
#include "log.hpp"
#include "sock.hpp"

namespace epoll_ns
{
    static const int defaultport = 8888; // 直接预设一个port
    static const int size = 128;         // 只要不小于0就行了,这个参数现在没有用
    static const int defaultvalue = -1;
    static const int defaultnum = 64;

    using func_t = std::function<std::string(const std::string &)>; // 创建一个处理函数

    class EpollServer
    {
    public:
        EpollServer(func_t f, uint16_t port = defaultport,int num = defaultnum) : func_(f), _num(num), _revs(nullptr), _port(port), _listensock(defaultvalue), _epfd(defaultvalue)
        {
        }
        void initServer()
        {
            // 1.创建socket
            _listensock = Sock::Socket();
            Sock::Bind(_listensock, _port);
            Sock::Listen(_listensock);
            // 2.创建epoll模型
            _epfd = epoll_create(size); // 这里的参数没有用
            if (_epfd < 0)
            {
                logMessage(FATAL, "epoll create error: %s", strerror(errno));
                exit(EPOLL_CREATE_ERR);
            }
            // 3.添加listensock到_epfd中去
            struct epoll_event ev;    // 这是一个联合体,目前看成fd使用
            ev.events = EPOLLIN;      // 读事件
            ev.data.fd = _listensock; // 当事件就绪,被重新捞取上来的时候,我们要知道是哪一个fd就绪了!
            epoll_ctl(_epfd, EPOLL_CTL_ADD, _listensock, &ev);

            // 4.申请就绪事件的空间
            _revs = new struct epoll_event[_num];

            logMessage(NORMAL, "init server success");
        }

        // 处理就绪事件
        void HandlerEvent(int readyNum)
        {
            logMessage(DEBUG, "HandlerEvent in");
            for (int i = 0; i < readyNum; ++i)
            {
                uint32_t events = _revs[i].events; // 按照顺序拿取就绪事件,但是我们并不清楚这个事件是谁(分不清是读还是写等等)
                // 这时候联合体作用就体现出来了,因为在初始化的时候我们就已经填进去了
                int sock = _revs[i].data.fd; // 拿到了对应就绪的描述符

                if (sock == _listensock && (events & EPOLLIN))
                {
                    // _listensock读事件就绪, 获取新连接
                    std::string clientip;
                    uint16_t clientport;
                    int fd = Sock::Accept(sock, &clientip, &clientport);
                    if (fd < 0)
                    {
                        logMessage(WARNING, "accept error");
                        continue;
                    }
                    // 获取fd成功,可以直接读取吗?还是不能,因为无法保证是否有数据到来了,所以我们还得继续添加到epoll模型中去
                    struct epoll_event ev;
                    ev.events = EPOLLIN;
                    ev.data.fd = fd;
                    epoll_ctl(_epfd, EPOLL_CTL_ADD, fd, &ev);
                }
                else if (events & EPOLLIN)
                {
                    // 普通的读事件就绪
                    // 注意这里的读取是有很大问题的
                    // 这样直接读取还是有问题,可以保证第一次读取有数据,但是无法保证读取完整
                    // 比如对方的报文差分多份就不可以读取完整了,为了简洁我们先这样读
                    // 打循环也无法保证
                    char buffer[1024];
                    int n = recv(sock, buffer, sizeof(buffer), 0);
                    if (n > 0)
                    {
                        buffer[n] = 0;
                        logMessage(DEBUG, "client# %s", buffer);
                        // TODO -- 这里的发送是有问题的,不能保证发送的条件是就绪的
                        std::string respose = func_(buffer);
                        send(sock, respose.c_str(), respose.size(), 0);
                    }
                    else if (n == 0)
                    {
                        // 这里表示对端把链接关了
                        // 关于这个套接字的所有都得清理掉
                        epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
                        close(sock);
                        logMessage(NORMAL, "client quit");
                    }
                    else
                    {
                        // 出错误了
                        epoll_ctl(_epfd, EPOLL_CTL_DEL, sock, nullptr);
                        close(sock);
                        logMessage(ERROR, "recv error, code: %d, errstring: %s", errno, strerror(errno));
                    }
                }
                else // 自己可以添加其他的条件,比如写事件之类的
                {
                }
            }
            logMessage(DEBUG, "HandlerEvent out");
        }

        void start()
        {
            int timeout = -1;
            for (;;)
            {
                int n = epoll_wait(_epfd, _revs, _num, timeout); // 就绪队列中拿取
                // n有几个就说明拿到了几个
                switch (n)
                {
                case 0:
                    logMessage(NORMAL, "timeout...");
                    break;
                case -1:
                    logMessage(WARNING, "epoll_wait failed, code: %d, errstring: %s", errno, strerror(errno));
                    break;
                default:
                    logMessage(NORMAL, "have event ready");
                    HandlerEvent(n); // 需要知道有几个事件就绪了
                    break;
                }
            }
        }
        ~EpollServer()
        {
            if (_listensock != defaultvalue)
                close(_listensock);
            if (_epfd != defaultvalue)
                close(_epfd);
            if (_revs)
                delete[] _revs;
        }

    private:
        uint16_t _port;
        int _listensock;
        int _epfd;
        struct epoll_event *_revs; // 获取事件
        int _num;
        func_t func_;
    };
}

err.hpp
#pragma once

#include <iostream>

enum
{
    USAGE_ERR = 1,
    SOCKET_ERR,
    BIND_ERR,
    LISTEN_ERR,
    EPOLL_CREATE_ERR
};

log.hpp
#pragma once

#include <iostream>
#include <string>
#include <cstdarg>
#include <ctime>
#include <unistd.h>

#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4

const char * to_levelstr(int level)
{
    switch(level)
    {
        case DEBUG : return "DEBUG";
        case NORMAL: return "NORMAL";
        case WARNING: return "WARNING";
        case ERROR: return "ERROR";
        case FATAL: return "FATAL";
        default : return nullptr;
    }
}

void logMessage(int level, const char *format, ...)
{
#define NUM 1024
    char logprefix[NUM];
    snprintf(logprefix, sizeof(logprefix), "[%s][%ld][pid: %d]",
        to_levelstr(level), (long int)time(nullptr), getpid());

    char logcontent[NUM];
    va_list arg;
    va_start(arg, format);
    vsnprintf(logcontent, sizeof(logcontent), format, arg);

    std::cout << logprefix << logcontent << std::endl;
}

makefile
epoll_server: epollServer.cc
	g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
	rm -f epoll_server

sock.hpp
#pragma once

#include <iostream>
#include <string>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"
#include "err.hpp"

class Sock
{
    const static int backlog = 32;

public:
    static int Socket()
    {
        // 1. 创建socket文件套接字对象
        int sock = socket(AF_INET, SOCK_STREAM, 0);
        if (sock < 0)
        {
            logMessage(FATAL, "create socket error");
            exit(SOCKET_ERR);
        }
        logMessage(NORMAL, "create socket success: %d", sock);

        int opt = 1;
        setsockopt(sock, SOL_SOCKET, SO_REUSEADDR|SO_REUSEPORT, &opt, sizeof(opt));
        return sock;
    }

    static void Bind(int sock, int port)
    {
        // 2. bind绑定自己的网络信息
        struct sockaddr_in local;
        memset(&local, 0, sizeof(local));
        local.sin_family = AF_INET;
        local.sin_port = htons(port);
        local.sin_addr.s_addr = INADDR_ANY;
        if (bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0)
        {
            logMessage(FATAL, "bind socket error");
            exit(BIND_ERR);
        }
        logMessage(NORMAL, "bind socket success");
    }

    static void Listen(int sock)
    {
        // 3. 设置socket 为监听状态
        if (listen(sock, backlog) < 0) // 第二个参数backlog后面在填这个坑
        {
            logMessage(FATAL, "listen socket error");
            exit(LISTEN_ERR);
        }
        logMessage(NORMAL, "listen socket success");
    }

    static int Accept(int listensock, std::string *clientip, uint16_t *clientport)
    {
        struct sockaddr_in peer;
        socklen_t len = sizeof(peer);
        int sock = accept(listensock, (struct sockaddr *)&peer, &len);
        if (sock < 0)
            logMessage(ERROR, "accept error, next");
        else
        {
            logMessage(NORMAL, "accept a new link success, get new sock: %d", sock); // ?
            *clientip = inet_ntoa(peer.sin_addr);
            *clientport = ntohs(peer.sin_port);
        }

        return sock;
    }
};

至此一个简单的epoll服务器就完成了,代码中提及了种种问题,我们得到下一文章才能更好的解决了

重新回归一下epoll原理,LT,ET

因为select/poll/epoll的默认通知机制都是LT模式!

epoll改成ET工作模式

 

预告

如何正确的处理I0+协议定制+业务逻辑=Reactor

基于ET模式下的Reactor,处理所有的IO

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

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

相关文章

所有网站文本复制方法(知网、百度文库等)

实现文字复制自由 在肝学校结课论文是发现要用的论文文字都无法复制&#xff0c;所以想到了以前学到的一个方法&#xff0c;开始忘了&#xff0c;突然想起来&#xff0c;现在记录一下。 拿我现在肝的课程结课论文为例&#xff1a; 方法一 在谷歌浏览器的网址前面有一个列表一…

WebSocket了解

一.什么是WebSocket WebSocket是HTML5下一种新的协议&#xff08;websocket协议本质上是一个基于tcp的协议&#xff09;它实现了浏览器与服务器全双工通信&#xff0c;能更好的节省服务器资源和带宽并达到实时通讯的目的Websocket是一个持久化的协议 二.websocket的原理 web…

持续集成部署-k8s-配置与存储-存储类:动态创建NFS-PV案例

动态创建NFS-PV案例 1. 前置条件2. StorageClass 存储类的概念和使用3. RBAC 配置4. storageClass 配置5. 创建应用&#xff0c;测试 PVC 的自动配置6. 解决 PVC 为 Pending 状态问题7. 单独测试自动创建 PVC 1. 前置条件 这里使用 NFS 存储的方式&#xff0c;来演示动态创建 …

[NOIP2006]明明的随机数

一、题目 登录—专业IT笔试面试备考平台_牛客网 二、代码 set去重&#xff0c;再利用vector进行排序 std::set是一个自带排序功能的容器&#xff0c;它已经按照一定的规则&#xff08;默认是元素的小于比较&#xff09;对元素进行了排序。因此&#xff0c;你不能直接对std::s…

2.5 逆矩阵

一、逆矩阵的注释 假设 A A A 是一个方阵&#xff0c;其逆矩阵 A − 1 A^{-1} A−1 与它的大小相同&#xff0c; A − 1 A I A^{-1}AI A−1AI。 A A A 与 A − 1 A^{-1} A−1 会做相反的事情。它们的乘积是单位矩阵 —— 对向量无影响&#xff0c;所以 A − 1 A x x A^{…

【挑战业余一周拿证】一、亚马逊云科技简介 - 第 2 节 - 模块 简介

CSDN 官方中文视频&#xff08;免费&#xff09;&#xff1a;点击进入 第 2 节 - 模块 1 简介 这门课程将为您提供需要了解的所有重要信息&#xff0c;让您能够轻松讨论亚马逊云科技并了解它为 何对您的企业有利 亚马逊云科技为每个企业都提供了非常广泛的服务&#xff0c;从…

Vscode工具使用指南

通用 快捷键文件 / 编辑查找 / 替换窗口插件主题 连接linux 快捷键 文件 / 编辑 新建文件&#xff1a;CtrlN放大或缩小&#xff1a;Ctrl /-代码行缩进&#xff0c;展开&#xff1a;Ctrl[ 和 Ctrl]在当前行下方插入一行&#xff1a;CtrlEnter在当前行上方插入一行&#xff1a;…

Springboot+vue的客户关系管理系统(有报告),Javaee项目,springboot vue前后端分离项目

演示视频&#xff1a; Springbootvue的客户关系管理系统&#xff08;有报告&#xff09;&#xff0c;Javaee项目&#xff0c;springboot vue前后端分离项目 项目介绍&#xff1a; 本文设计了一个基于Springbootvue的前后端分离的客户关系管理系统&#xff0c;采用M&#xff08…

常见的1/2/3位数码管接线详解

今天玩数码管的时候接触到了数码管的接线&#xff0c;分享一下供刚开始接触的童鞋参考 首先了解什么是数码管 数码管是一种可以显示数字和其他信息的电子设备&#xff0c;是显示屏其中一类&#xff0c; 通过对其不同的管脚输入相对的电流&#xff0c;会使其发亮&#xff0c;从而…

VMware上面安装部署centos7镜像系统【详细含镜像】

VMware上面安装部署centos7镜像系统【详细含镜像】 废话不多说直接开始 下载centos7镜像 网上有好多&#xff0c;但是我相信来看小编文章的基本上应该都有centos7的镜像了吧&#xff0c;毕竟咱们都是同一类人&#xff0c;哈哈不卖关子了&#xff0c;小编直接给大家一个百度云盘…

意大利 地图 RV REG M A 是什么意思

Regionale分为 普通列车&#xff08;R/REG)&#xff1a;经停每一个小站 高速列车&#xff08;RV/RGV)&#xff1a;经停大站 上车前必须打票 没有指定座位&#xff0c;任意选座 M是地铁&#xff0c;A表示A线 sit bus shuttle是机场大巴

如何清除redis缓存?

首先进入redis安装目录 当前目录下执行CMD命令&#xff08;shift 右键 -> 选择 ‘在此处打开Powershell窗口’ &#xff09; 执行 redis-cli.exe -h 127.0.0.1 -p 6379flushall

好物周刊#31:在线格式转换

https://github.com/cunyu1943/JavaPark https://yuque.com/cunyu1943 村雨遥的好物周刊&#xff0c;记录每周看到的有价值的信息&#xff0c;主要针对计算机领域&#xff0c;每周五发布。 一、项目 1. Soybean Admin 一个基于 Vue3、Vite3、TypeScript、NaiveUI、Pinia 和…

5.一维数组——输入一行字符,统计其中各个大写字母出现的次数。

文章目录 前言一、题目描述 二、题目分析 三、解题 程序运行代码 四、举一反三一、题目描述 二、题目分析 三、解题 程序运行代码 前言 本系列为一维数组编程题&#xff0c;点滴成长&#xff0c;一起逆袭。 一、题目描述 输入一行字符&#xff0c;统计其中各个大写字母出现的…

北大师兄聊一聊基于深度学习的三维重建方法

2019年4月15日下午6时50分左右,一场大火席卷了法国巴黎圣母院,持续长达14小时。幸而巴黎圣母院有着高分辨率的3D模型,研究人员可以了解圣母院本身的建造结构,以便修复工程的开展。 多视图立体几何(Multi-View Stereo,MVS)是计算机视觉中重要的研究领域,也是迈入三维计算…

jQuery_05 事件的绑定(尝试)

jQuery可以给dom对象添加事件 在程序执行期间动态的处理事件 jQuery如何绑定事件呢&#xff1f; 1. $("选择器").事件名称(事件处理函数) $("选择器") &#xff1a; 选择0或者多个dom对象 给他们添加事件 事件名称&#xff1a;就是js中事件名称去掉on的部…

力扣113. 路径总和 II(Java,DFS解法)

Problem: 113. 路径总和 II 文章目录 题目描述思路解题方法复杂度代码实现细节处Code 题目描述 给你二叉树的根节点 root 和一个整数目标和 targetSum &#xff0c;找出所有 从根节点到叶子节点 路径总和等于给定目标和的路径。 叶子节点 是指没有子节点的节点。 思路 题目要…

YOLOv5算法进阶改进(4)— 引入解耦合头部 | 助力提高检测准确率

前言:Hello大家好,我是小哥谈。解耦头是目标检测中的一种头部设计,用于从检测网络的特征图中提取目标位置和类别信息。具体来说,解耦头部将目标检测任务分解为两个子任务:分类和回归。分类任务用于预测目标的类别,回归任务用于预测目标的位置。这种设计可以提高目标检测的…

Google Docs系统设计

1 简介 谷歌文档是一种协作文档编辑服务。 协作文档编辑服务可以通过两种方式设计&#xff1a; 设计为C/S架构的集中式设施,为所有用户提供文档编辑服务使用点对点技术设计&#xff0c;以便在单个文档上协作 大多数商业解决方案侧重于客户端服务体系结构&#xff0c;以实现…

物联网AI 无线连接学习之蓝牙基础篇 协议概述

学物联网&#xff0c;来万物简单IoT物联网&#xff01;&#xff01; 1 蓝牙协议总体架构 1.1 Application层 应用属性层&#xff0c;通过API函数与协议栈交互&#xff1b; 1.2 Host层 Host层&#xff0c;逻辑链路控制及自适应协议层、安全管理层、属性协议层、通用访问配置…