zoukankan      html  css  js  c++  java
  • 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;
            }
        }
    }
  • 相关阅读:
    2014年之新年新愿
    C#解析Xml的Dom和Sax方式性能分析
    WCF协议与绑定
    使用SqlServer数据批量插入
    跨站脚本攻击XSS
    疯狂的JSONP
    SQLiteOpenHelper
    Android常用的UI布局
    Android用户界面
    ListView
  • 原文地址:https://www.cnblogs.com/qldabiaoge/p/12701413.html
Copyright © 2011-2022 走看看