Linux应用框架cpp-tbox之事件驱动Event

cpp-tbox项目链接 https://gitee.com/cpp-master/cpp-tbox

更多精彩内容欢迎关注微信公众号:码农练功房
在这里插入图片描述
往期精彩内容:
Linux应用框架cpp-tbox之弱定义
Linux应用框架cpp-tbox之日志系统设计
Linux应用框架cpp-tbox之事件驱动EventLoop

整体结构图

cpp-tbox实现了IO、Timer、Signal三种事件驱动,这是整个框架的心脏。下图是整个事件的结构图:
请添加图片描述

IO事件

EpollFdEvent完成对IO事件的封装,这里比较核心的其实是事件注册、注销。

Loop对象中保存着fd及与其有关联的EpollFdSharedData数据。EpollFdSharedData是一个十分重要的数据结构

从数据结构上看,一个fd可以关联多个EpollFdEvent对象,在监控到fd上发生对应事件时,如果关联了多个EpollFdEvent对象,这些回调函数会得到执行。

// cpp-tbox\modules\event\engines\epoll\types.h
//! 同一个fd共享的数据
struct EpollFdSharedData {
    int fd = 0;     //!< 文件描述符
    int ref = 0;    //!< 引用计数
    struct epoll_event ev;

    int read_event_num = 0;     //!< 监听可读事件的FdEvent个数
    int write_event_num = 0;    //!< 监听可写事件的FdEvent个数
    int except_event_num = 0;   //!< 监听异常事件的FdEvent个数
    int hup_event_num = 0;      //!< 监听挂起事件的FdEvent个数

    std::vector<EpollFdEvent*> fd_events;
};

// cpp-tbox\modules\event\engines\epoll\loop.cpp
class EpollLoop : public CommonLoop {
    // ...
 private:
    std::unordered_map<int, EpollFdSharedData*> fd_data_map_;
    ObjectPool<EpollFdSharedData> fd_shared_data_pool_{64};
};

事件注册、注销都依赖于这个数据结构,基于此,Loop对象实现了fd和具体EpollFdSharedData对象的关联建立,关联解除:

// cpp-tbox\modules\event\engines\epoll\loop.cpp
// 建立关联
EpollFdSharedData* EpollLoop::refFdSharedData(int fd)
{
    EpollFdSharedData *fd_shared_data = nullptr;

    auto it = fd_data_map_.find(fd);
    if (it != fd_data_map_.end())
        fd_shared_data = it->second;

    if (fd_shared_data == nullptr) {
        fd_shared_data = fd_shared_data_pool_.alloc();
        TBOX_ASSERT(fd_shared_data != nullptr);

        ::memset(&fd_shared_data->ev, 0, sizeof(fd_shared_data->ev));
        fd_shared_data->fd = fd;
        fd_shared_data->ev.data.ptr = static_cast<void *>(fd_shared_data);

        fd_data_map_.insert(std::make_pair(fd, fd_shared_data));
    }

    ++fd_shared_data->ref;
    return fd_shared_data;
}
// 解除关联
void EpollLoop::unrefFdSharedData(int fd)
{
    auto it = fd_data_map_.find(fd);
    if (it != fd_data_map_.end()) {
        auto fd_shared_data = it->second;
        --fd_shared_data->ref;
        if (fd_shared_data->ref == 0) {
            fd_data_map_.erase(fd);
            fd_shared_data_pool_.free(fd_shared_data);
        }
    }
}

EpollFdEvent::enable和EpollFdEvent::disable则最终完成事件注册、注销。注意到这两个方法都同时调用了reloadEpoll

