深度解析Muduo库中的SubReatcor唤醒操作【万字解读】

文章目录

  • 前言
  • 一、eventfd是什么
  • 二、eventfd与I/O多路复用结合
    • 为什么能与IO多路复用结合
    • 例子
  • 三、eventfd在muduo库中的应用
    • 前置知识,简单介绍一下channel与poller类
    • 1、为什么需要唤醒
    • 2、创建wakeupFd_与wakeupChannel_
    • 3、唤醒操作wakeup
    • 4、任务添加唤醒runInLoop
    • 5、执行回调doPendingFunctors
    • 6、TCPServer启动监听acceptSocket_与acceptChannel_
    • 7、newConnection新连接回调

前言

最近在看muduo库源码的时候注意到了一种比较陌生的线程间通讯方式,他的作用是唤醒subreactor,他是eventfd来实现的。本文就详细介绍与实现一下eventfd。先简单介绍一下什么是eventfd,然后举一个简单的例子,最后总结eventfd在muduo库中的实战应用。

一、eventfd是什么

从Linux 2.6.27版本开始增加了eventfd,主要用于进程或者线程间的通信(如通知/等待机制的实现)。eventfd是Linux内核提供的一种进程间通信方式,它为创建一个“事件文件描述符”提供了一个系统调用,可以用于在不同进程间传递事件信号,也可以在信号通知后等待读取该事件。

**eventfd使用一种文件描述符来传递事件信息。**在一个进程中,创建eventfd时,同时会创建一个文件描述符,可以对该文件描述符进行read和write操作。对文件描述符的写操作每次都会向内核中的计数器增加一个值,对文件描述符的读操作每次都会读取当前计数器的值并清零。当写操作完成后,其它等待该事件的进程会得到通知。

Eventfd在信号通知的场景下,相对比pipe有非常大的资源和性能优势, 本质其实是counter(计数器)和channel(数据信道)的区别。
打开文件数量的差异 由于pipe是半双工的传统IPC实现方式,所有两个线程通信需要两个pipe文件描述符,而用eventfd只需要打开一个文件描述符。 总所周知,文件描述符是系统中非常宝贵的资源,linux的默认值只有1024个而已,pipe只能在两个进程/线程间使用,面向连接,使用之前就需要创建好两个pipe,而eventfd是广播式的通知,可以多对多。
内存使用的差别 eventfd是一个计数器,内核维护的成本非常低,大概是自旋锁+唤醒队列的大小,8个字节的传输成本也微乎其微,而pipe完全不同,一来一回数据在用户空间和内核空间有多达4次的复制,而且最糟糕的是,内核要为每个pipe分配最少4k的虚拟内存页,哪怕传送的数据长度为0.

eventfd作为一种多进程通信方式,与其它通信方式相比,具有以下优缺点:

优点:
1、eventfd机制简单,易于使用
2、读写操作与文件描述符操作无区别,使用非常方便
3、eventfd实现了计数器功能,可以用于数值传递
4、eventfd的通知功能和信号一样处理,可以使用select、poll、epoll等I/O多路复用函数监听
缺点:
1、eventfd不能使用在进程间通信,只能用于父子进程间的通信
2、不能设置Event Filter,即不能选择事件类型,只能向计数器增加或减少数值事件

二、eventfd与I/O多路复用结合

为什么能与IO多路复用结合

由于eventfd的通知功能和信号一样处理,可以使用select、poll、epoll等I/O多路复用函数监听。eventfd设计之初就与epoll完美结合,支持非阻塞读取等,就是为epoll而生的,而pipe是Unix时代就有了,那时候不仅没有epoll,连linux还没诞生。
当pipe只用来发送通知,放弃它,放心的使用eventfd。
eventfd配合epoll才是它存在的原

例子

首先,代码创建了一个 eventfd 实例和一个 epoll 实例。然后,将 eventfd 添加到 epoll 监听集合中,设定监听事件为 EPOLLIN(可读事件)。接着创建了一个子线程 eventfd_child_Task,用于定期向 eventfd 写入数据。

在主线程中,使用 epoll_wait 函数等待事件的到来。当有事件发生时,会从 eventfd 读取数据,并进行相应的处理。在示例代码中,只是简单地打印读取的数据。

