Muduo源码Poller类 + EpollPoller类详解
简介
Poller class 是IO multiplexing的封装。在muduo中它是一个抽象类,因为muduo同时支持poll和epoll两种IO multiplexing机制。Poller是EventLoop的间接成员,只供其owner EventLoop在IO线程中调用,因此无需加锁。其生命周期和EvenLoop相等。Poller并不拥有Channel,Channel在析构前必须自己unregister(EventLoop::removeChannel()),避免悬空指针。
Poller.h
Poller.h只是一个简单的抽象类,简单分析一下源码
// Copyright 2010, Shuo Chen. All rights reserved. // http://code.google.com/p/muduo/ // // Use of this source code is governed by a BSD-style license // that can be found in the License file. // Author: Shuo Chen (chenshuo at chenshuo dot com) // // This is an internal header file, you should not include this. #ifndef MUDUO_NET_POLLER_H #define MUDUO_NET_POLLER_H #include <vector> #include <boost/noncopyable.hpp> #include <muduo/base/Timestamp.h> #include <muduo/net/EventLoop.h> namespace muduo { namespace net { class Channel; /// /// Base class for IO Multiplexing /// /// This class doesn't own the Channel objects. //这个Poller类只是一个抽象类主要实现在EpollPoller和PollPoller中 class Poller : boost::noncopyable { public: typedef std::vector<Channel *> ChannelList; Poller(EventLoop *loop); virtual ~Poller(); /// Polls the I/O events. /// Must be called in the loop thread. virtual Timestamp poll(int timeoutMs, ChannelList *activeChannels) = 0; // poll函数 /// Changes the interested I/O events. /// Must be called in the loop thread. virtual void updateChannel(Channel *channel) = 0;// 更新Channel, 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);// 在这里会选择epoll或者poll void assertInLoopThread() {// 确保所有的操作都在事件循环的线程中 ownerLoop_->assertInLoopThread(); } private: EventLoop *ownerLoop_; // Poller所属EventLoop }; } } #endif // MUDUO_NET_POLLER_H
DefaultPoller.cc
这里是一个选择器, 根据系统环境不同而选择epoll或者poll. 因为现在的Linux环境基本都支持epoll, 所以我们在此只关注Epollpoller的实现。(主要是因为我只用用epoll,没有使用过poll)
// Copyright 2010, Shuo Chen. All rights reserved. // http://code.google.com/p/muduo/ // // Use of this source code is governed by a BSD-style license // that can be found in the License file. // Author: Shuo Chen (chenshuo at chenshuo dot com) /*动态生成一个PollPoller类或者EPollPoller类变量*/ #include <muduo/net/Poller.h> #include <muduo/net/poller/PollPoller.h> #include <muduo/net/poller/EPollPoller.h> #include <stdlib.h> using namespace muduo::net; Poller *Poller::newDefaultPoller(EventLoop *loop) { if (::getenv("MUDUO_USE_POLL"))//如果在环境变量中找到MUDUO_USE_POLL这一项,就返回PollPoller类,否则返回EPollPoller类 { return new PollPoller(loop); } else { return new EPollPoller(loop); } }
Epoll原理与select原理
涉及到IO多路复用就顺便讲讲原理,就当复习一遍。
因为poll原理和select基本一样只是用链表存储,在这里就直接分析select原理。
select原理概述
调用select时,会发生以下事情:
1. 从用户空间拷贝fd_set到内核空间;
2. 注册回调函数__pollwait;
3. 遍历所有fd,对全部指定设备做一次poll(这里的poll是一个文件操作,它有两个参数,一个是文件fd本身,一个是当设备尚未就绪时调用的回调函数__pollwait,这个函数把设备自己特有的等待队列传给内核,让内核把当前的进程挂载到其中);
4. 当设备就绪时,设备就会唤醒在自己特有等待队列中的【所有】节点,于是当前进程就获取到了完成的信号。poll文件操作返回的是一组标准的掩码,其中的各个位指示当前的不同的就绪状态(全0为没有任何事件触发),根据mask可对fd_set赋值;
5. 如果所有设备返回的掩码都没有显示任何的事件触发,就去掉回调函数的函数指针,进入有限时的睡眠状态,再恢复和不断做poll,再作有限时的睡眠,直到其中一个设备有事件触发为止。
6. 只要有事件触发,系统调用返回,将fd_set从内核空间拷贝到用户空间,回到用户态,用户就可以对相关的fd作进一步的读或者写操作了。
epoll原理概述
调用epoll_create时,做了以下事情:
内核帮我们在epoll文件系统里建了个file结点;
在内核cache里建了个红黑树用于存储以后epoll_ctl传来的socket;
建立一个list链表,用于存储准备就绪的事件。
调用epoll_ctl时,做了以下事情:
把socket放到epoll文件系统里file对象对应的红黑树上;
给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪list链表里。
调用epoll_wait时,做了以下事情:
观察list链表里有没有数据。有数据就返回,没有数据就sleep,等到timeout时间到后即使链表没数据也返回。而且,通常情况下即使我们要监控百万计的句柄,大多一次也只返回很少量的准备就绪句柄而已,所以,epoll_wait仅需要从内核态copy少量的句柄到用户态而已。
总结如下:
一颗红黑树,一张准备就绪句柄链表,少量的内核cache,解决了大并发下的socket处理问题。
执行epoll_create时,创建了红黑树和就绪链表;
执行epoll_ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据;
执行epoll_wait时立刻返回准备就绪链表里的数据即可。
两种模式的区别:
LT模式下,只要一个句柄上的事件一次没有处理完,会在以后调用epoll_wait时重复返回这个句柄,而ET模式仅在第一次返回。
两种模式的实现:
如果是ET模式,当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时我们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表,最后,epoll_wait检查这些socket,如果是LT模式,并且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表。所以,LT模式的句柄,只要它上面还有事件,epoll_wait每次都会返回。
对比
select缺点:
最大并发数限制:使用32个整数的32位,即32*32=1024来标识fd,虽然可修改,但是有以下第二点的瓶颈;
效率低:每次都会线性扫描整个fd_set,集合越大速度越慢;
内核/用户空间内存拷贝问题。
epoll的提升:
本身没有最大并发连接的限制,仅受系统中进程能打开的最大文件数目限制;
效率提升:只有活跃的socket才会主动的去调用callback函数;
网上很多博客说epoll使用了共享内存,这个是完全错误的 ,可以阅读源码,会发现完全没有使用共享内存的任何api,而是 使用了copy_from_user跟__put_user进行内核跟用户虚拟空间数据交互.
当然,以上的优缺点仅仅是特定场景下的情况:高并发,且任一时间只有少数socket是活跃的。
如果在并发量低,socket都比较活跃的情况下,select就不见得比epoll慢了(就像我们常常说快排比插入排序快,但是在特定情况下这并不成立)。
EPollPoller.h
1.这个类主要利用epoll函数,封装了epoll三个函数,
2.其中epoll_event.data是一个指向channel类的指针,这里可以等价理解为channel就是epoll_event,用于在epoll队列中注册,删除,更改的结构体。因为文件描述符fd,Channel,以及epoll_event结构体(只有需要添加到epoll上时才有epoll_event结构体)三个都是一一对应的关系Channel.fd应该等于fd,epoll_event.data应该等于&Channel。如果不添加到epoll队列中,Channel和fd一一对应,就没有epoll_event结构体了
3.从epoll队列中删除有两种删除方法,
第一种暂时删除,就是从epoll队列中删除,并且把标志位置为kDeleted,但是并不从ChannelMap channels_中删除
第二种是完全删除,从epoll队列中删除,并且从ChannelMap channels_中也删除,最后把标志位置kNew。
可以理解为ChannelMap channels_的作用就是:暂时不需要的,就从epoll队列中删除,但是在channels_中保留信息,类似与挂起,这样下次再使用这个channel时,只需要添加到epoll队列中即可。而完全删除,就把channels_中也删除。
下面的源码有详细的注释
// Copyright 2010, Shuo Chen. All rights reserved. // http://code.google.com/p/muduo/ // // Use of this source code is governed by a BSD-style license // that can be found in the License file. // Author: Shuo Chen (chenshuo at chenshuo dot com) // // This is an internal header file, you should not include this. /*1.这个类主要利用epoll函数,封装了epoll三个函数, *2.其中epoll_event.data是一个指向channel类的指针 *这里可以等价理解为channel就是epoll_event,用于在epoll队列中注册,删除,更改的结构体 *因为文件描述符fd,Channel,以及epoll_event结构体(只有需要添加到epoll上时才有epoll_event结构体) *三个都是一一对应的关系Channel.fd应该等于fd,epoll_event.data应该等于&Channel *如果不添加到epoll队列中,Channel和fd一一对应,就没有epoll_event结构体了 *3.从epoll队列中删除有两种删除方法, *第一种暂时删除,就是从epoll队列中删除,并且把标志位置为kDeleted,但是并不从ChannelMap channels_中删除 *第二种是完全删除,从epoll队列中删除,并且从ChannelMap channels_中也删除,最后把标志位置kNew *可以理解为ChannelMap channels_的作用就是:暂时不需要的,就从epoll队列中删除,但是在channels_中保留信息,类似与挂起,这样 *下次再使用这个channel时,只需要添加到epoll队列中即可。而完全删除,就把channels_中也删除。 */ #ifndef MUDUO_NET_POLLER_EPOLLPOLLER_H #define MUDUO_NET_POLLER_EPOLLPOLLER_H #include <muduo/net/Poller.h> #include <map> #include <vector> struct epoll_event; namespace muduo { namespace net { /// /// IO Multiplexing with epoll(4). /// 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; //默认事件数组大小,是用来装epoll_wait()返回的可读或可写事件的 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_;//epoll监视的文件描述符 EventList events_;//用来存储活跃文件描述符的epoll_event结构体数组 ChannelMap channels_;//记录标志符是kAdded或者kDeleted的channel和fd }; } } #endif // MUDUO_NET_POLLER_EPOLLPOLLER_H
EPollPoller.cc
主要是一些EPollPoller类的具体实现,注释很详细。
// Copyright 2010, Shuo Chen. All rights reserved. // http://code.google.com/p/muduo/ // // Use of this source code is governed by a BSD-style license // that can be found in the License file. // Author: Shuo Chen (chenshuo at chenshuo dot com) #include <muduo/net/poller/EPollPoller.h> #include <muduo/base/Logging.h> #include <muduo/net/Channel.h> #include <boost/static_assert.hpp> #include <assert.h> #include <errno.h> #include <poll.h> #include <sys/epoll.h> using namespace muduo; using namespace muduo::net; // On Linux, the constants of poll(2) and epoll(4) // are expected to be the same. BOOST_STATIC_ASSERT(EPOLLIN == POLLIN); BOOST_STATIC_ASSERT(EPOLLPRI == POLLPRI); BOOST_STATIC_ASSERT(EPOLLOUT == POLLOUT); BOOST_STATIC_ASSERT(EPOLLRDHUP == POLLRDHUP); BOOST_STATIC_ASSERT(EPOLLERR == POLLERR); BOOST_STATIC_ASSERT(EPOLLHUP == POLLHUP); namespace { const int kNew = -1;//代表不在epoll队列中,也不在ChannelMap channels_中 const int kAdded = 1;//代表正在epoll队列当中 const int kDeleted = 2;//代表曾经在epoll队列当中过,但是被删除了,现在不在了,但是还是在ChannelMap channels_中的 } EPollPoller::EPollPoller(EventLoop *loop) : Poller(loop),//所属的EventLoop epollfd_(::epoll_create1(EPOLL_CLOEXEC)),//创建一个epoll文件描述符,用来监听所有注册的了事件 events_(kInitEventListSize) {//vector这样用时初始化kInitEventListSize个大小空间 if (epollfd_ < 0) { LOG_SYSFATAL << "EPollPoller::EPollPoller"; } } EPollPoller::~EPollPoller()//关闭epoll文件描述符 { ::close(epollfd_); } Timestamp EPollPoller::poll(int timeoutMs, ChannelList *activeChannels)//阻塞等待事件的发生,并且在发生后进行相关的处理 { int numEvents = ::epoll_wait(epollfd_, &*events_.begin(),//等价于&events[0],就是传入一个vecotr<struct epoll_event>的首指针进去 static_cast<int>(events_.size()), timeoutMs);//numEvents是活跃的文件描述符个数,就是待处理的文件描述符 Timestamp now(Timestamp::now()); if (numEvents > 0) { LOG_TRACE << numEvents << " events happended"; fillActiveChannels(numEvents, activeChannels); //如果返回的事件数目等于当前事件数组大小,就分配2倍空间, // 不必担心vector的大小问题了,后续会以乘以2倍的方式分配,这也是内存分配的常见做法。 if (implicit_cast<size_t>(numEvents) == events_.size())//如果活跃的文件符个数和存储活跃文件描述符的容量一样,就扩充events_ { events_.resize(events_.size() * 2); } } else if (numEvents == 0)//如果timeoutMs设置的是大于0的数,也就是超时时间有效的话,那么过了超时时间并且没有事件发生,就会出现这种情况 { LOG_TRACE << " nothing happended"; } else { LOG_SYSERR << "EPollPoller::poll()"; } return now;//返回的是事件发生时的时间 } void EPollPoller::fillActiveChannels(int numEvents, ChannelList *activeChannels) const//就是把需要处理的channel放到一个活跃channel列表中 { assert(implicit_cast<size_t>(numEvents) <= events_.size());//如果活跃的文件描述符个数大于活跃的文件描述符的容器个数,说明出错了,所以终止 for (int i = 0; i < numEvents; ++i)//将所有的活跃channel放到activeChannels列表中 { Channel *channel = static_cast<Channel *>(events_[i].data.ptr);//把产生事件的channel变量拿出来 /* 这是epoll模式epoll_event事件的数据结构,其中data不仅可以保存fd,也可以保存一个void*类型的指针。 typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; // Epoll events epoll_data_t data; //User data variable }; */ #ifndef NDEBUG//在调试时会执行下面的代码,否则就直接忽视 int fd = channel->fd(); ChannelMap::const_iterator it = channels_.find(fd); assert(it != channels_.end()); assert(it->second == channel);//判断ChannelMap中key和value的对应关系是否准确 #endif channel->set_revents(events_[i].events);//把已经触发的事件写入channel中 activeChannels->push_back(channel);//把channel放入要处理的channel列表中 } } void EPollPoller::updateChannel(Channel *channel)//根据channel的序号在epoll队列中来删除,增加channel或者改变channel { Poller::assertInLoopThread();//负责epoll_wait的线程和创建eventloop的线程为同一个 LOG_TRACE << "fd = " << channel->fd() << " events = " << channel->events(); const int index = channel->index(); if (index == kNew || index == kDeleted)//如果是完全没在或者曾经在epoll队列中的,就添加到epoll队列中 { // a new one, add with EPOLL_CTL_ADD int fd = channel->fd(); if (index == kNew) {//完全没在epoll队列中 assert(channels_.find(fd) == channels_.end());//确保这个channel的文件描述符不在channels_中 channels_[fd] = channel;//将新添加的fd和channel添加到channels_中 } else // index == kDeleted 曾经在epoll队列中 { assert(channels_.find(fd) != channels_.end());//确保这个channel的文件描述符在channels_中 assert(channels_[fd] == channel);//确保在epoll队列中channel和fd一致 } channel->set_index(kAdded);//修改index为已在队列中 update(EPOLL_CTL_ADD, channel); } else//如果是现在就在epoll队列中的,如果没有关注事件了,就暂时删除,如果有关注事件,就修改 { // update existing one with EPOLL_CTL_MOD/DEL int fd = channel->fd(); (void) fd; assert(channels_.find(fd) != channels_.end());//channels_中是否有这个文件描述符 assert(channels_[fd] == channel);//channels_中channel和fd是否一致 assert(index == kAdded);//标志位是否正在队列中 if (channel->isNoneEvent()) { update(EPOLL_CTL_DEL, channel); channel->set_index(kDeleted); } else { update(EPOLL_CTL_MOD, channel); } } } void EPollPoller::removeChannel(Channel *channel)//完全删除channel { Poller::assertInLoopThread();//???暂时不明白为什么要这么判断,也就是负责epoll管理的线程和创建eventloop的线程为同一个 int fd = channel->fd(); LOG_TRACE << "fd = " << fd; assert(channels_.find(fd) != channels_.end());//channels_中是否有这个文件描述符 assert(channels_[fd] == channel);//channels_中channel和fd是否一致 assert(channel->isNoneEvent());//channel中要关注的事件是否为空 int index = channel->index(); assert(index == kAdded || index == kDeleted);//标志位必须是kAdded或者kDeleted size_t n = channels_.erase(fd); (void) n; assert(n == 1); if (index == kAdded) { update(EPOLL_CTL_DEL, channel);//从epoll队列中删除这个channel } channel->set_index(kNew);//设置标志位是kNew,相当于完全删除 } void EPollPoller::update(int operation, Channel *channel)//主要执行epoll_ctl函数 { struct epoll_event event; bzero(&event, sizeof event); event.events = channel->events(); event.data.ptr = channel;//设置epoll_event结构体 int fd = channel->fd(); if (::epoll_ctl(epollfd_, operation, fd, &event) < 0) { if (operation == EPOLL_CTL_DEL) { LOG_SYSERR << "epoll_ctl op=" << operation << " fd=" << fd; } else { LOG_SYSFATAL << "epoll_ctl op=" << operation << " fd=" << fd; } } }