Análisis del kernel del framework muduo

Análisis del marco de la red muduo:

En primer lugar, debemos analizar el esqueleto del núcleo que puede soportar el marco muduo El esqueleto del núcleo del reactor de módulo es de tres clases, canal, Eventpool y Poller.

Primer vistazo al canal. El canal se usa para permitir que los eventos viajen libremente en el reactor. Dado que varios subprocesos pueden compartir memoria entre cada subproceso, la comunicación entre subprocesos generalmente utiliza la replicación de memoria como el principal medio de comunicación. utilizado, lo que significa que cada hilo tiene un bucle principal.

El código fuente de muduo es simple y fácil de entender, y el diseño es pequeño. Veamos primero el proceso de ejecución de todo el código desde su uso. El uso del código muduo:

int main()
{
    //初始化mqtt全局容器
    ::signal(SIGPIPE, SIG_IGN);
    MQTTContainer.globalInit();
    //LOG_INFO << "pid = " << getpid() << ", tid = " << CurrentThread::tid();
    muduo::net::EventLoop loop;
    muduo::net::InetAddress listenAddr(9500);
    DeviceServer::MQTTServer server(&loop, listenAddr);
    server.start();
    loop.loop();
    return 0;
}

Hay dos clases muy centrales aquí, EventLoop y muduo :: net :: TcpServer. Siga esta pista y continúe mirando TcpServer. ¿Qué hace el comienzo? Viendo aquí, realmente pensaré en dos lugares, ¿qué hace el constructor TcpServer? ? ¿Qué empezó a hacer?

Codificar directamente

TcpServer::TcpServer(EventLoop* loop,
                     const InetAddress& listenAddr,
                     const string& nameArg,
                     Option option)
  : loop_(CHECK_NOTNULL(loop)),
    ipPort_(listenAddr.toIpPort()),
    name_(nameArg),
    acceptor_(new Acceptor(loop, listenAddr, option == kReusePort)),
    threadPool_(new EventLoopThreadPool(loop, name_)),
    connectionCallback_(defaultConnectionCallback),
    messageCallback_(defaultMessageCallback),
    nextConnId_(1)
{
  acceptor_->setNewConnectionCallback(
      std::bind(&TcpServer::newConnection, this, _1, _2));
}

Los pasos centrales

Eventloop inicializado.

loop_(CHECK_NOTNULL(loop))

Grupo de subprocesos inicializado

threadPool_(new EventLoopThreadPool(loop, name_))

Receptor tcp inicializado

acceptor_(new Acceptor(loop, listenAddr, option == kReusePort))

De acuerdo, sigamos viendo qué hace el comienzo a continuación

void TcpServer::start()
{
  if (started_.getAndSet(1) == 0)
  {
    threadPool_->start(threadInitCallback_);

    assert(!acceptor_->listenning());
    loop_->runInLoop(
        std::bind(&Acceptor::listen, get_pointer(acceptor_)));
  }
}

Después de comenzar, hicimos algo importante, es decir, comenzamos el grupo de subprocesos. ¿Seguimos pensando en la línea del pensamiento? ¿Qué hace el constructor del grupo de subprocesos? ¿Qué empezó a hacer el grupo de subprocesos?

Primero mira el constructor del grupo de subprocesos

EventLoopThreadPool::EventLoopThreadPool(EventLoop* baseLoop, const string& nameArg)
  : baseLoop_(baseLoop),
    name_(nameArg),
    started_(false),
    numThreads_(0),
    next_(0)
{
}

La función principal es inicializar el baseLoop, otro atributo importante es el número de subprocesos numThreads

Es bueno seguir viendo el comienzo

void EventLoopThreadPool::start(const ThreadInitCallback& cb)
{
  assert(!started_);
  baseLoop_->assertInLoopThread();

  started_ = true;

  for (int i = 0; i < numThreads_; ++i)
  {
    char buf[name_.size() + 32];
    snprintf(buf, sizeof buf, "%s%d", name_.c_str(), i);
    EventLoopThread* t = new EventLoopThread(cb, buf);
    threads_.push_back(std::unique_ptr<EventLoopThread>(t));
    loops_.push_back(t->startLoop());
  }
  if (numThreads_ == 0 && cb)
  {
    cb(baseLoop_);
  }
}

Vea que el código es fácil de entender, muduo lanzó subprocesos numThreads , guardando objetos en el grupo de subprocesos en

Presta atención a lo que es cb, cb

const ThreadInitCallback& cb

Es la función de inicialización del hilo, que es la dirección de devolución de llamada de setThreadInitCallback en muduo

También hay una operación muy central en EventLoopThreadPool-> start, EventLoopThread. Aún debemos prestar atención a lo que inicializa y lo que hace startLoop. Podemos ver que si el número de subprocesos es 0,

  if (numThreads_ == 0 && cb)
  {
    cb(baseLoop_);
  }

Ejecutará la función de inicialización del hilo directamente, si no, entrará en EventLoopThread cuando se inicialice. Echemos un vistazo al código constructor de EventLoopThread.