eventfd 具体的作用是通过在不同线程之间传递无符号64位整数值来进行通信。它提供了一个文件描述符,可以被读取和写入。通过调用 eventfd_write 函数向 eventfd 写入数据,而调用 eventfd_read 函数则从 eventfd 读取数据。当有数据被写入 eventfd 时,会触发 epoll 监听集合中的可读事件,从而实现线程间的事件通知机制。

eventfd 的底层原理是使用一个无符号64位的计数器。当调用 eventfd_write 写入数据时,会将写入的数据加到计数器上。而调用 eventfd_read 读取数据时,会将计数器中的值读出,并将其清零。当计数器的值为非零时,可以被认为是一个事件触发。这种方式使得 eventfd 可以实现多生产者、多消费者的机制,非常适合用于线程间的同步与通信。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

int g_iEvtfd = -1;

void *eventfd_child_Task(void *pArg)
{
	//生产者调用write写一个64bit的整数value到eventfd即可
	uint64_t uiWrite = 1;

	while(1)
	{
		sleep(2);
		if (0 != eventfd_write(g_iEvtfd, uiWrite))
		{
			printf("child write iEvtfd failed\n");
		}
	}

	return;
}

int main(int argc, char**argv[])
{
	int iEvtfd, j;
	uint64_t uiWrite = 1;
	uint64_t uiRead;
	ssize_t s;
	int iEpfd;
	struct epoll_event stEvent;
	int iRet = 0;
	struct epoll_event stEpEvent;
	pthread_t stWthread;

	iEpfd = epoll_create(1);
	if (-1 == iEpfd)
	{
		printf("Create epoll failed.\n");
		return 0;
	}

	iEvtfd = eventfd(0,0);
	if (-1 == iEvtfd)
	{
		printf("failed to create eventfd\n");
		return 0;
	}

	g_iEvtfd = iEvtfd;

	memset(&stEvent, 0, sizeof(struct epoll_event));
	stEvent.events = (unsigned long) EPOLLIN;
	stEvent.data.fd = iEvtfd;
	iRet = epoll_ctl(iEpfd, EPOLL_CTL_ADD, g_iEvtfd, &stEvent);
	if (0 != iRet)
	{
		printf("failed to add iEvtfd to epoll\n");
		close(g_iEvtfd);
		close(iEpfd);
		return 0;
	}

	iRet = pthread_create(&stWthread, NULL, eventfd_child_Task, NULL);
	if (0 != iRet)
	{
		close(g_iEvtfd);
		close(iEpfd);
		return;
	}

	for(;;)
	{
		//1 -1 表示 epoll 数量,无限等待
		iRet = epoll_wait(iEpfd, &stEpEvent, 1, -1);
		if (iRet > 0)
		{
			s = eventfd_read(iEvtfd, &uiRead);
			if (s != 0)
			{
				printf("read iEvtfd failed\n");
				break;
			}
			printf("Read %llu (0x%llx) from iEvtfd\n", uiRead, uiRead);
		}
	}

	close(g_iEvtfd);
	close(iEpfd);
	return 0;
}

三、eventfd在muduo库中的应用

eventfd在muduo库中 完成对eventloop的唤醒操作

前置知识,简单介绍一下channel与poller类

Channel类和EpollPoller类是Muduo库中与事件驱动模型紧密相关的两个核心类。
1. Channel类:
Channel类封装了事件的处理逻辑,负责注册、删除和处理文件描述符上的事件。它包含以下主要成员函数和作用:

  • Channel(int fd):构造函数,初始化Channel对象,并关联一个文件描述符。
  • handleEvent():处理事件回调函数,由EpollPoller触发并调用。根据当前激活的事件类型,执行相应的操作。
  • setReadCallback()setWriteCallback()setErrorCallback():设置读、写、错误事件的回调函数。
  • enableReading()enableWriting()disableWriting():启用或禁用读、写事件的监听。
  • update():更新关注的事件类型,由EpollPoller调用。
  • remove():将Channel从EpollPoller中移除。

总结作用:向epollpoller注册回调事件,绑定感兴趣的fd

2. EpollPoller类:
EpollPoller类封装了epoll事件驱动模型,负责事件的监听和分发。它包含以下主要成员函数和作用:

  • epoll_create():创建epoll实例。
  • epoll_ctl():用于向epoll实例中注册、修改或删除事件。
  • epoll_wait():用于等待事件的发生。
  • fillActiveChannels():将激活的事件添加到activeChannels_列表中。
  • updateChannel()removeChannel():更新和移除Channel对象。

