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()),避免懸空指針。api

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原理。app

select原理概述

調用select時,會發生如下事情:socket

1. 從用戶空間拷貝fd_set到內核空間;函數

2. 註冊回調函數__pollwait;高併發

3. 遍歷全部fd,對所有指定設備作一次poll(這裏的poll是一個文件操做,它有兩個參數,一個是文件fd自己,一個是當設備還沒有就緒時調用的回調函數__pollwait,這個函數把設備本身特有的等待隊列傳給內核,讓內核把當前的進程掛載到其中);oop

4. 當設備就緒時,設備就會喚醒在本身特有等待隊列中的【全部】節點,因而當前進程就獲取到了完成的信號。poll文件操做返回的是一組標準的掩碼,其中的各個位指示當前的不一樣的就緒狀態(全0爲沒有任何事件觸發),根據mask可對fd_set賦值;ui

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;
        }
    }
}
相關文章
相關標籤/搜索