Reactor 모드 해석 - muduo 네트워크 라이브러리

8831 단어
최근 한동안 무두오 원본을 읽었는데 읽은 소감이 어수선하다.물론 코드가 흐트러진 것은 아니지만, 내가 아직 완전히 소화하고 이해하지 못했을 수도 있다.이 라이브러리를 더 잘 배우기 위해서는 뭔가를 써서 촉진시켜야 한다.
나는 읽으면서 몇몇 부분에서 c++11의 새로운 기능을 바꾸려고 시도했는데, 이 작업은 계속 진행되고 있다.왜 이렇게 해요?이유 없이 순전히 공부하기 위해서다.
주: 본고의 대부분 코드와 도문은 에서 나온 것으로 무두오의 원본 코드를 직접 참고하거나 제가 베낀 버전을 참고할 수 있습니다.

Reactor 소개


Reactor란?명사인'non-blocking IO + IO multiplexing'을 바꾸면 뜻이 뻔하다.Reactor 모드는 비저항 IO+poll(epoll) 함수로 처리되고 발송되며 프로그램의 기본 구조는 이벤트 순환으로 이벤트 구동과 이벤트 리셋의 방식으로 업무 논리를 실현한다.
while(!done)
{
    int retval  = poll(fds,nfds,timeout)
    if(retval < 0)
            ,     error handler
    else{
             timers,     timer handler
        if(retval > 0){
              IO  ,     IO event handler
        }
    }
}

이 코드는 형식이 매우 간단합니다. 지난 글의 epoll의 예와 매우 비슷합니다. 시간 초과timer 부분을 처리하지 않은 것을 제외하고는.muduo의 실현에서 타이머는 linux 플랫폼의timerfd *를 사용했다계열 함수,timers와 기타 IO가 통일되었다.

단일 스레드 Reactor 구현


muduo의Reactor 핵심은 주로Channel,EventLoop,Poller,TimerQueue 등 몇 가지 종류로 완성되었다.언뜻 보기에는 아직 빙빙 돌고 있는데, 코드 안의 각종 반환 함수는 보기에 좀 직관적이지 않다.또 이 몇 가지 생명주기도 주의해야 하기 때문에 잘 정리하지 못하기 쉽다.

1. Channel


Channel 클래스는 비교적 간단합니다. IO 이벤트의 분배를 책임지고 모든 Channel 대상은 fd에 대응합니다. 핵심 구성원은 다음과 같습니다.
  EventLoop* loop_;
  const int fd_;
  int events_;
  int revents_;
  int index_;

  ReadEventCallback readCallback_;
  EventCallback writeCallback_;
  EventCallback errorCallback_;
  EventCallback closeCallback_;

몇 개의 콜백 함수는 c++ 새 표준에 있는 function 대상입니다. (muduo에는 Boost: function) 이것은handle Event 구성원 함수에서 서로 다른 이벤트에 따라 호출됩니다.index_폴러류 중 폴프ds 입니다.배열의 아래 첨자.events_및 reventsstruct pollfd 구조의 구성원에 뚜렷하게 대응했다.주의해야 할 것은 채널이 이 fd를 가지고 있지 않다는 것이다. 채널은 분석 함수에서 이 fd를 닫지 않는다. (fd는 Socket류의 분석 함수에서 닫는다. 즉 RAII의 방법이다.) 채널의 생명 주기는 owner가 책임진다.

2. Poller


Poller 클래스는 poll 함수의 봉인입니다. (muduo 원본에서 추상적인 기본 클래스로 poll과 epoll을 지원합니다.) 두 핵심 데이터 구성원이 있습니다.
  typedef std::vector PollFdList;
  typedef std::map ChannelMap;  // fd to Channel
  PollFdList pollfds_;
  ChannelMap channels_;

ChannelMap은 fd가 Channel 클래스에 비치는 것입니다. PollFdList는 모든 fd가 관심을 가지는 이벤트를 저장하여 poll 함수에 매개 변수로 전달합니다. Channel 클래스의 index바로 이곳의 하표다.Poller 클래스에는 다음과 같은 네 가지 함수가 있다
Timestamp poll(int timeoutMs, ChannelList* activeChannels);
void updateChannel(Channel* channel);
void removeChannel(Channel* channel);
private:
void fillActiveChannels(int numEvents, ChannelList* activeChannels) const;

업데이트 채널과remove 채널은 모두 위의 두 데이터 구조에 대한 작업입니다.poll 함수는: poll의 봉인입니다.개인fillActiveChannels 함수는 되돌아오는 활동 시간을 activeChannels (vector) 구조에 추가해서 사용자에게 되돌려줍니다.Poller의 직책도 간단하다. IO multiplexing을 맡고 하나의 이벤트 Loop에는 하나의 Poller가 있다. Poller의 생명 주기는 이벤트 Loop과 같이 길다.

3. EventLoop