EventLoopThread::EventLoopThread(const ThreadInitCallback& cb,
                                 const string& name)
  : loop_(NULL),
    exiting_(false),
    thread_(std::bind(&EventLoopThread::threadFunc, this), name),
    mutex_(),
    cond_(mutex_),
    callback_(cb)
{
}

Inicializado thread_ mutex_ cond_ callback_ (devolución de llamada de inicialización del hilo), inicializamos algunos componentes importantes del hilo e instancias del hilo, y luego miramos directamente startLoop

EventLoop* EventLoopThread::startLoop()
{
  assert(!thread_.started());
  thread_.start();

  EventLoop* loop = NULL;
  {
    MutexLockGuard lock(mutex_);
    while (loop_ == NULL)
    {
      cond_.wait();
    }
    loop = loop_;
  }

  return loop;
}

Aquí hay realmente un fragmento de código muy interesante. Inicie el hilo en inicio, y luego use la variable de condición para esperar afuera hasta que el EventLoop se inicialice en el hilo y luego asígnelo al bucle. Este código usa las variables de condición de glibc muy Demuestra que las variables de condición son una herramienta poderosa en la programación multiproceso. pthread_create se llama al inicio, ¿no lo cree? ¡Continúe mirando el código en Thread-> start!

void Thread::start()
{
  assert(!started_);
  started_ = true;
  // FIXME: move(func_)
  detail::ThreadData* data = new detail::ThreadData(func_, name_, &tid_, &latch_);
  if (pthread_create(&pthreadId_, NULL, &detail::startThread, data))
  {
    started_ = false;
    delete data; // or no delete?
    LOG_SYSFATAL << "Failed in pthread_create";
  }
  else
  {
    latch_.wait();
    assert(tid_ > 0);
  }
}

Una vez creado el hilo, el código de inicio del hilo está en startThread y seguimos pensando en lo que se hace en startThread. , Tengo que decir muduo aquí, la clase Thread está diseñada con su ingenio

int Thread::join()
{
  assert(started_);
  assert(!joined_);
  joined_ = true;
  return pthread_join(pthreadId_, NULL);
}

Thread::~Thread()
{
  if (started_ && !joined_)
  {
    pthread_detach(pthreadId_);
  }
}

Puede usar join para monitorear la sincronización. Por supuesto, si no hace esto, no hay problema. Si el destructor de Thread encuentra que el hilo no está cerrado cuando destruya el Thread de nuevo, llamará a pthread_detach para evitar su hilo de fugas. ¿No es inteligente el diseño?

Bueno, ¡pensemos en lo que se hace en startThread!

void* startThread(void* obj)
{
  ThreadData* data = static_cast<ThreadData*>(obj);
  data->runInThread();
  delete data;
  return NULL;
}

Realmente llamado en el hilo

 thread_(std::bind(&EventLoopThread::threadFunc, this), name),

threadFunc

void EventLoopThread::threadFunc()
{
  EventLoop loop;

  if (callback_)
  {
    callback_(&loop);
  }

  {
    MutexLockGuard lock(mutex_);
    loop_ = &loop;
    cond_.notify();
  }

  loop.loop();
  //assert(exiting_);
  MutexLockGuard lock(mutex_);
  loop_ = NULL;
}

Active la función de inicialización del hilo, inicialice el bucle y notifique al hilo principal que la inicialización está completa cond.notify ();

Un alcance muy inteligente reduce el alcance del bloqueo y luego inicia el ciclo de eventos. Bueno, usamos una imagen para analizar todo el proceso. Tenga en cuenta que el punto clave es que el ciclo de eventos de cada hilo se almacenará en el hilo después de la Se completa la inicialización de cada hilo. En el std :: vector _loops del grupo, está muy expuesto a TcpServer para facilitar la comunicación entre TcpServer y los multi-hilos del grupo de hilos.

 

 

¿Qué se hace en cada hilo del grupo de hilos y qué se hace en Tcpserver? ¡Cómo llevar a cabo la comunicación de eventos entre hilos es la pregunta que debe considerarse!

 

Acceptor tiene dos atributos importantes, Channel y sockfd. Cuando se inicializa Acceptor, se asigna sockfd a acceptChannel_

Acceptor::Acceptor(EventLoop* loop, const InetAddress& listenAddr, bool reuseport)
  : loop_(loop),
    acceptSocket_(sockets::createNonblockingOrDie(listenAddr.family())),
    acceptChannel_(loop, acceptSocket_.fd()),
    listenning_(false),
    idleFd_(::open("/dev/null", O_RDONLY | O_CLOEXEC))
{
  assert(idleFd_ >= 0);
  acceptSocket_.setReuseAddr(true);
  acceptSocket_.setReusePort(reuseport);
  acceptSocket_.bindAddress(listenAddr);
  acceptChannel_.setReadCallback(
      std::bind(&Acceptor::handleRead, this));
}

Vemos que se llama a handleRead cuando la canalización recibe un evento legible, echemos un vistazo a handleRead de nuevo

