操作系统知识点总结--eventfd实现线程事件通知机制

什么是eventfd

eventfd是Linux 2.6提供的一种系统调用,它可以用来实现事件通知。eventfd包含一个由内核维护的64位无符号整型计数器,创建eventfd时会返回一个文件描述符,进程/线程可以通过对这个文件描述符进行read/write来读取/改变计数器的值,从而实现进程/线程间通信。

eventfd

eventfd - 事件通知文件描述符
eventfd系统函数 - 创建一个能被用户应用程序用于时间等待唤醒机制的eventfd对象。
eventfd 单纯的使用文件描述符实现的线程间的通知机制,可以很好的融入select、poll、epoll的I/O复用机制中。

创建eventfd

#include 
int eventfd(unsigned int initval ,int flags );
参数意义:
initval:

创建eventfd时它所对应的64位计数器的初始值;

flags:

eventfd文件描述符的标志,可由三种选项组成:EFD_CLOEXEC、EFD_NONBLOCK和EFD_SEMAPHORE。

  1. EFD_CLOEXEC:表示返回的eventfd文件描述符在fork后exec其他程序时会自动关闭这个文件描述符;
  2. EFD_NONBLOCK:设置返回的eventfd非阻塞;
  3. EFD_SEMAPHORE: 表示将eventfd作为一个信号量来使用。

读eventfd read(2)

1.read函数会从eventfd对应的64位计数器中读取一个8字节的整型变量;
2.read函数设置的接收buf的大小不能低于8个字节,否则read函数会出错,errno为EINVAL;
3.read函数返回的值是按小端字节序的;
4.如果eventfd设置了EFD_SEMAPHORE,那么每次read就会返回1,并且让eventfd对应的计数器减一;如果eventfd没有设置EFD_SEMAPHORE,那么每次read就会直接返回计数器中的数值,read之后计数器就会置0。不管是哪一种,当计数器为0时,如果继续read,那么read就会阻塞(如果eventfd没有设置EFD_NONBLOCK)或者返回EAGAIN错误(如果eventfd设置了EFD_NONBLOCK)。

写eventfd write(2)

1.在没有设置EFD_SEMAPHORE的情况下,write函数会将发送buf中的数据写入到eventfd对应的计数器中,最大只能写入0xffffffffffffffff,否则返回EINVAL错误;
2.在设置了EFD_SEMAPHORE的情况下,write函数相当于是向计数器中进行“添加”,比如说计数器中的值原本是2,如果write了一个3,那么计数器中的值就变成了5。如果某一次write后,计数器中的值超过了0xfffffffffffffffe(64位最大值-1),那么write就会阻塞直到另一个进程/线程从eventfd中进行了read(如果write没有设置EFD_NONBLOCK),或者返回EAGAIN错误(如果write设置了EFD_NONBLOCK)。

除此之外,eventfd还支持select和poll,与一般的读写描述符相类似。

EventLoop对eventfd的封装

所增加的接口及成员:

typedef std::function Functor;
    //如果用户在当前IO线程调用这个函数, 回调会同步进行; 如果用户在其他线程调用runInLoop(),cb会被加入队列, IO线程会被唤醒来调用这个Functor。
    void runInLoop(const Functor& cb);

    //写已注册到poll的eventfd 通知poll 处理读事件。
    void wakeup(); //是写m_wakeupFd 通知poll 处理读事件。

    //会将回调添加到容器,同时通过wakeup()唤醒poll()调用容器内的回调。
    void queueInLoop(const Functor& cb);

private:
    //used to waked up

    //poll回调读事件,处理eventfd
    void handleRead();

    //处理挂起的事件
    void doPendingFunctors();


    int m_wakeupFd;
    std::unique_ptr p_wakeupChannel;
    mutable MutexLock m_mutex;
    bool m_callingPendingFunctors; /* atomic */
    std::vector m_pendingFunctors; // @GuardedBy mutex_

工作时序:
(runInLoop() -> quueInLoop())/queueInLoop() -> wakeup() -> poll() -> handleRead() -> doPendingFunctors()

runInLoop()
void EventLoop::runInLoop(const Functor&  cb)
{
  if(isInloopThread())
    cb();
  else
    queueInLoop(cb);
}
  1. EventLoop大多数时间都在运行loop中的循环,也就是说想要在本线程内执行runInLoop只有一种可能,那就是在调用handleEvents的时候,对应的文件描述符调用了runInLoop,此时处于LoopThread内,于是直接执行cb。
  2. 再来看由其他线程调用runInLoop的情况,首先会直接进入queueInLoop分支,把cb加入到待处理的回调函数数组里。因为我们要让IO线程及时处理我们的cb,我们势必要wakeupIO线程,让他从poll中出来,所以我们执行wakeup。
    至于另一种情况,我们先来假设在IO线程中也会调用queueInLoop,那么这种调用会在两个地方出现,第一是handleEvent,我们此时无需wakeup,因为IO线程处理完所有的channel之后便会执行doPendingFunctors,处理我们加入的cb。第二种情况是在执行doPendingFunctors的时候,执行functorsi时执行到了queueInLoop,那我们为了让IO线程在执行完所有的functors后立即处理我们这个时候加入的cb,还要唤醒他,也就是说提前唤醒,等到下次IO运行到poll直接就返回了,如果不这样我们的cb可能会被晚处理一段时间。
void EventLoop::runInLoop(Functor&&cb)
{
    if(isInLoopThread())
        cb();
    else
        queueInLoop(std::move(cb));
}
 
void EventLoop::queueInLoop(Functor&&cb)
{
    {
        MutexLockGuard lock(mutex_);
        pendingFunctors_.emplace_back(std::move(cb));
    }
    if(!isInLoopThread()||callingPendingFunctors_)
        wakeup();
}
void EventLoop::loop()
{
    assert(!looping_);
    assert(isInLoopThread());
    looping_=true;
    quit_=false;
    std::vectorret;
    while(!quit_)
    {
        ret.clear();
        ret=poller_->poll();
        eventHandlding_=true;
        for(auto & it :ret)
            it->handleEvents();
        eventHandling_=false;
        doPendingFunctors();
        poller_->handleExpired();
    }
    looping_=false;
}
void EventLoop::doPendingFunctors()
{
    std::vectorfunctors;
    callingPendingFunctors_=true;
 
    {
        MutexLockGuard lock(mutex_);
        functors.swap(pendingFunctors_);
    }
    for(size_t i=0;i

参考链接:
Linux进程间通信——eventfd
eventfd实现线程事件通知机制
关于muduo库中EventLoop的runInLoop功能
muduo库发送数据简述
muduo网络库学习(四)事件驱动循环EventLoop
muduo net库学习笔记4——事件驱动循环EventLoop、runInLoop和queueInLoop及对应唤醒

你可能感兴趣的:(操作系统知识点总结--eventfd实现线程事件通知机制)