muduo网络库剖析——监听者EpollPoller类

muduo网络库剖析——监听者EpollPoller类

  • 前情
    • 从muduo到my_muduo
  • 概要
    • epoll原理解析
    • epoll提供的接口
    • epoll的触发模式
    • epoll实现多路复用
  • 框架与细节
    • 成员
    • 函数
    • 使用方法
  • 源码
  • 结尾

前情

从muduo到my_muduo

作为一个宏大的、功能健全的muduo库,考虑的肯定是众多情况是否可以高效满足;而作为学习者,我们需要抽取其中的精华进行简要实现,这要求我们足够了解muduo库。

做项目 = 模仿 + 修改,不要担心自己学了也不会写怎么办,重要的是积累,学到了这些方法,如果下次在遇到通用需求的时候你能够回想起之前的解决方法就够了。送上一段话!

在这里插入图片描述

概要

转自夏天匆匆2过。

epoll原理解析

从socket接收网络数据说起:
1、网络传输中,网卡会把接收到的数据写入内存,网卡向 CPU 发出一个中断信号,操作系统便能得知有新数据到来,再通过网卡中断程序去处理数据。
2、进程执行socket()函数创建socket,这个socket 对象包含了发送缓冲区、接收缓冲区与等待队列等成员,等待队列指向所有需要等待该 Socket 事件的进程。
3、假设上面socket进程为A,另外内核还有进程B和C,内核会分时执行运行状态的ABC进程。
4、当程序执行到 Recv 时,操作系统会将进程 A 从工作队列移动到该 Socket 的等待队列中,A进程被阻塞,不会往下执行代码,也就不会占用CPU资源,此时内核只剩B和C进程分时执行。
5、一个socket 对应着一个端口号,而网络数据包中包含了 IP 和端口的信息,内核可以通过端口号找到对应的socket。
6、当socket 接收到数据后,操作系统将该socket 等待队列上的进程重新放回到工作队列,该进程变成运行状态,继续执行代码。同时由于 socket 的接收缓冲区已经有了数据,Recv 可以返回接收到的数据。

epoll的设计思路:
服务服务器需要管理多个客户端连接,而Recv 只能监视单个socket,epoll 的诞生就是高效地监视多个socket。
epoll是select 和poll的增强版本,epoll的改进:
1、epoll将“维护等待队列”和“阻塞进程“分离,先用 epoll_create 创建一个epoll 对象 Epfd,再通过 epoll_ctl 将需要监视的socket 添加到 Epfd 中,最后调用 epoll_wait 等待数据。
2、内核维护一个“就绪列表”Rdlist ,引用收到数据的 Socket,当进程被唤醒后,只要获取 Rdlist 的内容,就能够知道哪些 Socket 收到数据。

epoll的工作流程
1、当某个进程调用 epoll_create 方法时,内核会创建一个 eventpoll 对象(Epfd),eventpoll 对象是文件系统中的一员,有等待队列。Rdlist 是eventpoll的成员。
2、创建 Epoll 对象后,可以用 epoll_ctl 添加或删除所要监听的 Socket,内核会将 eventpoll 添加到这个 Socket 的等待队列中。当 Socket 收到数据后,中断程序会操作 eventpoll 对象,而不是直接操作进程。
3、当 Socket 收到数据后,中断程序会给 eventpoll 的就绪列表Rdlist 添加这个Socket 引用。eventpoll 对象相当于 Socket 和进程之间的中介,Socket 的数据接收并不直接影响进程,而是通过改变 eventpoll 的就绪列表来改变进程状态。当程序执行到 epoll_wait 时,如果 Rdlist 已经引用了 Socket,那么 epoll_wait 直接返回,如果 Rdlist 为空,阻塞进程。
4、假设计算机正在运行进程 A 和进程 B,在某时刻进程 A 运行到了 epoll_wait 语句。 内核会将进程 A 放入 eventpoll 的等待队列中,阻塞进程。当 Socket 接收到数据,中断程序一方面修改 Rdlist,另一方面唤醒 eventpoll 等待队列中的进程,进程 A 再次进入运行状态。因为 Rdlist 的存在,进程 A 可以知道哪些 Socket 发生了变化。

