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;
        }
    }
}

 

上一篇:内核对设备树的处理(四)__device_node转换为platform_device


下一篇:6张时序图,谈谈Tomcat请求处理流程