三大核心库
参考:
1.长文梳理Muduo库核心代码及优秀编程细节剖析
2.[muduo网络库]——muduo库三大核心组件之Channel类(剖析muduo网络库核心部分、设计思想)
从头开始梳理[Muduo网络库]:梳理Muduo库核心代码之后,我们开始来看Muduo三大核心库Channel类、Poller/EpollPoller类以及EventLoop类
三大核心模块之一:EventLoop类
1.1 EventLoop概述(反应器Reactor)
“除了循环,什么都没做的loop()”,这句话的意思并非是loop()只是一个空循环,而是它的主要功能并未通过自身定义的函数实现。
loop函数中主要通过调用一次Poller::poll方法它就能给你返回事件监听器的监听结果,然后调用这些Channel里面保管的不同类型事件的处理函数。
loop()函数最主要的作用是实现循环,负责驱动“循环”的重要模块。
1.2 重要成员变量
-
std::unique_ptr<Poller> poller_
:通过它会返回给EventLoop发生的事件 -
const pid_t threadId_
:创建时要保存当前时间循环所在的线程,用于之后运行时判断使用EventLoop的线程是否时EventLoop所属的线程 -
Timestamp pollReturnTime_
:保存poll返回的时间,用于计算从激活到调用回调函数的延迟 -
int wakeupFd_
,wakeupChannel_
:wakeupFd_
是非常重要的一个成员,是调用函数eventfd()
创建的eventfd
对象,与之对应的wakeupChannel_
,起到了一个唤醒loop所在的线程的作用,因为当前线程主要阻塞在poll函数上,唤醒的方法时手动激活这个wakeupChannel_
, 写入几个字节让Channel
变为可读, 当然这个Channel
也注册到Pooll
中,在下面的成员函数会详细介绍它的实现 -
ChannelList activeChannels_
:就是poller返回的所有发生事件的channel列表。 -
bool callingPendingFunctors_
:存储loop需要执行的所有回调操作,避免本来属于当前线程的回调函数被其他线程调用,应该把这个回调函数添加到属于它所属的线程,等待它属于的线程被唤醒后调用,满足线程安全 -
mutex_
:互斥锁,用来保护vector容器的线程安全操作
1.3 重要成员函数
- 最最最重要的函数
loop()
最核心的部分就是调用了Poller的poll方法,它返回了发生的事件channel列表以及发生的时间now。Channel处理对应的时间,接着还有一个doPendingFunctors函数
void EventLoop::loop()
{
assert(!looping_);
assertInLoopThread();
looping_ = true;
quit_ = false; // FIXME: what if someone calls quit() before loop() ?
LOG_TRACE << "EventLoop " << this << " start looping";
while (!quit_)
{
activeChannels_.clear();
// 监听两类fd 一种是client的fd 一种是wakeup
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
++iteration_;
if (Logger::logLevel() <= Logger::TRACE)
{
printActiveChannels();
}
// TODO sort channel by priority
eventHandling_ = true;
for (Channel *channel : activeChannels_)
{
currentActiveChannel_ = channel;
// poller监听哪些channel发生事件了,然后上报给EventLoop,通知channel处理相应的事件
currentActiveChannel_->handleEvent(pollReturnTime_);
}
currentActiveChannel_ = NULL;
eventHandling_ = false;
// 执行当前EventLoop事件循环需要处理的回调操作
/**
* IO线程 mainloop accept fd <= channel subloop
* mainloop事先注册一个回调cb,需要subloop执行
* wakeup subloop后执行下面的方法 执行之前mainloop注册的cb回调
*
*/
doPendingFunctors();
}
LOG_TRACE << "EventLoop " << this << " stop looping";
looping_ = false;
}
doPendingFunctors
执行当前loop需要执行的回调函数
值得注意的一点就是: 使用一个互斥锁mutex_
和局部vector
和pendingFunctors_
交换,避免因为要读取pendingFunctors_
加上锁的时候,没有释放锁导致新的事件无法写入的时候
void EventLoop::doPendingFunctors()
{
std::vector<Functor> functors;
callingPendingFunctors_ = true;//需要执行回调
{
MutexLockGuard lock(mutex_);
functors.swap(pendingFunctors_);
}
for (const Functor &functor : functors)
{
functor();//执行当前loop需要执行的回调操作
}
callingPendingFunctors_ = false;
}
pendingFunctors_
的来源是runInLoop()和queueInLoop()函数
// 在当前loop中执行cb
void EventLoop::runInLoop(Functor cb)
{
if (isInLoopThread()) // 在当前的loop线程中,执行cb
{
cb();
}
else // 在非当前loop执行cb,就需要唤醒loop所在线程执行cb
{
queueInLoop(std::move(cb));
}
}
void EventLoop::queueInLoop(Functor cb)
{
{
MutexLockGuard lock(mutex_);
pendingFunctors_.push_back(std::move(cb));
}
if (!isInLoopThread() || callingPendingFunctors_)
{
wakeup();
}
}
runInLoop
主要是判断是否处于当前IO线程,是则执行这个函数,如果不是则将函数加入队列queueInLoop
。在queueInLoop
就会把cb
放入pendingFunctors_
值得注意: wakeup()
这个函数:
回到主函数, 在构造函数中我们向需要监听的事件中写入回调
wakeupChannel_->setReadCallback(std::bind(&EventLoop::handleRead,this));
wakeupChannel_->enableReading();
每一个 EventLoop
都将需要监听的wakeupChannel_
的EPOLLIN
可读事件,mianreactor通过给subreactor写东西,通知其苏醒
handleRead
也是其中比较重要的一个回调了,它读取wakeupFd_
文字描述符
//发送给subreactor一个读信号,唤醒subreactor
void EventLoop::handleRead()
{
uint64_t one = 1;
ssize_t n = read(wakeupFd_, &one, sizeof one);
if(n != sizeof one)
{
LOG_ERROR("EventLoop::handleRead() reads %d bytes instead of 8",n);
}
}
wakeup()
源码,向wakeupFd
文字描述符写入数据
void EventLoop::wakeup()
{
uint64_t one = 1;
ssize_t n = write(wakeupFd_,&one,sizeof one);
if(n != sizeof one)
{
LOG_ERROR("EventLoop::wakeup() writes %lu bytes instead of 8 \n",n);
}
}
- 在析构的时候,关闭它
EventLoop::~EventLoop()
{
wakeupChannel_->disableAll();
wakeupChannel_->remove();
::close(wakeupFd_);
t_loopInThisThread = nullptr;
}
这就和上面提到的wakeupFd_
联系起来了,
首先wakeupFd_
实际上是调用eventfd
,把这个wakeupFd_
添加到poll中,在需要唤醒时写入8字节数据,
在构造函数中,也注册了它对应的回调函数wakeupChannel_->setReadCallback(std::bind(&EventLoop::handleRead,this));
,
此时poll返回,执行回调函数,然后执行在pendingFunctors_中的函数。
什么时候需要唤醒呢?
if(!isInLoopThread() || callingPendingFunctors_)
前者还是比较好理解的,One Loop Per Thread
既然不在这个loop中,那就唤醒它;后者呢?从doPendingFunctors
函数中我们可以看到callingPendingFunctors_= true
;时,是表明正在执行回调函数,在loop()中可以看出执行完回调,又会阻塞在poller_->poll(kPollTimeMs,&activeChannels_);
,如果再次调用queueInLoop
,就需要再次唤醒才能继续执行新的回调doPendingFunctors
- 判断是否在当前线程
首先通过以下代码获取了当前的loop的线程id,
threadId_(CurrentThread::tid())
- 然后通过isInLoopThread()
bool isInLoopThread() const { return threadId_ == CurrentThread::tid(); }
进行比较,来判断是否在当前的线程
1.4 EventLoop和EpollPoller和Channel的关系
当Channel需要更新或者除去时,通过调用EventLoop中的管理实现,同理当Epoll中有新的请求的时候,也是通过EventLoop来添加。从下图可以看出EventLoop相当于管理Channel,Epoll的上层机构。EventLoop整合封装了二者并向上提供了更方便的接口来使用。
三大核心模块之二:Channel
2.1 Channel类概述
Channel类其实相当于一个文件描述符的保姆!(2.2.1 Channel类概述-我在地铁站里吃闸机)
当调用epoll_ctl将一个文件描述符添加到epoll红黑树中,当该文件描述符上有事件发生时,拿到它、处理事件,这样我们每次只能拿到一个文件描述符,也就是一个int
类型的整型值。一开始我们只需要监听服务器接受的文件描述符,到后面还要加上已连接的客户端的文件描述符。不同的连接类型也将决定不同的处理逻辑,仅仅通过一个文件描述符来区分显然会很麻烦。
channel类则封装了一个文字描述符fd
,和这个fd感兴趣的event
以及epoll_wait实际上监听的到该fd实际发生的事件revent
,同时Channel还提供设置感兴趣事件,以及其将该fd和其感兴趣事件注册到事件监听器或者事件监听器上移除,以及保存了该fd的每个事件对应的处理函数
Channel *channel = static_cast<Channel *>(events_[i].data.ptr)
events是一个epoll_events数据指针,epoll_data 是一个union联合体,所有数据共享同一块地址空间,同一个时间内只能指向一个数据,取出*ptr,指向我们自定义的Channel。
typedef union epoll_data {
void *ptr; // 用户自定义数据指针
int fd; // 关联的文件描述符
uint32_t u32; // 用户定义的 32 位整数
uint64_t u64; // 用户定义的 64 位整数
} epoll_data_t;
struct epoll_event {
uint32_t events; // 事件类型掩码(epoll 监视的事件类型)
epoll_data_t data; // 用户数据,用于标识文件描述符或其他用途
};
2.2 重要成员变量
const int fd_
:这个Channel对应的文字描述符EventLoop* loop_
:这个fd对应的EventLLoop对象int events_
:fd感兴趣的事件类型集合(POLLHUP ,POLLIN,POLLERR等)int revents_
:实际监听到该fd发生的事件类型集合read_callback_
、write_callback_
、close_callback_
、error_callback_
:这些是std::function类型,代表着这个Channel为这个文件描述符保存的各事件类型发生时的处理函数。比如这个fd发生了可读事件,需要执行可读事件处理函数
事件 | 描述 | 是否可以作为输入 | 是否可以作为输出 |
---|---|---|---|
EPOLLIN | 普通(包括普通数据和优先数据)可读 | 是 | 是 |
EPOLLRDNORM | 普通数据可写 读 | 是 | 是 |
EPOLLRDBAND | 优先级带数据可读 | 是 | 是 |
EPOLLPRI | 高优先数据可读 | 是 | 是 |
EPOLLOUT | 普通(包括普通数据和优先数据可写 | 是 | 是 |
EPOLLWRNORM | 普通数据可写 | 是 | 是 |
EPOLLWRBAND | 优先级带数据可写 | 是 | 是 |
EPOLLRDHUP | TCP连接被对方关闭,或者对方关闭了写操作 | 是 | 是 |
EPOLLERR | 错误 | 否 | 是 |
EPOLLHUP | 挂起,比如管道的写端被关闭,读端描述符上将收到EPOLLHUP事件 | 否 | 是 |
EPOLLNVAL | 文件描述符没打开 | 否 | 是 |
EPOLLET | 边沿触发模式 | 否 | 是 |
EPOLLNESHOT | 表示一次性事件。处理完该事件后,文件描述符将不会再收到事件通知,除非重新注册该事件 | 否 | 是 |
2.3 重要成员函数
- 向Channel对象注册各类事件的处理函数
void setReadCallback(ReadEventCallback cb){ readCallback_ = std::move(cb); }
void setWriteCallback(EventCallback cb){ writeCallback_ = std::move(cb); }
void setCloseCallback(EventCallback cb){ closeCallback_ = std::move(cb); }
void setErrorCallback(EventCallback cb){ errorCallback_ = std::move(cb); }
设置回调函数:分别对应文件描述符发生可读,可写,关闭,错误事件时,相对应的回调函数。外部通过调用上面这四个函数可以将事件处理函数放进Channel类中,当需要调用的时候就可以直接拿出来调用了。
- 将Channel中的文件描述符及其感兴趣事件注册到事件监听器上或从事件监听器上移除
void enableReading() { events_ |= kReadEvent; update(); }
void disableReading() { events_ &= ~kReadEvent; update(); }
void enableWriting() { events_ |= kWriteEvent; update(); }
void disableWriting() { events_ &= ~kWriteEvent; update(); }
void disableAll() { events_ = kNoneEvent; update(); }
外部将感兴趣的事件通过这几个函数告知Channel函数,并把该文件描述符和感兴趣事件通过eventLoop注册到事件监听器(IO多路复用模块)epoll模块上。update()私有成员方法,实际上就是调用了EventLoop
里面的updateChannel()
,进一步调用EPollPoller::updateChannel
,这个update其实本质上就是调用了epoll_ctl()。
-
void set_revents(int revt) { revents_ = revt; }
used by pollers
, 当事件监听器监听到某个文件描述符发生了什么事件,通过这个函数可以将文件描述符实际上发生的事件封装到Channel中 -
void handleEvent(Timestamp receiveTime);
master function
,该函数调用在EventLoop::loop()中调用epoll_wait()之后,可以得知事件监听器上哪些Channel(文件描述符)发生了哪些事件,事件发生后自然就要调用这些Channel对应的处理函数。 Channel::HandleEvent,让每个事件触发的Channel调用自己保存的事件处理函数,每个Channel会根据发生的事件(通过Channel中的revents_变量得知)和感兴趣的事件(通过Channel中的event_变量得知)来选择调用响应的回调函数. -
void Channel::tie(const std::shared_ptr& obj)
当客户端正常断开TCP连接,IO事件会触发Channel中设置的closeCallback回调,但是客户在onClose()中有可能析构Channel对象,导致回调执行到一半,其所属的Channel对象本身被销毁了。这是程序会出现问题,muduo的解决方法时提供void Channel::tie(const std::shared_ptr<void>& obj)
这个函数,用于延长某对象的生命期,使之超过Channel::handleEvent函数,
void Channel::handleEvent(Timestamp receiveTime)
{
std::shared_ptr<void> guard;
if (tied_)
{
guard = tie_.lock();
if (guard)
{
handleEventWithGuard(receiveTime);
}
}
else
{
handleEventWithGuard(receiveTime);
}
}
handleEvent中tie
实际上这是一个弱指针,绑定到TcpConnection
的共享指针 ,如果TcpConnection中有执行tie()
,那么可以由原来的弱指针,变成了强指针,这时候tie()
的作用就表明了,延长了TcpConnection的生命周期,使之长过Channel::handleEvent()
,保证了TcpConnection
不被销毁,因此Channel
中存了一个TcpConnection
的弱指针,在处理事件的时候,lock将引用计数加1,Channel::handleEvent()
来关闭连接时,guard
变量依然持有一份TcpConnection
。也就是说Channel不会在执行完Channel::handleEvent()之前被析构。这一点在这里可能还是有点稀里糊涂,等到TcpConnection时,我们再来强调
三大核心模块之一:Poller/EpollPoller类
3.1 Poller/EpollPoller类概述
该模块封装poll和epoll两种IO多路复用的后端。它们负责监听文件描述符事件已经返回发生事件的文字描述符以及具体事件的模块。在多reactor多线程模型中,有多少个reactor就有多少个poller。
这个Poller是个抽象虚类,由EpollPoller和PollPoller继承实现,与监听文件描述符和返回监听结果的具体方法也基本上是在这两个派生类中实现。poll的存在价值是便于调试,因为poll调用是上下文无关的, 用strace很容易知道库的行为是否正确,所以本文谈到Poller都是EpollPoller。
3.2 重要成员变量
int epollfd_
:epollfd_(::epoll_create1(EPOLL_CLOEXEC)),
返回的epoll句柄EventList events_
:调用epoll_wait返回的事件集合ChannelMap channels_
:它主要负责记录 fd —> Channel的映射,也保管所有注册在这个Poller上的Channel。EventLoop* ownerLoop_
:就是所属的EventLoop对象
3.3重要成员函数
首先EPollPoller重写了基类Poller的抽象方法
TimeStamp poll(int timeoutMs, ChannelList* activeChannels) override;
void updateChannel(Channel* channel) override;
void removeChannel(Channel* channel) override;
需要强调的一点: 在EPollPoller重写的抽象方法,首先派生类要继承基类,基类定义为虚函数,且如果派生类在虚函数声明时使用了override描述符,那么该函数必须重载其基类中的同名函数,否则代码将无法通过编译,所以在Poller中,我们可以看出:
virtual TimeStamp poll(int timeoutMs, ChannelList *activeChannel) = 0;
virtual void updateChannel(Channel* channel) = 0;
virtual void removeChannel(Channel* channel) = 0;
TimeStamp poll(int timeoutMs, ChannelList *activeChannels)
是poller的核心
- 通过epoll_wait将发生事件的文件描述符的数量返回
- fillActiveChannels()将发生的事件装入
activeChannels
,activeChannels在EventLoop
中传入Channel
Timestamp EPollPoller::poll(int timeoutMs, ChannelList* activeChannels)
{
LOG_TRACE << "fd total count " << channels_.size();
int numEvents = ::epoll_wait(epollfd_,
&*events_.begin(),
static_cast<int>(events_.size()),
timeoutMs);
int savedErrno = errno;
Timestamp now(Timestamp::now());
if (numEvents > 0)
{
LOG_TRACE << numEvents << " events happened";
fillActiveChannels(numEvents, activeChannels);
//说明当前发生的事件可能多于vector能存放的 ,需要扩容,等待下一轮处理
if (implicit_cast<size_t>(numEvents) == events_.size())
{
events_.resize(events_.size()*2);
}
}
else if (numEvents == 0)
{
LOG_TRACE << "nothing happened";
}
else
{
// error happens, log uncommon ones
if (savedErrno != EINTR)
{
errno = savedErrno;
LOG_SYSERR << "EPollPoller::poll()";
}
}
return now;
}
void EPollPoller::fillActiveChannels(int numEvents,
ChannelList *activeChannels) const
{
assert(implicit_cast<size_t>(numEvents) <= events_.size());
for (int i = 0; i < numEvents; ++i)
{
//Channel 的初始化
Channel *channel = static_cast<Channel *>(events_[i].data.ptr);
#ifndef NDEBUG
int fd = channel->fd();
ChannelMap::const_iterator it = channels_.find(fd);
assert(it != channels_.end());
assert(it->second == channel);
#endif
channel->set_revents(events_[i].events);
//EventLoop就拿到了他的poller给他返回的所有发生事件的channel列表了
activeChannels->push_back(channel);
}
}
activeChannels
是一个vector
容器类型,将监听到该fd发生的事件通过set_revents
写进这个Channel中的revents成员变量中。这样获取到了发生事件的集合,然后把这个Channel装进activeChannels中,当外界调用完poll之后就能拿到事件监听器的监听结果,在EventLoop中就可以对它进行处理。
- 更新
channel
通道epoll_ctl add/mod/del
将Channel中的文件描述符及其感兴趣事件注册时,
调用了Channel::update()->Eventloop::updateChannel->EpollPoller->updateChannel
void Channel::
update
()
{
addedToLoop_ = true;
loop_->updateChannel(this)
;
}
void EventLoop::updateChannel(Channel *channel)
{
assert(channel->ownerLoop() == this);
assertInLoopThread();
poller_->updateChannel(channel);
}
void EPollPoller::updateChannel(Channel *channel)
{
Poller::assertInLoopThread();
const int index = channel->index();
LOG_TRACE << "fd = " << channel->fd()
<< " events = " << channel->events() << " index = " << index;
// 如果是完全没在或者曾经在epoll队列中的,就添加到epoll队列中
if (index == kNew || index == kDeleted)
{
// a new one, add with EPOLL_CTL_ADD
int fd = channel->fd();
if (index == kNew)
{
assert(channels_.find(fd) == channels_.end());
// 将新添加的fd和channel添加到channels_中
channels_[fd] = channel;
}
else // index == kDeleted
{
assert(channels_.find(fd) != channels_.end());
assert(channels_[fd] == channel);
}
channel->set_index(kAdded);
update(EPOLL_CTL_ADD, channel);
}
else // channel 已经在poller上注册过了
{
// update existing one with EPOLL_CTL_MOD/DEL
int fd = channel->fd();
(void)fd;
assert(channels_.find(fd) != channels_.end());
assert(channels_[fd] == channel);
assert(index == kAdded);
if (channel->isNoneEvent()) // 没有到关注的事件
{
update(EPOLL_CTL_DEL, channel);
channel->set_index(kDeleted);
}
else
{
update(EPOLL_CTL_MOD, channel);
}
}
}
在这个函数中,通过判断index
来决定对channel的修改 mod/add/del,index
在channel类中,对其初始化为-1,在这里对应:
const int kNew = -1; //表示一个channel还没有被添加进epoll里面 channel中index_初始化为-1
const int kAdded = 1; //表示一个channel已经添加进epoll里面
const int kDeleted = 2; //表示一个channel已经从epoll里面删除
- 删除Channel
void EPollPoller::removeChannel(Channel* channel)
{
int fd = channel->fd();
channels_.erase(fd);
LOG_INFO("func=%s => fd=%d \n",__FUNCTION__, fd);
int index = channel->index();
if (index == kAdded)
{
update(EPOLL_CTL_DEL,channel);
}
channel->set_index(kNew);
}
实际上,流程是这样的 channel update remove => EventLoop updateChannel removeChannel =>Poller updateChannel removeChannel,完全对应第一张图
总结
下面借用我在地铁站里吃闸机博主的图
EventLoop起到一个驱动循环的功能,Poller负责从事件监听器上获取监听结果。而Channel类则在其中起到了将fd及其相关属性封装的作用,将fd及其感兴趣事件和发生的事件以及不同事件对应的回调函数封装在一起,这样在各个模块中传递更加方便
在EventLoop就能够充分提现muduo库的重要思想:One Loop Per Thread
在muduo库里边有两种线程:一种里边的事件循环专门处理新用户连接(mainLoop( 也就是baseLoop)),一种里边的事件循环专门处理对应连接的所有读写事件(ioLoop)