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

文章目录

  • 1、理解Epoll和对应接口
  • 2、简单实现


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个接口。

在这里插入图片描述

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

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

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.ccg++ -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 ...");elselogMessage(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 ...");elselogMessage(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

结束。

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

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

相关文章

SQL-窗口函数

什么是窗口函数 可以像聚合函数一样对一组数据进行分析并返回结果&#xff0c;二者的不同之处在于&#xff0c;窗口函数不是将一组数据汇总成单个结果&#xff0c;而是为每一行数据都返回一个结果。 窗口函数组成部分 1.创建数据分区 窗口函数OVER子句中的PARTITION BY选项用…

拼多多无货源中转仓项目真的靠谱吗?发展前景如何?

阿阳最近一直在关注无货源电商这一块&#xff0c;尤其是拼多多无货源中转仓&#xff0c; 现如今也有了自己的运营团队和交付团队&#xff0c;整体来看这个项目还算不错&#xff01; 说实话&#xff0c;在考察这个项目的时候&#xff0c;看到市面上很多人在做&#xff0c;包括我…

一、VTK 9.0.0 编译安装步骤 VS2019 CMake3.26.0

零基础开始学习VTK &#xff0c;请跟我进行第一步&#xff0c;配置好开放环境&#xff01; 首先&#xff0c;你时间比较紧急&#xff0c;想直接使用VTK &#xff0c;而无需编译、那么请使用 PCL-1.12.0-AllInOne-msvc2019-win64.exe 它已经帮你编译好VTK 9 了&#xff0c;直…

echarts柱状图顶部设置倾斜并且展示数字

将下面代码直接复制粘贴在此运行就能查看效果Apache ECharts&#xff0c;一款基于JavaScript的数据可视化图表库&#xff0c;提供直观&#xff0c;生动&#xff0c;可交互&#xff0c;可个性化定制的数据可视化图表。https://echarts.apache.org/examples/zh/editor.html?care…

AI制作《流浪地球3》高清宣传片

AI制作《流浪地球3》高清宣传片 星辰大海&#xff0c;再次启航&#xff0c;人类的冒险&#xff0c;永无止境。The vast expanse of stars and oceans, setting sail once again. Human adventure knows no bounds. 当家园变得遥不可及&#xff0c;我们唯有勇往直前。With our …

Docker部署Dubbo-Admin浏览器无法访问问题!

Dubbo配置开发环境 很多小伙伴在使用docker部署Dubbo-Admin会出现浏览器无法访问问题&#xff0c;但是虚拟机防火墙都是关着的&#xff0c;那么这可能是镜像源出现问题了&#xff0c;可以按照如下方法操作&#xff1a; 先将现有的镜像和容器全部删除&#xff08;配置完镜像源需…

[Python] 如何通过ctypes库来调用C++ 动态库 DLL?

ctypes库介绍 ctypes是Python的一个外部库,它提供了一种灵活的方式来调用C语言的动态链接库(DLL)或共享库(SO)。通过ctypes,我们可以在Python中直接调用C语言编写的函数和变量,从而实现跨语言的互操作。 ctypes 它提供了与 C 兼容的数据类型,并允许调用 DLL 或共享库中的…

【Internet Protocol】ip介绍,如何组局域网实现远程桌面和文件共享

文章目录 1.何为“上网”1.1 定义1.2 为什么连了WiFi就能上网了&#xff1f; 2.ip2.1 什么是ip2.2 为什么区分广域网和局域网&#xff0c;ip的唯一性2.3 如何查看设备的ip2.4 什么叫"ping"2.5 区分是否两个ip是否在同一局域网2.5.1 最稳妥的方式&#xff1a;ip&m…

第 2 章 数据结构和算法概述

文章目录 2.1 数据结构和算法的关系2.2 看几个实际编程中遇到的问题2.2.1 问题一-字符串替换问题2.2.2 一个五子棋程序2.2.3 约瑟夫(Josephu)问题(丢手帕问题)2.2.4 其它常见算法问题: 2.3 线性结构和非线性结构2.3.1 线性结构2.3.2 非线性结构 2.1 数据结构和算法的关系 数据 …

Javascript,到底要不要写分号?

小白随机在互联网上乱丢一些赛博垃圾&#xff0c;还望拨冗批评斧正。 要不要加分号&#xff1f; 先说结论&#xff1a;“不引起程序出错的前提下&#xff0c;加不加都可以&#xff0c;按自身习惯来。” 为什么JS可以不加分号&#xff1f; 实际上&#xff0c;行尾使用分号的风…

五、基础篇 vue列表渲染

在v-for里使用对象用 v-for 把一个数组对应为一组元素 我们可以用 v-for 指令基于一个数组来渲染一个列表。v-for 指令需要使用 item in list形式的特殊语法&#xff0c;其中 list是源数据数组&#xff0c;而 item 则是被迭代的数组元素的别名。 <template><div clas…

【leetcode题解C++】54.螺旋矩阵I and 59.螺旋矩阵II

54.螺旋矩阵I 给你一个 m 行 n 列的矩阵 matrix &#xff0c;请按照 顺时针螺旋顺序 &#xff0c;返回矩阵中的所有元素。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a;[1,2,3,6,9,8,7,4,5]示例 2&#xff1a; 输入&#xff1a;m…

C# 实现单线程异步互斥锁

文章目录 前言一、异步互斥锁的作用是什么&#xff1f;示例一、创建和销毁 二、如何实现&#xff1f;1、标识&#xff08;1&#xff09;标识是否锁住&#xff08;2&#xff09;加锁&#xff08;3&#xff09;解锁 2、异步通知&#xff08;1&#xff09;创建对象&#xff08;2&a…

Python单元测试之pytest的使用

一、前提准备 1、前提&#xff1a;需要安装pytest和pytest-html(生成html测试报告&#xff09; pip install pytest 和 pip install pytest-html 安装插件&#xff1a;pip install 插件名 2、命名规范 Pytest单元测试中的类名和方法名必须是以test开头,执行中只能找到test开…

Spring(19) ThreadPoolTaskExecutor 线程池的使用

目录 一、线程池简介1.1 为什么使用线程池1.2 线程池为什么需要使用队列1.3 线程池为什么要使用阻塞队列而不是用非阻塞队列1.4 如何配置线程池1.5 execute() 和 submit() 方法 二、ThreadPoolTaskExecutor 线程池简介2.1 简介2.2 核心参数配置2.3 ThreadPoolTaskExecutor 内部…

数据库作业三

1.创建student和score表 2.为student表和score表增加记录 3.查询student表的所有记录 4.查询student表的第2条到4条记录 5.从student表查询所有学生的学号&#xff08;id&#xff09;、姓名&#xff08;name&#xff09;和院系&#xff08;department&#xff09;的信息 6.从st…

flutter3使用dio库发送FormData数据格式时候的坑,和get库冲突解决办法

问题描述 问题1&#xff1a;当你使用FormData.from(Flutter3直接不能用)的时候&#xff0c;可能会提示没有这个方法&#xff0c;或者使用FormData.fromMap(flutter3的dio支持)的时候也提示没有&#xff0c;这时候可能就是和get库里面的Formdata冲突了 问题1&#xff1a;The me…

vue基于spring boot框架的发艺美发店理发店管理系统的设计q9xpe

店铺信息、美发信息是发艺美发店管理系统的重要组成部分&#xff0c;信息清晰、详细、准确&#xff0c;能够有效地促进发艺美发店管理系统的运行[5]。基础设定函数是对整个系统的总体布局进行合理安排&#xff0c;包括&#xff1a;店铺活动、物品信息、领用信息等。通过对各类资…

【C++】vector模拟实现过程中值得注意的点

Hello大家好&#xff01;我是咕噜的铁蛋&#xff01;C中的vector是一种动态数组&#xff0c;它能够根据需要自动增长和缩小。虽然C标准库已经为我们提供了vector的实现&#xff0c;但在某些情况下&#xff0c;我们可能需要自己模拟实现一个类似于vector的数据结构。今天铁蛋将给…

CPU密集型计算、IO密集型计算、多进程、多线程

参考链接&#xff1a; 使用多进程multiprocessing模块加速程序的运行_哔哩哔哩_bilibili 什么是CPU密集型计算、IO密集型计算&#xff1a; CPU密集型&#xff1a; CPU密集型也叫计算密集型&#xff0c;是指I/O在很短的时间就可以完成&#xff0c;CPU需要大量的计算和处理&a…