epoll数据结构
eventpoll结构体包含了 Lock、MTX、WQ(等待队列)与 Rdlist 等成员。
就绪列表Rdlist:是一种能够快速插入和删除的数据结构,Epoll 使用双向链表来实现就绪队列。
索引结构RBR:epoll使用红黑树作为索引结构来保存监听的socket列表。

在这里插入图片描述

epoll提供的接口

1、调用epoll_create建立epoll对象,创建一个eventpoll结构体,包括rbr(在内核cache里创建红黑树用于存储以后epoll_ctl传来的socket)和rdllist(用于存储准备就绪事件的向链表)。

//创建一个epoll实例(本质是红黑树),也占用个文件描述符,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
//返回值size,用来告诉内核这个监听的数目一共有多大,自从Linux 2.6.8开始,size参数被忽略,但是依然要大于0。
int epoll_create(int size);
struct eventpoll {.../*红黑树的根节点,这棵树中存储着所有添加到epoll中的事件,也就是这个epoll监控的事件*/struct rb_root rbr;/*双向链表rdllist保存着将要通过epoll_wait返回给用户的、满足条件的事件*/struct list_head rdllist;...
};

2、调用epoll_ctl向epoll对象中添加或删除socket事件,所有添加到epoll中的事件都会与设备(如网卡)驱动程序建立回调关系,向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。

/*** @brief 将监听的文件描述符添加到epoll对象中* @param epfd epoll_create的返回值,epoll对象* @param op   要执行的动作:EPOLL_CTL_ADD:注册新的fd到epfd中;EPOLL_CTL_MOD:修改已经注册的fd的监听事件;EPOLL_CTL_DEL:从epfd中删除一个fd;* @param fd   要执行动作的fd* @param event告诉内核需要监听什么事件,epoll_event结构体:*     struct epoll_event {__uint32_t events; // Epoll eventsepoll_data_t data; // User data variable};events可以是以下几个宏的集合(常用的IN/OUT/ERR/ET):EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);EPOLLOUT:表示对应的文件描述符可以写;EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);EPOLLERR:表示对应的文件描述符发生错误;EPOLLHUP:表示对应的文件描述符被挂断;EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。epoll_data_t联合体定义如下:(注意是联合体)typedef union epoll_data{void *ptr;		//可以传递任意类型数据,常用来传 回调函数int fd;		//可以直接传递客户端的fduint32_t u32;uint64_t u64;} epoll_data_t;* @return 返回值:成功返回0。发生错误时返回-1并设置errno*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 

3、当epoll_wait调用时,观察rdllist双向链表里有没有数据。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。

/*** @brief           等待epoll事件从epoll实例中发生* @param epfd      等待的监听描述符,也就是哪个池子中的内容* @param events    出参,指针,指向epoll_event的数组,监听描述符中的连接描述符就绪后,将会依次将信息填入* @param maxevents 表示每次能处理的最大事件数,告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size* @param timeout   等待时间,要是有连接描述符就绪,立马返回,如果没有,timeout时间后也返回,单位是ms;(超时情况下,0会立即返回,-1将不确定,也有说法说是永久阻塞)* @return          成功返回为请求的I / O准备就绪的文件描述符的数目,如果在请求的超时毫秒内没有文件描述符准备就绪,则返回零。发生错误时,epoll_wait()返回-1并正确设置errno。*/
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); 

epoll的触发模式

epoll的两种触发模式:
边沿触发vs水平触发
epoll事件有两种模型,边沿触发:edge-triggered (EPOLLET), 水平触发:level-triggered (EPOLLLT)
水平触发(level-triggered),是epoll的默认模式
socket接收缓冲区不为空 有数据可读 读事件一直触发
socket发送缓冲区不满 可以继续写入数据 写事件一直触发
边沿触发(edge-triggered)
socket的接收缓冲区状态变化时触发读事件,即空的接收缓冲区刚接收到数据时触发读事件
socket的发送缓冲区状态变化时触发写事件,即满的缓冲区刚空出空间时触发读事件
边沿触发仅触发一次,水平触发会一直触发。
开源库:libevent 采用水平触发, nginx 采用边沿触发。