이벤트Loop 클래스가 핵심입니다. 대부분의 클래스는 이벤트Loop*의 구성원을 포함합니다. 모든 이벤트가 이벤트Loop::loop()에서 채널을 통해 나눠지기 때문입니다.먼저 이 loop 순환을 살펴보자.
while (!quit_)
{
    activeChannels_.clear();
    pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
    for (ChannelList::iterator it = activeChannels_.begin();
        it != activeChannels_.end(); ++it)
    {
      (*it)->handleEvent(pollReturnTime_);
    }
    doPendingFunctors();
}

handle Event는 Channel 클래스의 구성원 함수로 이벤트 유형에 따라 다른 Callback을 호출합니다.순환 말미에는 DoPendingFunctors () 가 있는데, 이 함수의 작용은 뒤의 다중 루틴 부분에서 설명한다.위의 세 가지 클래스로 Reactor의 핵심을 구성할 수 있으며 전체 프로세스는 다음과 같습니다.
  • 사용자가 채널을 통해 Poller 클래스에 fd와 관심 이벤트를 등록
  • EventLoop이 poll에서 활성화된 fd와 대응하는 채널
  • 을 되돌려줍니다
  • Channel을 통해 해당 시간을 되돌려줍니다.

  • muduo의 책 속에는 시차도(8-1)가 있는데 전체 과정을 명확하게 설명한다.

    4. TimerQueue


    muduo의 타이머는 표준 용기 라이브러리 set을 사용하여 관리합니다.TimerQueue:
    typedef std::shared_ptr TimerPtr;
    typedef std::pair Entry;
    typedef std::set TimerList;
    Channel timerfdChannel_;
    const int timerfd_;
    TimerList timers_;

    std::pair에 set을 더한 형식은 두 Timer 동료의 만료 상황을 처리하기 위해서입니다. 만료 시간이 같아도 그들의 주소는 다릅니다.timerfdChannel_timerfdcreate 함수로 만든 fd입니다.Timer 클래스에는 리셋 함수와 만료 시간이 포함되어 있습니다.expiration_바로 위 Entry의 Timestamp입니다.
    const TimerCallback callback_;
    Timestamp expiration_;

    이렇게 하면 전체적인 사고방식이 매우 분명해진다.
  • 모든 이벤트와 시간을 set으로 저장합니다
  • set 집합에서 가장 빠른 시간에 따라timerfd 업데이트의 만료 시간(timerfd settime 함수 사용)
  • 시간이 만료되면 EventLoop의 poll 함수가 되돌아오고timerfdChannel 을 호출합니다안에 있는 handle Event 콜백 함수입니다.
  • handle 이벤트라는 리셋 함수를 통해 만료된 모든 이벤트를 처리합니다.
  • timerfdChannel_.setReadCallback(
        std::bind(&TimerQueue::handleRead,this));
    timerfdChannel_.enableReading();

    timerfdChannel_의 콜백 함수는 TimerQueue의handleRead 함수를 등록했습니다.handleRead에서 무엇을 해야 하는지 분명히 알 수 있다. 당연히 기한이 지난 timer를 모두 찾아내서 대응하는 이벤트를 한 번에 실행한다.
    void TimerQueue::handleRead()
    {
      loop_->assertInLoopThread();
      Timestamp now(Timestamp::now());
      readTimerfd(timerfd_, now);
      std::vector expired = getExpired(now);
      // safe to callback outside critical section
      for (std::vector::iterator it = expired.begin();
          it != expired.end(); ++it)
      {
        it->second->run();
      }
      reset(expired, now);
    }

    지금까지 단일 스레드 Reator가 완료되었습니다.항상 무두오와 같은 사건이 리셋되는 코드 스타일이 읽기에 비교적 복잡하고 직관적이지 않다고 느낀다.다른 리액터 모드의 네트워크 프로그램도 이런 느낌일지 모르겠다.

    다중 스레드 기술


    다선정은 본질적으로 어려운 것이다. 왜냐하면 두 가지 일이 동시에 발생할 수 있는 여러 가지 상황을 생각하도록 뇌에 강요하기 때문이다.나는 현재 다른 사람의 경험과 총결을 보는 것 외에 다선정 문제를 해결할 기교나 방법론이 없다고 느낀다.
    하나의 스레드, 하나의 EventLoop, 모든 스레드는 자체적으로 관리하는 각종 ChannelList와 TimerQueue를 가지고 있다.때때로, 우리는 각 노선 사이에서 임무를 배정해야 하는 수요가 있다.예를 들어 IO 라인에 정해진 시간을 추가하면 TimerQueue는 두 라인이 동시에 접근할 수 있습니다.나는 무두오가 자물쇠를 처리하는 문제에 있어서 매우 배울 만하다고 생각한다.

    1. runInLoop() 및 queueInLoop()


    먼저 EventLoop의 주요 함수와 구성원을 살펴보겠습니다.
    std::vector pendingFunctors_; // @GuardedBy mutex_
    void EventLoop::runInLoop(Functor&& cb) {
      if (isInLoopThread()) {
        cb();
      } else {
        queueInLoop(std::move(cb));
      }
    }
    void EventLoop::queueInLoop(Functor&& cb) {
      {
        MutexLockGuard lock(mutex_);
        pendingFunctors_.push_back(cb);
      }
      if (!isInLoopThread() || callingPendingFunctors_) {
        wakeup();
      }
    }

    이 함수 매개 변수를 주의하십시오. C++11의 오른쪽 값 인용을 사용했습니다.앞에 있는 EventLoop::loop에서 우리는 이미doPendingFunctors()라는 함수를 보았습니다. EventLoop에는 또 하나의 중요한 구성원인 pendingFunctors 가 있습니다.그 멤버는 다른 라인에 노출되었다.이렇게 하면 다른 스레드가 입출력 스레드에 일정 시간을 추가하는 프로세스는 다음과 같습니다.
  • 기타 스레드 호출runInLoop(),
  • 현재 IO 스레드가 아닌 경우queueInLoop()
  • queueLoop에서 시간push를pendingFunctors에서 현재 IO 라인을 깨우고 이곳의 깨우기 조건을 주의하십시오: 현재 IO 라인이 아니면 반드시 깨워야 합니다.또한 펜딩 펀치를 호출하는 경우에도 깨워야 합니다.(왜?, Pending Functor에서 실행되고 있으면queue Loop도 실행되고 깨우지 않으면 새로 추가된 cb가 바로 실행되지 않기 때문이다.)

  • 2.doPendingFunctors()


    이제 DoPendingFunctors () 함수를 살펴보겠습니다.
    void EventLoop::doPendingFunctors() {
      std::vector functors;
      callingPendingFunctors_ = true;
      {
        // reduce the lenth of the critical section
        // avoid the dead lock cause the functor can call queueInloop(;)
        MutexLockGuard lock(mutex_);
        functors.swap(pendingFunctors_);
      }
      for (size_t i = 0; i < functors.size(); ++i) {
        functors[i]();
      }
      callingPendingFunctors_ = false;
    }

    PendingFunctors는 임계 구역에서 직접 functors를 실행하지 않고 하나의 창고 대상을 이용하여 이벤트 swap을 창고 대상에 넣고 실행합니다.이렇게 하면 두 가지 좋은 점이 있다.
  • 임계구의 길이를 줄이고 다른 라인이queueInLoop을 호출하여pendingFunctors에 자물쇠를 잠글 때 막히지 않습니다
  • 사라진 자물쇠를 피하고 functors에서queueInLoop()을 호출하여 사라진 자물쇠를 만들 수 있습니다.

  • 돌아보면 muduo는 다중 루트와 잠금 액세스 공유 데이터를 처리하는 전략에서 매우 중요한 원칙을 가지고 있다. 임계구의 길이를 필사적으로 줄이는 것이다. 만약에 pendingFunctors 가 없다면 생각해 보자.이 데이터 구성원은 TimerQueue에 timer를 추가하려면 TimerQueue의 insert 함수에 자물쇠를 잠그고 자물쇠의 사용을 다투어야 합니다.pendingFunctors이 멤버는 잠금의 범위를 벡터의push 로 줄였습니다.백작업상.그 밖에 DoPendingFunctors에서 하나의 창고 대상을 이용하여 임계 구역을 줄이는 것도 교묘한 중요한 기교이다.

    3.wake()


    앞에서 IO 라인을 깨우면 EventLoop이 폴 함수에 막히는데 어떻게 제때에 깨웁니까?이전의 방법은 pipe를 이용하여 pipe에 바이트를 쓰면 이 pipe의 읽기 이벤트를 감시하는poll 함수가 바로 되돌아오는 것이다.muduo에서 linux에서 이벤트fd 호출을 사용했습니다
    static int createEventfd() {
      int evtfd = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
      if (evtfd < 0) {
        LOG_SYSERR << "Failed in eventfd";
        abort();
      }
      return evtfd;
    }
    void EventLoop::wakeup() {
      uint64_t one = 1;
      ssize_t n = ::write(wakeupFd_, &one, sizeof one);
      if (n != sizeof one) {
        LOG_ERROR << "EventLoop::wakeup() writes " << n << " bytes instead of 8";
      }
    }

    이벤트fd에서 얻은 fd를 앞과 같이 채널을 통해poll에 등록하고 깨울 때wakeupFd에 한 바이트만 쓰면 깨우는 목적을 달성할 수 있습니다.eventfd, timerfd 모두 linux의 디자인 철학, Everyting is a fd를 구현했다.
    mudood의 Reactor 모델에 관해서 나는 마침내 약간의 이해를 하게 되었다.TCPServer 섹션은 다음 섹션에 설명되어 있습니다.다음 단계에서는 무두오의 HTTP 예시를 계속 연구하고 확장해 보겠습니다.

    좋은 웹페이지 즐겨찾기