// cpp-tbox\modules\event\engines\epoll\fd_event.cpp
void EpollFdEvent::reloadEpoll()
{
    uint32_t old_events = d_->ev.events;
    uint32_t new_events = 0;

    if (d_->write_event_num > 0)
        new_events |= EPOLLOUT;

    if (d_->read_event_num > 0)
        new_events |= EPOLLIN;

    if (d_->except_event_num > 0)
        new_events |= EPOLLERR;

    if (d_->hup_event_num > 0)
        new_events |= EPOLLHUP;

    d_->ev.events = new_events;

    if (old_events == 0) {
        if (LIKELY(new_events != 0))
            epoll_ctl(wp_loop_->epollFd(), EPOLL_CTL_ADD, fd_, &d_->ev);
    } else {
        if (new_events != 0)
            epoll_ctl(wp_loop_->epollFd(), EPOLL_CTL_MOD, fd_, &d_->ev);
        else
            epoll_ctl(wp_loop_->epollFd(), EPOLL_CTL_DEL, fd_, nullptr);
    }
}

Signal事件

从SignalEvent::initialize接口上看,一个信号处理器支持订阅多个信号。

SignalEvent在EpollFdEvent的基础上进行了封装,在信号处理器进行信号订阅时,会使用pipe2建立一个管道。

bool CommonLoop::subscribeSignal(int signo, SignalSubscribuer *who)
{
    if (signal_read_fd_ == -1) {    //! 如果还没有创建对应的信号
        // 建立管道
        if (!CreateFdPair(signal_read_fd_, signal_write_fd_))
            return false;

        sp_signal_read_event_ = newFdEvent("CommonLoop::sp_signal_read_event_");
        sp_signal_read_event_->initialize(signal_read_fd_, FdEvent::kReadEvent, Event::Mode::kPersist);
        sp_signal_read_event_->setCallback(std::bind(&CommonLoop::onSignal, this));
        sp_signal_read_event_->enable();
    }
    // ......
}

管道的读端注册到EventLoop循环中。
在信号处理函数中,对管道进行写操作。这样一旦有信号发送,则CommonLoop::onSignal会被回调,最终会调用到信号处理器设置的回调函数。
这里为何如此大费周章,直接使用signal()函数注册信号回调会存在什么问题?因为Linux的信号处理是不安全的,与以下几个方面有关:

  1. 异步性:信号是在任意时刻由操作系统发出的,这可能导致信号处理函数在代码的任何位置被调用,打断正常的程序执行流程。如果信号处理函数改变了关键数据结构或执行了不可重入(非原子)的操作,就可能导致程序状态不一致或死锁。
  2. 不可预测的执行时序:信号可能在代码的任何点到达,包括在执行不可重入函数或内核态操作时。这可能导致数据损坏,因为信号处理程序可能会看到不一致的数据状态。
  3. 信号处理时的上下文切换:当信号发生时,程序可能从用户态切换到内核态执行信号处理函数,这可能导致状态保存和恢复的开销,如果处理不当,可能会引发竞态条件。
  4. 默认信号处理行为:许多信号的默认行为是终止进程或产生核心转储,如果不正确地处理这些信号,可能意外结束程序或产生崩溃。
  5. 共享资源访问:在多线程或多进程环境下,如果多个线程同时响应同一信号或信号处理函数访问共享资源,没有适当的同步机制,可能导致竞争条件或数据竞争。
  6. 可重入函数:信号处理函数必须是可重入的,即在被信号中断的任何点都能安全地重新进入执行。使用非可重入函数(如某些标准I/O函数)可能导致未定义行为。

这里使用管道(pipe)作为同步机制,可以保证在信号处理前后执行特定操作时的原子性和一致性。将本身是异步的信号转化为同步,

精确控制信号处理时序和避免竞态条件。

我们也可以使用signalfd函数和eventfd结合的方法达到同样的效果。

Timer事件

定时器的实现就比较简单了,在EventLoop中介绍过,之前没有提及的是此处用到了最小堆。

总结

  1. 在Linux中,信号处理有时被视为是不安全的,此时可以考虑使用pipe、signalfd、eventfd等机制来提高信号处理的安全性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值