EpollPoller类还包含了一个成员变量 std::vector activeChannels_,用于存储当前激活的Channel对象。它通过调用Channel的回调函数来处理事件。

总结作用:封装epoll事件驱动模型,更新和管理channel,为eventloop提供激活的Channel对象activeChannels_

Muduo库使用EpollPoller类封装了底层的epoll系统调用,将其与Channel类结合起来实现了高效的事件驱动模型。Channel对象负责封装事件的处理逻辑,而EpollPoller对象负责监听和分发事件。通过回调函数机制

1、为什么需要唤醒

我们先来看看eventloop的loop是如何自信的。循环体中进行这几件事情:
1、使用poller_对象进行轮询,等待事件发生;
2、将触发事件的Channel对象存储在activeChannels_容器中。
3、遍历activeChannels_容器中的每个Channel对象;处理活跃的Channel事件:
4、调用doPendingFunctors()方法,执行当前EventLoop事件循环需要处理的回调操作
这些回调操作通常是由其他线程通过runInLoop()函数添加到当前EventLoop的任务队列中的。

void EventLoop::loop()
{
    looping_ = true;
    quit_ = false;

    LOG_INFO("EventLoop %p start looping \n", this);

    while(!quit_)
    {
        activeChannels_.clear();
        // 监听两类fd   一种是client的fd,一种wakeupfd
        pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_);
        for (Channel *channel : activeChannels_)
        {
            // Poller监听哪些channel发生事件了,然后上报给EventLoop,通知channel处理相应的事件
            channel->handleEvent(pollReturnTime_);
        }
        // 执行当前EventLoop事件循环需要处理的回调操作
        /**
         * IO线程 mainLoop accept fd《=channel subloop
         * mainLoop 事先注册一个回调cb(需要subloop来执行)    wakeup subloop后,执行下面的方法,执行之前mainloop注册的cb操作
         */ 
        doPendingFunctors();
    }

    LOG_INFO("EventLoop %p stop looping. \n", this);
    looping_ = false;
}

doPendingFunctors中包含了添加channel或者其他的一些操作,但是
如果没有唤醒操作,下面poll这一步就会永远阻塞,没有活跃的channel就执行不了下面的doPendingFunctors回调函数列表。

  pollReturnTime_ = poller_->poll(kPollTimeMs, &activeChannels_); z阻塞在这里

所以eventloop要进行添加channel或者其他的一些事件循环需要处理的回调操作的时候就需要唤醒,然后才能执行doPendingFunctors回调

2、创建wakeupFd_与wakeupChannel_

wakeupChannel_绑定感兴趣的fd是wakeupFd_,用来进行唤醒操作。

int createEventfd()
{
    int evtfd = ::eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
    if (evtfd < 0)
    {
        LOG_FATAL("eventfd error:%d \n", errno);
    }
    return evtfd;
}
EventLoop::EventLoop()
    : looping_(false)
    , quit_(false)
    , callingPendingFunctors_(false)
    , threadId_(CurrentThread::tid())
    , poller_(Poller::newDefaultPoller(this))
    , wakeupFd_(createEventfd())
    , wakeupChannel_(new Channel(this, wakeupFd_))

wakeupChannel_绑定唤醒回调handleRead,这里的唤醒回调就是简单的读操作,没什么别的作用仅仅是唤醒

  wakeupChannel_->setReadCallback(std::bind(&EventLoop::handleRead, this));
  
void EventLoop::handleRead()
{
  uint64_t one = 1;
  ssize_t n = read(wakeupFd_, &one, sizeof one);
  if (n != sizeof one)
  {
    LOG_ERROR("EventLoop::handleRead() reads %lu bytes instead of 8", n);
  }
}

3、唤醒操作wakeup

唤醒操作就是简单从wakeupfd_写一个字符。就会激活对应的wakeupChannel_,然后epoller监听到wakeupChannel_活跃就会解除阻塞先下执行。

// 用来唤醒loop所在的线程的  向wakeupfd_写一个数据,wakeupChannel就发生读事件,当前loop线程就会被唤醒
void EventLoop::wakeup()
{
    uint64_t one = 1;
    ssize_t n = write(wakeupFd_, &one, sizeof one);
    if (n != sizeof one)
    {
        LOG_ERROR("EventLoop::wakeup() writes %lu bytes instead of 8 \n", n);
    }
}