epoll实现多路复用

使用一个进程(线程)同时监控若干个文件描述符读写情况,这种读写模式称为多路复用。
多用于TCP的服务端,用于监控客户端的连接和数据的发送。
优点:不需要频繁地创建、销毁进程,从而节约了内存资源、时间资源,也避免了进程之间的竞争、等待。
缺点:要求单个客户端的任务不能太过于耗时,否则其它客户端就会感知到卡顿。
适合并发量高、但是任务量短小的情景,例如:Web服务器。

epoll就是为实现多路复用而生,一个epoll线程可同时监听多个fd收发、tcp服务监听、异常事件监听等。

框架与细节

对于EpollPoller,主要是使用epoll家族来进行监听与对channel的控制。

成员

在这里插入图片描述
创建要用到的epoll文件描述符,以及events的监听事件列表。

函数

在这里插入图片描述
epoll_create1可以传入一个flag,这里调用EPOLL_CLOEXEC,和SOCK_CLOEXEC一样,关闭新进程的继承效果。
在这里插入图片描述
析构重写,调用close函数,关闭epoll文件描述符。

在poll函数中,主要使用了epoll_wait函数监听准备好的事件,以及调用了fillactiveChannels来准备激活的channel列表。下面是对epoll_wait函数的一段具体解释。并且给epoll_wait函数设定了timeOut时间,超过该时间就结束等待,返回相应的值。
在这里插入图片描述
对于updatechannel函数,给channel设置了三种状态,kNew,kAdded,kDeleted,分别代表未注册到Poller上,已注册到Poller上,已从Poller上删除。针对这三种状态,对相应的哈希表进行修改。在这里我对为什么muduo源码选择实现了vector的channel列表和哈希表的channel列表有一些理解。vector其实是监听到的激活的channel通道集合,哈希表则是是否这个channel还注册在Poller上面,或者是已经从Poller上消失了。那这么看可能vector的size会比哈希表的小,虽然这只是猜测,没有验证过。对于相应的事件,会调用update去更新通道。
在这里插入图片描述
removechannel其实也是对哈希表的channel通道集合进行一些处理,包括状态的转换。
在这里插入图片描述
对于update,就是更改channel对应的event。
在这里插入图片描述
fillactivechannels就是建立监听到的events列表与channel列表之间的联系,这样channel在之后的更新状态或删除都可以访问到对应的event。
在这里插入图片描述

使用方法

源码