void Acceptor::handleRead()
{
  loop_->assertInLoopThread();
  InetAddress peerAddr;
  //FIXME loop until no more
  int connfd = acceptSocket_.accept(&peerAddr);
  if (connfd >= 0)
  {
    // string hostport = peerAddr.toIpPort();
    // LOG_TRACE << "Accepts of " << hostport;
    if (newConnectionCallback_)
    {
      newConnectionCallback_(connfd, peerAddr);
    }
    else
    {
      sockets::close(connfd);
    }
  }
  else
  {
    LOG_SYSERR << "in Acceptor::handleRead";
    // Read the section named "The special problem of
    // accept()ing when you can't" in libev's doc.
    // By Marc Lehmann, author of libev.
    if (errno == EMFILE)
    {
      ::close(idleFd_);
      idleFd_ = ::accept(acceptSocket_.fd(), NULL, NULL);
      ::close(idleFd_);
      idleFd_ = ::open("/dev/null", O_RDONLY | O_CLOEXEC);
    }
  }
}

Si entra una nueva conexión, llame a newConnectionCallback , ¿qué es newConnectionCallback ? Resultó ser una devolución de llamada enlazada en TcpServer

acceptor_->setNewConnectionCallback(
      std::bind(&TcpServer::newConnection, this, _1, _2));

Eche un vistazo más de cerca a lo que hace newConnection

void TcpServer::newConnection(int sockfd, const InetAddress& peerAddr)
{
  loop_->assertInLoopThread();
  EventLoop* ioLoop = threadPool_->getNextLoop();
  char buf[64];
  snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);
  ++nextConnId_;
  string connName = name_ + buf;

  LOG_INFO << "TcpServer::newConnection [" << name_
           << "] - new connection [" << connName
           << "] from " << peerAddr.toIpPort();
  InetAddress localAddr(sockets::getLocalAddr(sockfd));
  // FIXME poll with zero timeout to double confirm the new connection
  // FIXME use make_shared if necessary
  TcpConnectionPtr conn(new TcpConnection(ioLoop,
                                          connName,
                                          sockfd,
                                          localAddr,
                                          peerAddr));
  connections_[connName] = conn;
  conn->setConnectionCallback(connectionCallback_);
  conn->setMessageCallback(messageCallback_);
  conn->setWriteCompleteCallback(writeCompleteCallback_);
  conn-setCloseCallback(
      std::bind(&TcpServer::removeConnection, this, _1)); // FIXME: unsafe
  ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
}

Genere de forma abstracta un TcpConnectionPtr, luego llame a runInLoop, continúe observando el código runInLoop

void EventLoop::runInLoop(Functor cb)
{
  if (isInLoopThread())
  {
    cb();
  }
  else
  {
    queueInLoop(std::move(cb));
  }
}

Continuar para ver queueInLoop

void EventLoop::queueInLoop(Functor cb)
{
  {
  MutexLockGuard lock(mutex_);
  pendingFunctors_.push_back(std::move(cb));
  }

  if (!isInLoopThread() || callingPendingFunctors_)
  {
    wakeup();
  }
}

Suspenda la función que se iniciará en pendientesFunctores_ y luego llame a wakeup para activar el hilo correspondiente

void EventLoop::wakeup()
{
  uint64_t one = 1;
  ssize_t n = sockets::write(wakeupFd_, &one, sizeof one);
  if (n != sizeof one)
  {
    LOG_ERROR << "EventLoop::wakeup() writes " << n << " bytes instead of 8";
  }
}

Piense en cómo encontrar el hilo correspondiente cuando se produce la conexión, consulte aquí

EventLoop* ioLoop = threadPool_->getNextLoop();

Combinado con la inicialización de EventPool

EventLoop::EventLoop()
  : looping_(false),
    quit_(false),
    eventHandling_(false),
    callingPendingFunctors_(false),
    iteration_(0),
    threadId_(CurrentThread::tid()),
    poller_(Poller::newDefaultPoller(this)),
    timerQueue_(new TimerQueue(this)),
    wakeupFd_(createEventfd()),
    wakeupChannel_(new Channel(this, wakeupFd_)),
    currentActiveChannel_(NULL)
{
  LOG_DEBUG << "EventLoop created " << this << " in thread " << threadId_;
  if (t_loopInThisThread)
  {
    LOG_FATAL << "Another EventLoop " << t_loopInThisThread
              << " exists in this thread " << threadId_;
  }
  else
  {
    t_loopInThisThread = this;
  }
  wakeupChannel_->setReadCallback(
      std::bind(&EventLoop::handleRead, this));
  // we are always reading the wakeupfd
  wakeupChannel_->enableReading();
}

Bueno, es básicamente claro y finalmente usa un diagrama de flujo de resumen

 

 

Finalmente, la activación mutua entre las tuberías se realiza a través de eventfd, y cada tubería usa wakefd en Eventloop, que es eventfd para activarse entre sí.

Supongo que te gusta

Origin blog.csdn.net/qq_32783703/article/details/108014372
Recomendado
Clasificación