4、任务添加唤醒runInLoop

不能无缘无故的唤醒,唤醒的时机是添加一个要执行的回调,比如说mainloop向subloop添加一个添加新的channel的操作,就需要调用runInLoop操作

`// 在当前loop中执行cb
void EventLoop::runInLoop(Functor cb)
{
    if (isInLoopThread()) // 在当前的loop线程中,执行cb
    {
        cb();
    }
    else // 在非当前loop线程中执行cb , 就需要唤醒loop所在线程,执行cb
    {
        queueInLoop(cb);
    }
}

// 把cb放入队列中,唤醒loop所在的线程,执行cb
void EventLoop::queueInLoop(Functor cb)
{
    {
        std::unique_lock<std::mutex> lock(mutex_);
        pendingFunctors_.emplace_back(cb);
    }

    // 唤醒相应的,需要执行上面回调操作的loop的线程了
    // || callingPendingFunctors_的意思是:当前loop正在执行回调,但是loop又有了新的回调
    if (!isInLoopThread() || callingPendingFunctors_) 
    {
        wakeup(); // 唤醒loop所在线程
    }
}

runInLoop(Functor cb)方法首先判断当前是否在EventLoop所在线程中,如果是,则直接执行回调函数cb();如果不是,则调用queueInLoop(cb)方法将回调函数加入待处理队列,并唤醒EventLoop所在线程。

queueInLoop(Functor cb)方法将回调函数cb加入待处理队列pendingFunctors_,并根据条件决定是否唤醒EventLoop所在线程。如果当前不在EventLoop所在线程中,或者EventLoop正在执行回调函数(callingPendingFunctors_为true),则调用wakeup()方法唤醒EventLoop所在线程。

5、执行回调doPendingFunctors

执行回调 比如说加入新的连接,这里面新建一个副本functors然后把pendingFunctors_的数据全部转移到这个副本中去,这样做是为了,在执行回调的时候,如果有新的回调进来的话,就不会发生误读。

void EventLoop::doPendingFunctors() // 执行回调  比如说加入新的连接
//    ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
{
    std::vector<Functor> functors;
    callingPendingFunctors_ = true;

    {
        std::unique_lock<std::mutex> lock(mutex_);
        // 清空
        functors.swap(pendingFunctors_);
    }
    for (const Functor &functor : functors)
    {
        functor(); // 执行当前loop需要执行的回调操作
    }
    callingPendingFunctors_ = false;
}


6、TCPServer启动监听acceptSocket_与acceptChannel_

启动一个mainloop,绑定的回调是Acceptor::listen

// 开启服务器监听   loop.loop()
void TcpServer::start()
{
    if (started_++ == 0) // 防止一个TcpServer对象被start多次
    {
        threadPool_->start(threadInitCallback_); // 启动底层的loop线程池
        
        loop_->runInLoop(std::bind(&Acceptor::listen, acceptor_.get()));
    }
}

Accepor中保存有acceptChannel_监听的是感兴趣acceptSocket_也就是每当有新链接来得时就执行回调。handleRead

Acceptor::Acceptor(EventLoop *loop, const InetAddress &listenAddr, bool reuseport)
    : loop_(loop)
    , acceptSocket_(createNonblocking()) // socket
    , listenning_(false)
     //这是mainreactor channel感兴趣的就是acceptSocket_  
    //  如果感兴趣的acceptSocket_ 有动静就执行回调handleRead
    , acceptChannel_(loop, acceptSocket_.fd())
{
    acceptSocket_.setReuseAddr(true);
    acceptSocket_.setReusePort(true);
    acceptSocket_.bindAddress(listenAddr); // bind绑定地址与端口
    // TcpServer::start() Acceptor.listen  有新用户的连接,要执行一个回调(connfd=》channel=》subloop)
    // baseLoop => acceptChannel_(listenfd) => 
    // 注册有新用户连接后的回调 
    acceptChannel_.setReadCallback(std::bind(&Acceptor::handleRead, this));
}

handleRead做的事情就是拿到新链接的clientfd然后 调用newConnectionCallback_回调,这个回调是TCPserver绑定的。


// listenfd有事件发生了,就是有新用户连接了
void Acceptor::handleRead()
{
    InetAddress peerAddr;
    // 拿到 连接的clientfd
    int connfd = acceptSocket_.accept(&peerAddr);
    if (connfd >= 0)
    {
        // TCPserver传进来的回调
        if (newConnectionCallback_)
        {
            newConnectionCallback_(connfd, peerAddr); 
            // 轮询找到subLoop,唤醒,分发当前的新客户端的Channel
        }
        else
        {
            ::close(connfd);
        }
    }
    else
    {
        LOG_ERROR("%s:%s:%d accept err:%d \n", __FILE__, __FUNCTION__, __LINE__, errno);
        if (errno == EMFILE)
        {
            LOG_ERROR("%s:%s:%d sockfd reached limit! \n", __FILE__, __FUNCTION__, __LINE__);
        }
    }
}

7、newConnection新连接回调

newConnection绑定给acceptor,每当有新链接来的时候就执行。

TcpServer::TcpServer(EventLoop *loop,
                const InetAddress &listenAddr,
                const std::string &nameArg,
                Option option)
                : loop_(CheckLoopNotNull(loop))
                , ipPort_(listenAddr.toIpPort())
                , name_(nameArg)
                // mainloop 
                , acceptor_(new Acceptor(loop, listenAddr, option == kReusePort))
            //    主loop加入 线程池
                , threadPool_(new EventLoopThreadPool(loop, name_))
                , connectionCallback_()
                , messageCallback_()
                , nextConnId_(1)
                , started_(0)
{
    // 当有先用户连接时,会执行TcpServer::newConnection回调
    acceptor_->setNewConnectionCallback(std::bind(&TcpServer::newConnection, this, 
        std::placeholders::_1, std::placeholders::_2));
}

重点:经过轮询算法选取到合适的subreactor或者subloop,,通过ioLoop->runInLoop()将TcpConnection的connectEstablished()方法绑定为回调函数,在IO线程中执行该函数。connectEstablished()方法会在新连接建立时被调用,主要用于注册该连接的事件监听。
runInLoop就是上面介绍到的,如果不是本地loop就会进行唤醒操作来执行connectEstablished函数


/ 有一个新的客户端的连接,acceptor会执行这个回调操作
void TcpServer::newConnection(int sockfd, const InetAddress &peerAddr)
{
    // 轮询算法,选择一个subLoop,来管理channel
    // 封装channel
    EventLoop *ioLoop = threadPool_->getNextLoop(); 
    char buf[64] = {0};
    snprintf(buf, sizeof buf, "-%s#%d", ipPort_.c_str(), nextConnId_);
    ++nextConnId_;
    std::string connName = name_ + buf;

    LOG_INFO("TcpServer::newConnection [%s] - new connection [%s] from %s \n",
        name_.c_str(), connName.c_str(), peerAddr.toIpPort().c_str());

    // 通过sockfd获取其绑定的本机的ip地址和端口信息
    sockaddr_in local;
    ::bzero(&local, sizeof local);
    socklen_t addrlen = sizeof local;
    if (::getsockname(sockfd, (sockaddr*)&local, &addrlen) < 0)
    {
        LOG_ERROR("sockets::getLocalAddr");
    }
    InetAddress localAddr(local);

    // 根据连接成功的sockfd,创建TcpConnection连接对象
    TcpConnectionPtr conn(new TcpConnection(
                            ioLoop,
                            connName,
                            sockfd,   // Socket Channel
                            localAddr,
                            peerAddr));
    connections_[connName] = conn;
    // 下面的回调都是用户设置给TcpServer=>TcpConnection=>Channel=>Poller=>notify channel调用回调
    conn->setConnectionCallback(connectionCallback_);
    conn->setMessageCallback(messageCallback_);
    conn->setWriteCompleteCallback(writeCompleteCallback_);

    // 设置了如何关闭连接的回调   conn->shutDown()
    conn->setCloseCallback(
        std::bind(&TcpServer::removeConnection, this, std::placeholders::_1)
    );

    // 直接调用TcpConnection::connectEstablished
    // runInLoop wakeup fd会唤醒这个loop 去执行connectEstablished,绑定新的连接
    ioLoop->runInLoop(std::bind(&TcpConnection::connectEstablished, conn));
}

你可能感兴趣的:(linux,开发语言,c++,服务器)