本篇主要讲多路复用IO接口以及对定时器的实现。在void EventLoop::loop()中调用多路复用IO接口的代码是:
pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
多路复用IO接口
被封装成类Poller
class Poller : boost::noncopyable { public: typedef std::vector<Channel*> ChannelList;//注册到多路复用IO接口的Channel Poller(EventLoop* loop); virtual ~Poller(); virtual Timestamp poll(int timeoutMs, ChannelList* activeChannels) = 0;//阻塞等待事件触发,或者超时 virtual void updateChannel(Channel* channel) = 0;//更新Channel的注册状态 /// Remove the channel, when it destructs. /// Must be called in the loop thread. virtual void removeChannel(Channel* channel) = 0;//将Channel从监听队列中删除 static Poller* newDefaultPoller(EventLoop* loop);//获取默认的多路复用IO接口可能是poll可能是epoll void assertInLoopThread() { ownerLoop_->assertInLoopThread(); } private: EventLoop* ownerLoop_; };
Poller本身只提供一些借口。真正实现功能的还是PollPoller和EpollPoller也解释poll和epoll。我们重点分析epoll:
class EPollPoller : public Poller { public: EPollPoller(EventLoop* loop); virtual ~EPollPoller(); virtual Timestamp poll(int timeoutMs, ChannelList* activeChannels); virtual void updateChannel(Channel* channel); virtual void removeChannel(Channel* channel); private: static const int kInitEventListSize = 16; void fillActiveChannels(int numEvents, ChannelList* activeChannels) const; void update(int operation, Channel* channel); typedef std::vector<struct epoll_event> EventList; typedef std::map<int, Channel*> ChannelMap; int epollfd_; EventList events_;//用于存放激活的epoll事件 ChannelMap channels_; };
下面是EpollPoller类实现的poll方法:
Timestamp EPollPoller::poll(int timeoutMs, ChannelList* activeChannels) { 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 happended"; fillActiveChannels(numEvents, activeChannels);//将活动事件对应的Channel加入到活动队列中去。 if (implicit_cast<size_t>(numEvents) == events_.size()) { events_.resize(events_.size()*2); } } else if (numEvents == 0) { LOG_TRACE << " nothing happended"; } else { // error happens, log uncommon ones if (savedErrno != EINTR) { errno = savedErrno; LOG_SYSERR << "EPollPoller::poll()"; } } return now; }
回头看loop()方法中可以知道在当所有活动Channel存入到队列中之后就将一次调用Channel的回调函数处理相应的请求。真个过程是比较常见的处理方式,我个人认为这一部分除了对超时事件需要仔细分析其它的都是可以卡巴代码就一目了然的。当然主要还是要理清封装的层次以及epoll事件和Channel类之间的封装关系,这是理解额度关键。
对于超时事件的处理库的作者是面向比较新的版本的linux设计的。作者利用新版linux提供的timerfd_create()函数创建了一个文件描述符用于描述定时器。这样定时事件到来时就会通过对应的文件描述符检测到。这样就将定时器和IO事件同等地放入多路IO复用接口处理。这归功于新版linux停提供这个接口,对于版本比较老或者强调兼容性来说,经典的做法是将定时器和多路IO复用接口的超时返回进行有效的结合,libevent就是这么实现的。
关于定时器的文档可以参看http://cpp.ezbty.org/import_doc/linux_manpage/timerfd_gettime.2.html。
Muduo的定时器主要涉及的类:
Timer
TimerQueue
Timestamp
TimeZon
Timer主要维护了定时器到时间需要处理的具体任务,Timestamp主要维护了定时器超时的具体时间。TimerQueue则主要维护了定时器队列(set)。一个IO线程有一个定时器队列对象,即一个
TimerQueue
对象,这些定时器都被这个
中的timerfd_描述(timerfd_create()函数创建),在TimerQueue
对象
中已经将TimerQueue
timerfd_
封装成Channel注册到IO多路复用接口中了,这使得io事件和定时器事件统一了起来。当多路复用接口监测到
有事件,说明有定时器超时了,这时就从定时器队列中找出超时定时器,并处理相关回调函数操作。timerfd_
定时器的队列设计作者使用了std::set,由于set是通过红黑树实现所以在删除和添加元素时性能较好。鉴于此set中存放的对象必须重载操作符<。在TimerQueue中set存储的对象是一个std::pair<Timestamp, Timer*>。只要
Timestamp
能够重载<操作符就可以了。具体代码就不在此分析了,希望有兴趣的可以参看代码。
到现在Muduo的简单分析就结束了,如果需要详细理解看代码是必不可少的。代码中的许多细节是值得我们学习的,比如智能指针以及非阻塞型的connect的实现都是一些很靠得参考。当然我对作者的谢谢代码还是存有异议的,比如在获取超时定时器的函数:std::vector<TimerQueue::Entry> TimerQueue::getExpired(Timestamp now);
我不建议将一个复杂的类型作为一个函数返回值,而我更愿意这样:
int TimerQueue::getExpired(Timestamp now,
std::vector<TimerQueue::Entry>
& outtime_list);
这样减少了内存复制的次数,特别想使用容器时,也减少了容器内部的操作次数。
为了直观的感受可以运行下面的代码:
#include <iostream> #include <stdio.h> #include <string> #include <vector> class testclass { public: testclass():i(0){} testclass(int m){i = m;} ~testclass(){printf("kkk\n");} private: int i; }; void WithReference(std::vector<testclass>& kk) { testclass l(18); testclass m; kk.push_back(l); kk.push_back(m); } void WithPtr(std::vector<testclass>* kk) { testclass l(18); testclass m; kk->push_back(l); kk->push_back(m); } std::vector<testclass> WithObject() { std::vector<testclass> temp; testclass l(18); testclass m; temp.push_back(l); temp.push_back(m); return temp; } int main() { printf("********WithWithReference********\n"); std::vector<testclass> l0; WithReference(l0); printf("********WithPtr********\n"); std::vector<testclass> l1; WithPtr(&l1); printf("********WithObject********\n"); std::vector<testclass> l2; l2 = WithObject(); return 0; }
希望你运行完能知道我说的是什么。整个Muduo的实现还是不算很复杂,向对于C++编写的库了解流程是一个关键,但是搞清楚各个类之间的封装层次上下关系和包含关系也是关键。像这类以基于回调机制的reactor模式的网络库,寻找到对应的回调函数,跟着回调函数指针走,就能理清流程,即使它很有悖人类的自然思维方式。