最近在看muduo库源码的时候注意到了一种比较陌生的线程间通讯方式,他的作用是唤醒subreactor,他是eventfd来实现的。本文就详细介绍与实现一下eventfd。先简单介绍一下什么是eventfd,然后举一个简单的例子,最后总结eventfd在muduo库中的实战应用。
从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的通知功能和信号一样处理,可以使用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库中 完成对eventloop的唤醒操作
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
,用于存储当前激活的Channel对象。它通过调用Channel的回调函数来处理事件。
总结作用:封装epoll事件驱动模型,更新和管理channel,为eventloop提供激活的Channel对象activeChannels_
Muduo库使用EpollPoller类封装了底层的epoll系统调用,将其与Channel类结合起来实现了高效的事件驱动模型。Channel对象负责封装事件的处理逻辑,而EpollPoller对象负责监听和分发事件。通过回调函数机制,
我们先来看看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回调
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);
}
}
唤醒操作就是简单从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);
}
}
不能无缘无故的唤醒,唤醒的时机是添加一个要执行的回调,比如说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所在线程。
执行回调 比如说加入新的连接,这里面新建一个副本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;
}
启动一个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__);
}
}
}
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));
}