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 events
epoll_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; //可以直接传递客户端的fd
uint32_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_event
int 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大神!!!
鉴于博主只是一名平平无奇的大三学生,没什么项目经验,所以可能很多东西有所疏漏,如果有大神发现了,还劳烦您在评论区留言,我会努力尝试解决问题!