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

2024-01-18 09:36

本文主要是介绍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大神!!!

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

这篇关于muduo网络库剖析——监听者EpollPoller类的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/618643

相关文章

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

ASIO网络调试助手之一:简介

多年前,写过几篇《Boost.Asio C++网络编程》的学习文章,一直没机会实践。最近项目中用到了Asio,于是抽空写了个网络调试助手。 开发环境: Win10 Qt5.12.6 + Asio(standalone) + spdlog 支持协议: UDP + TCP Client + TCP Server 独立的Asio(http://www.think-async.com)只包含了头文件,不依

poj 3181 网络流,建图。

题意: 农夫约翰为他的牛准备了F种食物和D种饮料。 每头牛都有各自喜欢的食物和饮料,而每种食物和饮料都只能分配给一头牛。 问最多能有多少头牛可以同时得到喜欢的食物和饮料。 解析: 由于要同时得到喜欢的食物和饮料,所以网络流建图的时候要把牛拆点了。 如下建图: s -> 食物 -> 牛1 -> 牛2 -> 饮料 -> t 所以分配一下点: s  =  0, 牛1= 1~

poj 3068 有流量限制的最小费用网络流

题意: m条有向边连接了n个仓库,每条边都有一定费用。 将两种危险品从0运到n-1,除了起点和终点外,危险品不能放在一起,也不能走相同的路径。 求最小的费用是多少。 解析: 抽象出一个源点s一个汇点t,源点与0相连,费用为0,容量为2。 汇点与n - 1相连,费用为0,容量为2。 每条边之间也相连,费用为每条边的费用,容量为1。 建图完毕之后,求一条流量为2的最小费用流就行了

poj 2112 网络流+二分

题意: k台挤奶机,c头牛,每台挤奶机可以挤m头牛。 现在给出每只牛到挤奶机的距离矩阵,求最小化牛的最大路程。 解析: 最大值最小化,最小值最大化,用二分来做。 先求出两点之间的最短距离。 然后二分匹配牛到挤奶机的最大路程,匹配中的判断是在这个最大路程下,是否牛的数量达到c只。 如何求牛的数量呢,用网络流来做。 从源点到牛引一条容量为1的边,然后挤奶机到汇点引一条容量为m的边

配置InfiniBand (IB) 和 RDMA over Converged Ethernet (RoCE) 网络

配置InfiniBand (IB) 和 RDMA over Converged Ethernet (RoCE) 网络 服务器端配置 在服务器端,你需要确保安装了必要的驱动程序和软件包,并且正确配置了网络接口。 安装 OFED 首先,安装 Open Fabrics Enterprise Distribution (OFED),它包含了 InfiniBand 所需的驱动程序和库。 sudo

【机器学习】高斯网络的基本概念和应用领域

引言 高斯网络(Gaussian Network)通常指的是一个概率图模型,其中所有的随机变量(或节点)都遵循高斯分布 文章目录 引言一、高斯网络(Gaussian Network)1.1 高斯过程(Gaussian Process)1.2 高斯混合模型(Gaussian Mixture Model)1.3 应用1.4 总结 二、高斯网络的应用2.1 机器学习2.2 统计学2.3

网络学习-eNSP配置NAT

NAT实现内网和外网互通 #给路由器接口设置IP地址模拟实验环境<Huawei>system-viewEnter system view, return user view with Ctrl+Z.[Huawei]undo info-center enableInfo: Information center is disabled.[Huawei]interface gigabit

Golang 网络爬虫框架gocolly/colly(五)

gcocolly+goquery可以非常好地抓取HTML页面中的数据,但碰到页面是由Javascript动态生成时,用goquery就显得捉襟见肘了。解决方法有很多种: 一,最笨拙但有效的方法是字符串处理,go语言string底层对应字节数组,复制任何长度的字符串的开销都很低廉,搜索性能比较高; 二,利用正则表达式,要提取的数据往往有明显的特征,所以正则表达式写起来比较简单,不必非常严谨; 三,使

Golang网络爬虫框架gocolly/colly(四)

爬虫靠演技,表演得越像浏览器,抓取数据越容易,这是我多年爬虫经验的感悟。回顾下个人的爬虫经历,共分三个阶段:第一阶段,09年左右开始接触爬虫,那时由于项目需要,要访问各大国际社交网站,Facebook,myspace,filcker,youtube等等,国际上叫得上名字的社交网站都爬过,大部分网站提供restful api,有些功能没有api,就只能用http抓包工具分析协议,自己爬;国内的优酷、