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.hqwc.cn/news/412616.html

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

相关文章

关键信息基础设施安全相关材料汇总

文章目录 前言一、法律(1)《中华人民共和国国家安全法》(2)《中华人民共和国网络安全法》(3) 《中华人民共和国密码法》(4)《中华人民共和国数据安全法》(5) 《中华人民共和国个人信息保护法》二、行政法规(6)《中华人民共和国保守国家秘密法实施条例》(7) 《关键信息基础设施安…

圈小猫游戏HTML源码

源码介绍 圈小猫游戏html源码&#xff0c;HTMLCSSJS,记事本可以打开修改内容&#xff0c;电脑本地双击index.html即可运行&#xff0c;也可以上传到服务器上面运行&#xff0c;喜欢的同学可以拿去使用 下载地址 蓝奏云&#xff1a;https://wfr.lanzout.com/iFkVc1lb5akj CS…

IP定位助力网络安全防线

随着互联网技术的飞速发展&#xff0c;网络安全问题日益凸显。在网络安全领域&#xff0c;IP地址定位技术正发挥着越来越重要的作用&#xff0c;成为维护网络安全的一道有力防线。 一、追踪黑客攻击者&#xff0c;维护公共安全 在网络安全领域&#xff0c;黑客攻击是一个严重的…

unocss+iconify技术在vue项目中使用20000+的图标

安装依赖 npm i unocss iconify/json配置依赖 vue.config.js文件 uno.config.js文件 main.js文件 使用 <i class"i-fa:user"></i> <i class"i-fa:key"></i>class名是 i- 开头&#xff0c;跟库名:图标名&#xff0c;那都有什么库…

LRU Cache

文章目录 1. 什么是LRU Cache2. LRU Cache的实现3. LRU Cache的OJ题目分析AC代码 1. 什么是LRU Cache LRU是Least Recently Used的缩写&#xff0c;意思是最近最少使用&#xff0c;它是一种Cache替换算法。 什么是Cache&#xff1f; 狭义的Cache指的是位于CPU和主存间的快速RAM…

Mybatis之关联

一、一对多关联 eg&#xff1a;一个用户对应多个订单 建表语句 CREATE TABLE t_customer (customer_id INT NOT NULL AUTO_INCREMENT, customer_name CHAR(100), PRIMARY KEY (customer_id) ); CREATE TABLE t_order ( order_id INT NOT NULL AUTO_INCREMENT, order_name C…

burp靶场--ssrf

burp靶场–ssrf 1.什么是ssrf 服务器端请求伪造是一种 Web 安全漏洞&#xff0c;允许攻击者导致服务器端应用程序向非预期位置发出请求。 在典型的 SSRF 攻击中&#xff0c;攻击者可能会导致服务器连接到组织基础设施内的仅供内部使用的服务。在其他情况下&#xff0c;他们可…

LeetCode 104. 二叉树的最大深度

104. 二叉树的最大深度 给定一个二叉树 root &#xff0c;返回其最大深度。 二叉树的 最大深度 是指从根节点到最远叶子节点的最长路径上的节点数。 示例 1&#xff1a; 输入&#xff1a;root [3,9,20,null,null,15,7] 输出&#xff1a;3示例 2&#xff1a; 输入&#xff1…

SpringBoot中整合MybatisPlus快速实现Mysql增删改查和条件构造器

场景 Mybatis-Plus(简称MP)是一个Mybatis的增强工具&#xff0c;只是在Mybatis的基础上做了增强却不做改变&#xff0c;MyBatis-Plus支持所有Mybatis原生的特性&#xff0c; 所以引入Mybatis-Plus不会对现有的Mybatis构架产生任何影响。MyBatis 增强工具包&#xff0c;简化 C…

短视频代运营抖音项目规划管理计划模板

【干货资料持续更新&#xff0c;以防走丢】 短视频代运营抖音项目规划管理计划模板 部分资料预览 资料部分是网络整理&#xff0c;仅供学习参考。 短视频代运营模板&#xff08;完整资料包含以下内容&#xff09; 目录 具体的表格设计和内容可能因不同的情况和需求而有所变…

重拾计网-第一弹

&#x1f389;欢迎您来到我的MySQL基础复习专栏 ☆* o(≧▽≦)o *☆哈喽~我是小小恶斯法克&#x1f379; ✨博客主页&#xff1a;小小恶斯法克的博客 &#x1f388;该系列文章专栏&#xff1a;重拾计算机网络 &#x1f379;文章作者技术和水平很有限&#xff0c;如果文中出现错…

BIOS知识枝桠——RAID 磁盘阵列

文章目录 前言一、RAID介绍二、RAID等级分类1.RAID02.RAID13.RAID24.RAID3和RAID45.RAID5和RAID66.RAID77.RAID10 BIOS下组建RAID 前言 假设存在多块磁盘&#xff0c;如果不组建阵列&#xff0c;磁盘与磁盘之间是没有任何关系的。磁盘A和B&#xff0c;放在A中的文件与B磁盘没有…