//EpollPoller.h
#pragma once#include <sys/epoll.h>#include "Poller.h"
#include "EventLoop.h"
#include "string.h"
#include "Log.h"class Channel;class EpollPoller : public Poller {
public:EpollPoller(EventLoop* loop);~EpollPoller() override;// 重写父类的函数Timestamp poll(int timeoutMs, ChannelList* activeChannels) override;void updateChannel(Channel* channel) override;void removeChannel(Channel* channel) override;private:static const int kInitEventListSize = 16;using EventList = std::vector<epoll_event>; //自己用,为私有void update(int operation, Channel* channel);void fillActiveChannels(int numEvents, ChannelList* activeChannels) const;int epollfd_;EventList events_;
};//EpollPoller.cc
#include "EpollPoller.h"//实现channel与epoll_event一一映射enum status {kNew, //channel 未添加到 Poller 中kAdded,     //channel 已添加到 Poller 中kDeleted,    //channel 从 Poller 中删除
};EpollPoller::EpollPoller(EventLoop* loop) : Poller(loop), epollfd_(::epoll_create1(EPOLL_CLOEXEC)), events_(kInitEventListSize) {if (epollfd_ < 0) {LOG_FATAL("%s--%s--%d--%d : epoll_create error\n", __FILE__, __FUNCTION__, __LINE__, errno);}
}EpollPoller::~EpollPoller() {::close(epollfd_);
}Timestamp EpollPoller::poll(int timeoutMs, ChannelList* activeChannels) {   //设置channel感兴趣的事件int numEvent = ::epoll_wait(epollfd_, &*events_.begin(), events_.size(), timeoutMs);Timestamp now = Timestamp::now();int saveErrno = errno;if (numEvent < 0) {if (saveErrno != EINTR) { //中断errno = saveErrno;LOG_FATAL("%s--%s--%d--%d : epoll_wait error\n", __FILE__, __FUNCTION__, __LINE__, errno);}}else if (numEvent == 0) {LOG_INFO("%s--%s--%d : epoll_wait timeout\n", __FILE__, __FUNCTION__, __LINE__);}else {LOG_INFO("%s--%s--%d : epoll_wait %d events happened\n", __FILE__, __FUNCTION__, __LINE__, numEvent);fillActiveChannels(numEvent, activeChannels);if (numEvent == events_.size()) {events_.resize(numEvent * 2);}}return now;
}void EpollPoller::updateChannel(Channel* channel) { //通过改变channel来改变对应的epoll_eventint status = channel->status();if (status == kNew || status == kDeleted) {if (status == kNew) {int fd = channel->fd();channels_[fd] = channel;}channel->set_status(kAdded);update(EPOLL_CTL_ADD, channel);}else {  //channel已注册到Poller上了int fd = channel->fd();if (channel->isNoneEvent()) {update(EPOLL_CTL_DEL, channel);channel->set_status(kDeleted);  //只是不监听了}else {update(EPOLL_CTL_MOD, channel);}}
}void EpollPoller::removeChannel(Channel* channel) {int fd = channel->fd();channels_.erase(fd);int status = channel->status();if (status == kAdded) {update(EPOLL_CTL_DEL, channel);}channel->set_status(kNew);
}void EpollPoller::update(int operation, Channel* channel) { //epoll_ctl,对指定的channel进行修改epoll_event event;memset(&event, 0, sizeof event);event.events = channel->events();event.data.fd = channel->fd();event.data.ptr = channel;if (::epoll_ctl(epollfd_, operation, channel->fd(), &event) == -1) {if (operation == EPOLL_CTL_DEL) {LOG_ERROR("%s--%s--%d--%d : epoll_ctl error\n", __FILE__, __FUNCTION__, __LINE__, errno);}else {LOG_FATAL("%s--%s--%d--%d : epoll_ctl error\n", __FILE__, __FUNCTION__, __LINE__, errno);}}
}void EpollPoller::fillActiveChannels(int numEvents, ChannelList* activeChannels) const {for (int i = 0; i < numEvents; i++) {Channel* channel = static_cast<Channel*>(events_[i].data.ptr);channel->set_revents(events_[i].events);    //channel和event之间建立了连接activeChannels->push_back(channel);}
}

结尾

以上就是监听者EpollPoller类的相关介绍,以及我在进行项目重写的时候遇到的一些问题,和我自己的一些心得体会。发现写博客真的会记录好多你的成长,而且对于一个好的项目,写博客也是证明你确实有过深度思考,并且在之后面试或者工作时遇到同样的问题能够进行复盘的一种有效的手段。所以,希望uu们也可以像我一样,养成写博客的习惯,逐渐脱离菜鸡队列,向大佬前进!!!加油!!!

也希望我能够完成muduo网络库项目的深度学习与重写,并在功能上能够拓展。也希望在完成这个博客系列之后,能够引导想要学习muduo网络库源码的人,更好地探索这篇美丽繁华的土壤。致敬chenshuo大神!!!

鉴于博主只是一名平平无奇的大三学生,没什么项目经验,所以可能很多东西有所疏漏,如果有大神发现了,还劳烦您在评论区留言,我会努力尝试解决问题!

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

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

相关文章

SpringBoot 更新业务场景下,如何区分null是清空属性值 还是null为vo属性默认值?

先看歧义现象 值为null 未传递此属性 所以此时如何区分null 时传递进来的的null&#xff0c;还是属性的默认值null? 引入方案 引入过滤器&#xff0c;中间截获requestBodyData并保存到HttpServletRequest&#xff0c;业务层从HttpServletRequest 获取到requestBodyData辅…

openssl3.2 - 官方demo学习 - smime - smver.c

文章目录 openssl3.2 - 官方demo学习 - smime - smver.c概述笔记END openssl3.2 - 官方demo学习 - smime - smver.c 概述 对于签名文件(不管是单独签名, 还是联合签名), 都要用顶层证书进行验签(靠近根CA的证书) 读证书文件, 得到x509*, 添加到证书容器 读取签名密文, 得到p…

LaTeX 多栏文档 Multiple columns如何插入图片并修改样式

在今天写报告的时候用到了 latex 的多栏列表&#xff0c;插入图片的时候感觉很无助 如果不喜欢让Latex自动安排图片位置&#xff0c;可以使用float包&#xff0c;然后可以使用\begin{figure}[H]。 记得提前导入这个包 \usepackage{float} 为了让我的图片的caption居中&#xf…

市面上常见硬盘分析及对比

固态硬盘 vs. 机械硬盘对比&#xff1a; 工作原理&#xff1a; 固态硬盘(SSD)&#xff1a; 使用非易失性存储器&#xff08;NAND闪存&#xff09;来存储数据&#xff0c;通过电子方式读写。机械硬盘(HDD)&#xff1a; 使用旋转的磁盘片和移动的磁头进行数据读写&#xff0c;依赖…

django电影推荐系统

电影推荐 启动 ./bin/pycharm.shdjango-admin startproject movie_recommendation_projectcd movie_recommendation_project/python manage.py movie_recommendation_apppython manage.py startapp movle_recommendation_applspython manage.py runserver Using the URLconf d…

Python多线程爬虫——数据分析项目实现详解

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 ChatGPT体验地址 文章目录 前言爬虫获取cookie网站爬取与启动CSDN爬虫爬虫启动将爬取内容存到文件中 多线程爬虫选择要爬取的用户 线程池 爬虫 爬虫是指一种自动化程序&#xff0c;能够模…

达梦数据库 忘记 SYSDBA 密码 处理方法

DM 提供数据库身份验证模式、基于操作系统的身份验证模式、外部身份验证模式和 UKEY 身份验证模式来保护对数据库访问的安全。数据库身份验证模式需要利用数据库口令&#xff0c; 即在创建或修改用户时指定用户口令&#xff0c;用户在登录时输入对应口令进行身份验证;基于操作 …

Kylin 安装novnc 远程访问

noVNC可以使用浏览器直接访问服务器&#xff0c;而不需要使用VNC客户端。 1.初始环境 关闭防火墙或允许IP访问本机 2.安装依赖 dnf install -y tigervnc-server git 3.git下载novnc git clone https://github.com/novnc/noVNC.git 4.配置信任证书 openssl req -new -x509 …

DEJA_VU3D - Cesium功能集 之 119-三维热力图

前言 编写这个专栏主要目的是对工作之中基于Cesium实现过的功能进行整合,有自己琢磨实现的,也有参考其他大神后整理实现的,初步算了算现在有差不多实现小140个左右的功能,后续也会不断的追加,所以暂时打算一周2-3更的样子来更新本专栏(每篇博文都会奉上完整demo的源代码…

爬虫系列实战:使用json解析天气数据

大家好&#xff0c;爬虫是一项非常抢手的技能&#xff0c;收集、分析和清洗数据是数据科学项目中最重要的部分&#xff0c;本文介绍使用json解析气象局天气数据。 在官网上获取天气数据信息&#xff0c;可以定义当前查询的位置&#xff0c;提取时间、温度、湿度、气压、风速等…

RabbitMQ使用篇

☆* o(≧▽≦)o *☆嗨~我是小奥&#x1f379; &#x1f4c4;&#x1f4c4;&#x1f4c4;个人博客&#xff1a;小奥的博客 &#x1f4c4;&#x1f4c4;&#x1f4c4;CSDN&#xff1a;个人CSDN &#x1f4d9;&#x1f4d9;&#x1f4d9;Github&#xff1a;传送门 &#x1f4c5;&a…

(力扣记录)146. LRU 缓存

数据类型&#xff1a;链表 时间复杂度&#xff1a;O(1) 空间复杂度&#xff1a;O(N) 代码实现&#xff1a; class Node:def __init__(self, key-1, value-1):self.key keyself.val valueself.next Noneself.prev Noneclass LRUCache:def __init__(self, capacity: int)…

Django笔记(三):路由urls

首 Django中视图的作用是在链接与视图函数之间做映射。创建完Django项目后&#xff0c;同名文件夹下的urls.py为项目的总路由&#xff08;/project/project/urls.py&#xff09;&#xff1a; from django.contrib import admin from django.urls import pathurlpatterns [pa…

微信小程序带参数分享界面、打开界面加载分享内容

分享功能是微信小程序常用功能之一&#xff0c;带参分享和加载可以让分享对象打开界面时看到和分享内容。 带参分享 用户点击微信小程序右上角自带分享&#xff0c;或者点击自定义分享按钮进行分享时&#xff0c;可在onShareAppMessage函数定义分享行为。 分享界面路径可带参…

USB转SPI USB转IIC 串口转SPI串口转IIC SPI I2C模块

一款支持USB转SPI、USB转I2C、USB转GPIO、USB转PWM、USB转ADC的模块。提供上位机工具&#xff0c;开发协议。 资料下载&#xff0c;链接&#xff1a;https://pan.baidu.com/s/1sw3RCMwjhrMO4qzUBq9bjA 提取码&#xff1a;qzjp 概述 串口转多协议模组为了客户调试一些功能…

PICO Developer Center 创建和调试 ADB 命令

PICO 开发者中心概览 ADB 是一个轻量级的 Android 调试桥(Android Debug Bridge&#xff0c;简称 ADB)&#xff0c;用于与 Android 设备进行通信和调试。ADB提供了许多有用的功能&#xff0c;使开发人员能够轻松地管理和调试设备上的应用程序。 你可以使用 PDC 工具来调试系统…

阿里云服务器4核8G配置收费标准及新老用户优惠价格整理

阿里云服务器4核8g配置云服务器u1价格是955.58元一年&#xff0c;4核8G配置还可以选择ECS计算型c7实例、计算型c8i实例、计算平衡增强型c6e、ECS经济型e实例、AMD计算型c8a等机型等ECS实例规格&#xff0c;规格不同性能不同&#xff0c;价格也不同&#xff0c;阿里云服务器网al…

openssl3.2 - 官方demo学习 - smime - smsign2.c

文章目录 openssl3.2 - 官方demo学习 - smime - smsign2.c概述笔记END openssl3.2 - 官方demo学习 - smime - smsign2.c 概述 PKCS7联合签名 从N张证书中, 分别得到N对(x509和私钥) 对明文进行签名(只是指定了bio_in和flag), 得到pkcs7* 对此pkcs7进行附加签名者的操作(指定证…

SparkSession对象操作--学习笔记

1,SparkSession对象操作 from pyspark.sql import SparkSessionfrom pyspark import SparkConffrom pyspark.sql import functions as F"""创建ss对象时可以指定一些参数如果参数在脚本中不生效&#xff0c;就需要通过saprk-submit指令中进行设置spark sql 的分…

使用 vsCode创建GO项目

最近回顾了一下go的使用&#xff1a;具体操作看下面的参考连接&#xff0c;下面只描述一些踩过的坑&#xff1a; 1. go安装配置 安装go->配置go环境变量 推荐官网下载&#xff0c;速度很快&#xff1b; 这里需要配置五个参数&#xff1a;GOPATH/GOROOT/Path、GO111MODULE/…