C++项目实战——sylar服务器框架:I/O 协程调度模块

180 阅读15分钟

设计思路

IOManager 类的设计思路是为了实现一个高效的 I/O 多路复用机制,并且能够与协程调度结合,以便在进行异步 I/O 操作时能够有效地调度执行协程。其核心目标是通过使用 epoll 或类似机制来高效地管理大量的文件描述符,并且根据事件类型(读、写)进行协程调度和回调执行。

此模块结合协程调度和 epoll I/O 多路复用技术。对于 I/O 协程调度来说,每次调度都包含一个三元组信息,分别是描述符-事件类型(可读或可写)-回调函数,调度器记录全部需要调度的三元组信息,其中描述符和事件类型用于 epoll_wait,回调函数用于协程调度。这个三元组信息通过 FdContext 结构体来存储,在执行 epoll_wait 时通过 epoll_event 的私有数据指针 data.ptr 来保存 FdContext 结构体信息。IO 协程调度器在 idle 时会 epoll_wait 所有注册的 fd,如果有 fd 满足条件,epoll_wait 返回,从私有数据中拿到 fd 的上下文信息,并且执行其中的回调函数(实际只是加到任务池中,因为加到任务池中,调度方法就可以自己去消耗这个任务了)。

IOManager

#ifndef __SYLAR_IOMANAGER_H__
#define __SYLAR_IOMANAGER_H__

#include "scheduler.h"
#include "timer.h"

namespace sylar{

class IOManager : public Scheduler, public TimerManager{
public:
    typedef std::shared_ptr<IOManager> ptr;
    typedef RWMutex RWMutexType;

    enum Event{
        NONE  = 0x0,
        READ  = 0x1,
        WRITE = 0x4
    };

private:
    struct FdContext{
        typedef Mutex MutexType;
        struct EventContext{
            Scheduler* scheduler = nullptr; // 事件执行的 scheduler
            Fiber::ptr fiber;               // 事件协程
            std::function<void()> cb;       // 事件的回调函数
        };

        EventContext& getContext(Event event);  // 获取特定事件的上下文
        void resetContext(EventContext& ctx);   // 重置事件的上下文

        void triggerEvent(Event event);         // 触发事件

        EventContext read;      // 读事件
        EventContext write;     // 写事件
        int fd = 0;             // 事件关联的句柄
        Event m_events = NONE;  // 已经注册的事件
        MutexType mutex; 
    };

public:
    IOManager(size_t threads = 1, bool use_caller = true, const std::string& name = "");
    ~IOManager();

    // 添加事件到指定文件描述符
    int addEvent(int fd, Event event, std::function<void()> cb = nullptr);
    bool delEvent(int fd, Event event);     // 删除指定文件描述符上的事件
    bool cancelEvent(int fd, Event event);  // 取消指定文件描述符上的事件

    bool cancelAll(int fd);                 // 取消指定文件描述符的所有事件
    static IOManager* GetThis();

protected:
    void tickle() override;
    bool stopping() override;
    void idle() override;
    void onTimerInsertedAtFront() override;

    void contextResize(size_t size);
    bool stopping(uint64_t& timeout);
private:
    int m_epfd = 0;
    int m_tickleFds[2];

    std::atomic<size_t> m_pendingEventCount = {0};  // 当前待处理的事件数量
    RWMutexType m_mutex;
    std::vector<FdContext*> m_fdContexts;   // 存储所有文件描述符的上下文
};
}
#endif

IOManager 类解析

(一)继承自 SchedulerTimerManager

  • IOManager 继承自 Scheduler,意味着它本身就具备了协程调度的能力,能够在事件发生时调度协程继续执行。Scheduler 负责协程的创建、切换和调度。

  • IOManager 继承自 TimerManager,意味着它也能够处理定时器事件,可以在 I/O 事件发生时执行定时器任务(比如执行超时操作)。

(二) 事件类型 Event

  • IOManager 定义了一个 Event 枚举类型,表示 I/O 事件的类型。常见的有:

    • READ:读事件,用于监听文件描述符是否可以读取数据。
    • WRITE:写事件,用于监听文件描述符是否可以写入数据。
    • NONE:表示没有任何事件。
  • Event 使用 0x00x10x4 等十六进制值的设计方式,是通过位运算(bitmask)来高效管理和组合多个事件。每个事件都对应一个特定的位,可以通过位运算来检查、添加、删除和组合多个事件。这种设计方式既简洁又高效,适合用于大规模的事件管理。

  • 如果我们希望同时注册 READWRITE 事件,可以使用 位或(| 操作将这两个事件合并:

Event events = READ | WRITE; // 同时注册 READ 和 WRITE 事件

最终,events 的值为 0x5(二进制 0101),表示同时注册了 READWRITE 两个事件。

  • 要检查某个特定事件是否在 Event 变量中,可以使用 位与(& 操作。例如,检查 events 是否包含 READ 事件:
if (events & READ) 
{ 
    // 表示 events 中包含 READ 事件 
}

(三)FdContext 结构体

这个结构体表示一个文件描述符(FD)的上下文信息,主要是为了存储与该文件描述符相关的事件和协程等信息。

  • fd:文件描述符本身,表示需要监控的 I/O 对象。
  • m_events:已经注册的事件,存储当前文件描述符上需要处理的事件类型(如读事件、写事件等)。
  • readwrite:分别表示与读事件和写事件相关的上下文信息。每个事件都有一个 EventContext包含了要执行的协程或回调函数。

FdContext 还提供了两个重要的方法:

  • getContext(Event event):根据事件类型获取对应的上下文信息。
  • triggerEvent(Event event):触发指定事件并执行相应的回调或恢复协程。

成员函数解析

(一)FdContext 的成员函数

(1)getContext()

这个函数的主要作用是根据事件类型获取对应的事件上下文,从而确保每个事件(如读取或写入操作)都有自己的协程、回调函数等资源。

// 获取特定事件的上下文
IOManager::FdContext::EventContext& IOManager::FdContext::getContext(IOManager::Event event)
{
    switch(event) 
    {
        case IOManager::READ:
            return read;
        case IOManager::WRITE:
            return write;
        default:
            SYLAR_ASSERT2(false, "getContext");
    }
}

(2)resetContext()

重置 EventContext,目的是清除 EventContext 中的状态,确保它处于初始状态。这个方法会被调用在取消或删除事件时,以便重置与事件相关的协程、回调函数和调度器等信息,确保下次该事件被触发时能够重新设置正确的处理方式。

void IOManager::FdContext::resetContext(EventContext &ctx)
{
    ctx.scheduler = nullptr;
    ctx.fiber.reset();
    ctx.cb = nullptr;
}

(3)triggerEvent()

将事件的回调或协程调度到相应的调度器中执行,以响应 I/O 操作的完成。

// 触发指定的 I/O 事件
void IOManager::FdContext::triggerEvent(Event event)
{
    // 检查 m_event 中是否已经注册了该事件
    SYLAR_ASSERT(m_events & event);
    // 从 m_event 中移除该事件,表示事件已经被触发
    m_events = (Event)(m_events & ~event);
    EventContext& ctx = getContext(event);
    if(ctx.cb) {    // 回调函数不为空,则将其调度到调度器中执行
        ctx.scheduler->schedule(&ctx.cb);
    } else {        // 否则将协程调度到调度器中执行
        ctx.scheduler->schedule(&ctx.fiber);
    }
    // 事件触发并调度完成后,清空事件上下文中的调度器指针
    // 确保该事件只会触发一次,并避免重复调度
    ctx.scheduler = nullptr;
    return;
}

(二)构造函数

IOManager::IOManager(size_t threads, bool use_caller, const std::string &name)
    :Scheduler(threads, use_caller, name)
{
    m_epfd = epoll_create(5000);    // 创建一个 epoll 文件描述符
    SYLAR_ASSERT(m_epfd > 0);       // 确保 epoll 文件描述符创建成功
    int rt = pipe(m_tickleFds);     // 创建管道,用于线程间通信,[0] 是读取端,[1] 是写入端
    SYLAR_ASSERT(!rt);              // 确保管道创建成功

    epoll_event event;
    memset(&event, 0, sizeof(epoll_event));
    event.events = EPOLLIN | EPOLLET;
    event.data.fd = m_tickleFds[0];

    // 设置管道读取端为非阻塞模式
    rt = fcntl(m_tickleFds[0], F_SETFL, O_NONBLOCK);
    SYLAR_ASSERT(!rt);
    // 将管道读取端添加到 epoll 实例中,监听其是否有数据可读
    rt = epoll_ctl(m_epfd, EPOLL_CTL_ADD, m_tickleFds[0], &event);
    SYLAR_ASSERT(!rt);

    contextResize(32);

    start();    // 启动调度器
}

(三)addEvent()

向特定的文件描述符(fd)添加一个 I/O 事件(如 READWRITE),并将事件的回调函数或协程与事件关联。整个过程涉及检查、注册、调度等多个步骤,确保事件能够正确地触发并处理。

int IOManager::addEvent(int fd, Event event, std::function<void()> cb)
{
    FdContext* fd_ctx = nullptr;
    RWMutexType::ReadLock lock(m_mutex);
    if((int)m_fdContexts.size() > fd)   // 检查文件描述符是否已经有对应的上下文
    {
        fd_ctx = m_fdContexts[fd];      // 有则直接获取对应的 FdContext
        lock.unlock();
    }
    else
    {   // 没有则释放读锁加写锁
        lock.unlock();
        RWMutexType::WriteLock lock2(m_mutex);
        contextResize(fd * 1.5);        // 拓展文件描述符上下文数组,增加新的空间
        fd_ctx = m_fdContexts[fd];
    }

    FdContext::MutexType::Lock lock2(fd_ctx->mutex);
    // 检查事件是否已经被注册过
    if(fd_ctx->m_events & event)
    {
        SYLAR_LOG_ERROR(g_logger) << "addEvent assert fd=" << fd
                                  << " event=" << event
                                  << " fd_ctx.event=" << fd_ctx->m_events;
        // 如果事件已被注册,则不允许重复注册
        SYLAR_ASSERT(!(fd_ctx->m_events & event));
    }

    // 修改已有事件还是添加新事件
    int op = fd_ctx->m_events ? EPOLL_CTL_MOD : EPOLL_CTL_ADD;
    epoll_event epevent;
    epevent.events = EPOLLET | fd_ctx->m_events | event;
    epevent.data.ptr = fd_ctx;
    // 调用 epoll_ctl 来添加或修改事件
    int rt = epoll_ctl(m_epfd, op, fd, &epevent);
    if(rt)
    {
        SYLAR_LOG_ERROR(g_logger) << "epoll_ctl(" << m_epfd << ", "
            << op << ", " << fd << ", " << epevent.events << "):"
            << rt << " (" << errno << ") (" << strerror(errno) << ")";
        return -1;
    }
    // 事件注册成功,增加待处理事件的计数
    ++m_pendingEventCount;
    // 更新该文件描述符上下文的已注册事件
    fd_ctx->m_events = (Event)(fd_ctx->m_events | event);
    FdContext::EventContext& event_ctx = fd_ctx->getContext(event);
    // 确保事件上下文没有被其他协程或回调占用
    SYLAR_ASSERT(!event_ctx.scheduler && !event_ctx.fiber && !event_ctx.cb);

    event_ctx.scheduler = Scheduler::GetThis();
    if(cb)
    {
        event_ctx.cb.swap(cb);
    }
    else
    {
        event_ctx.fiber = Fiber::GetThis();
        SYLAR_ASSERT(event_ctx.fiber->getState() == Fiber::EXEC);
    }

    return 0;
}
  • if((int)m_fdContexts.size() > fd) : 当进程打开一个文件时,操作系统会查找第一个可用的文件描述符,并将其分配给新打开的文件。每次分配后,文件描述符会加 1,确保下一个文件获得一个新的描述符。文件描述符通常是通过递增方式分配的,通常从 0 开始,依次递增。 如果进程已经打开了文件描述符 0123,并关闭了文件描述符 2,那么下次 open() 操作时,操作系统可能会将文件描述符 2 分配给新打开的文件。

  • 为什么要释放读锁,加写锁:需要扩展 m_fdContexts 数组

  • int op = fd_ctx->m_events ? EPOLL_CTL_MOD : EPOLL_CTL_ADD;

    • EPOLL_CTL_ADD 是用来添加新事件的,只能用于文件描述符首次注册事件时。

    • EPOLL_CTL_MOD 是用来修改已有事件的,可以更新已注册的事件类型。

  • epevent.events = EPOLLET | fd_ctx->m_events | event;

    这一行代码配置 epoll_event 结构体中的 events 字段,表示要在 epoll 中注册的事件类型。具体来说:

    • EPOLLET:启用边缘触发(Edge Triggered)模式。边缘触发模式是 epoll 的一种工作方式,只有在事件发生时,epoll_wait 才会返回事件。当某个事件再次发生时,不会再返回(除非重新注册该事件)。这要求程序必须尽可能快速地处理事件。
    • fd_ctx->m_events:表示当前文件描述符已经注册的事件类型(读取、写入等),是一个位掩码。
    • event:表示当前需要添加或修改的事件类型(读取或写入)。

    这三者通过按位或 (|) 操作符连接在一起,表示文件描述符上的多个事件。最终 epevent.events 就是该文件描述符上需要注册的所有事件的组合

  • epevent.data.ptr = fd_ctx;

    这里设置 epevent.data.ptrfd_ctx,即将文件描述符对应的上下文结构体 FdContext 指针存入 epoll_eventdata.ptr 字段。这样,当 epoll_wait 返回时,我们可以通过 data.ptr 获取到该事件的上下文(即文件描述符的 FdContext),进而获取事件的相关信息。

  • int rt = epoll_ctl(m_epfd, op, fd, &epevent);

    通过 epoll_ctl 系统调用将事件注册到 epoll 中:

    • m_epfdepoll 的文件描述符,表示当前进程正在使用的 epoll 实例。
    • op:事件操作类型,决定是添加新事件 (EPOLL_CTL_ADD) 还是修改已有事件 (EPOLL_CTL_MOD)。
    • fd:文件描述符,需要注册或修改事件的目标文件描述符。
    • &epevent:指向 epoll_event 结构体的指针,包含了需要注册的事件信息和相关上下文。
    • epoll_ctl(A, EPOLL_CTL_ADD, B, C); 表示  “epoll 例程 A 中注册文件描述符 B,主要目的是监视参数 C 中的事件。”

(四)delEvent()

epoll 中删除一个文件描述符(fd)上注册的指定事件。

// 删除指定文件描述符 fd 上的某个事件
bool IOManager::delEvent(int fd, Event event)
{
    RWMutexType::ReadLock lock(m_mutex);
    // 检查文件描述符 fd 是否在 m_fdcontexts 数组内
    if((int)m_fdContexts.size() <= fd)
    {
        return false;
    }
    // 获取文件描述符对应的 FdContext
    FdContext* fd_ctx = m_fdContexts[fd];
    lock.unlock();

    FdContext::MutexType::Lock lock2(fd_ctx->mutex);
    // 检查文件描述符的事件是否已经注册了要删除的事件
    if(!(fd_ctx->m_events & event))
    {
        return false;
    }
    // 更新事件集,移除要删除的事件
    Event new_events = (Event)(fd_ctx->m_events & ~event);
    // 确定 epoll_ctl 的操作类型
    int op = new_events ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
    // 准备 epoll_event 结构体,设置新的事件
    epoll_event epevent;
    epevent.events = EPOLLET | new_events;  // 事件使用边缘触发模式
    epevent.data.ptr = fd_ctx;  // 关联 fd_ctx 事件

    // 调用 epoll_ctl 删除事件
    int rt = epoll_ctl(m_epfd, op, fd, &epevent);
    if(rt)
    {
        SYLAR_LOG_ERROR(g_logger) << "epoll_ctl(" << m_epfd << ", "
            << op << ", " << fd << ", " << epevent.events << "):"
            << rt << " (" << errno << ") (" << strerror(errno) << ")";
        return false;
    }
    // 更新待处理事件的计数
    --m_pendingEventCount;
    fd_ctx->m_events = new_events;  // 更新 fd_ctx 的事件状态
    // 重置事件上下文,清理相关资源
    FdContext::EventContext& event_ctx = fd_ctx->getContext(event);
    fd_ctx->resetContext(event_ctx);
    return true;
}

(五)cancelEvent()

// 取消文件描述符 fd 上的某个事件 event
bool IOManager::cancelEvent(int fd, Event event)
{
    RWMutexType::ReadLock lock(m_mutex);
    // 检查文件描述符 fd 是否在 m_fdContexts 数组内
    if((int)m_fdContexts.size() <= fd)
    {
        return false;
    }
    FdContext* fd_ctx = m_fdContexts[fd];
    lock.unlock();

    FdContext::MutexType::Lock lock2(fd_ctx->mutex);
    // 该文件描述符的事件中是否已经注册了要取消的事件
    if(!(fd_ctx->m_events & event))
    {
        return false;
    }

    // 更新事件集,移除要取消的事件
    Event new_events = (Event)(fd_ctx->m_events & ~event);
    // 确定 epoll_ctl 的操作类型
    int op = new_events ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
    // 准备 epoll_event 结构体,设置新的事件
    epoll_event epevent;
    epevent.events = EPOLLET | new_events;  // 事件使用边缘触发模式
    epevent.data.ptr = fd_ctx;              // 关联 fd_ctx 数据

    // 调用 epoll_ctl 取消事件
    int rt = epoll_ctl(m_epfd, op, fd, &epevent);
    if(rt)
    {
        SYLAR_LOG_ERROR(g_logger) << "epoll_ctl(" << m_epfd << ", "
            << op << ", " << fd << ", " << epevent.events << "):"
            << rt << " (" << errno << ") (" << strerror(errno) << ")";
        return false;
    }

    // 触发事件回调
    fd_ctx->triggerEvent(event);

    // 更新待处理事件的计数
    --m_pendingEventCount;
    return true;
}

(六)calcelAll()

bool IOManager::cancelAll(int fd)
{
    RWMutexType::ReadLock lock(m_mutex);
    if((int)m_fdContexts.size() <= fd)
    {
        return false;
    }
    FdContext* fd_ctx = m_fdContexts[fd];
    lock.unlock();

    FdContext::MutexType::Lock lock2(fd_ctx->mutex);
    // 没有任何事件则说明该文件描述符不需要处理
    if(!fd_ctx->m_events)
    {
        return false;
    }

    int op = EPOLL_CTL_DEL;
    epoll_event epevent;
    epevent.events = 0;
    epevent.data.ptr = fd_ctx;

    int rt = epoll_ctl(m_epfd, op, fd, &epevent);
    if(rt)
    {
        SYLAR_LOG_ERROR(g_logger) << "epoll_ctl(" << m_epfd << ", "
            << op << ", " << fd << ", " << epevent.events << "):"
            << rt << " (" << errno << ") (" << strerror(errno) << ")";
        return false;
    }

    // 如果 fd_ctx 上注册了读事件,触发该事件的回调
    if(fd_ctx->m_events & READ)
    {
        fd_ctx->triggerEvent(READ); // 执行回调
        --m_pendingEventCount;      // 减少待处理事件的数量
    }
    // 写事件
    if(fd_ctx->m_events & WRITE) {
        fd_ctx->triggerEvent(WRITE);
        --m_pendingEventCount;
    }

    // 确保 fd_ctx 的事件为空
    SYLAR_ASSERT(fd_ctx->m_events == 0);
    return true;
}

(七)tickle()

通过管道机制向其他线程发送一个信号,以便唤醒正在等待的线程进行工作。

void IOManager::tickle()
{
    // 没有空闲线程则直接返回
    if(!hasIdleThreads()) 
    {
        return;
    }
    // 向 tickleFds[1] 写入一个字节 T,用于唤醒其他线程
    int rt = write(m_tickleFds[1], "T", 1);
    // 确保写操作成功
    SYLAR_ASSERT(rt == 1);
}

有任务需要执行时,线程不是会自动唤醒并执行任务吗,为什么还要额外用函数进行唤醒?

尽管 epoll 可以在 I/O 事件发生时唤醒线程,但 空闲线程(那些没有 I/O 事件或没有任务在等待的线程)可能会陷入阻塞状态,无法继续工作。如果一个线程池中的线程处于 空闲状态,并且没有任何 I/O 事件发生,线程就会一直等待,直到有新的 I/O 事件发生。

这时,即使有任务需要执行(例如:协程调度、定时器超时等),这些空闲线程也可能不会被及时唤醒,因为 epoll 只会唤醒与 I/O 事件相关的线程。

(八)idle()

void IOManager::idle()
{
    // 创建一个大小为 64 的 epoll_event 数组
    epoll_event* events = new epoll_event[64]();
    // 使用智能指针管理数组内存,避免内存泄漏
    std::shared_ptr<epoll_event> shared_events(events, [](epoll_event* ptr){
        delete[] ptr;
    });

    while(true) {
        uint64_t next_timeout = 0;
        // 检查是否需要停止 IOManager
        if(stopping(next_timeout)) 
        {
            SYLAR_LOG_INFO(g_logger) << "name=" << getName()
                                    << " idle stopping exit";
            break;
               
        }

        int rt = 0;
        do {
            static const int MAX_TIMEOUT = 3000;    // 最大等待超时时间
            // 处理下一个定时器超时时间
            if(next_timeout != ~0ull) {
                next_timeout = (int)next_timeout > MAX_TIMEOUT
                                ? MAX_TIMEOUT : next_timeout;
            } else {
                next_timeout = MAX_TIMEOUT;
            }
            
            // 调用 epoll_wait 等待 I/O 事件
            rt = epoll_wait(m_epfd, events, 64, (int)next_timeout);
            // 如果 epoll 被中断,继续等待
            if(rt < 0 && errno == EINTR) {
            } else {
                break;
            }
        } while(true);

        // 处理过期的定时器回调
        std::vector<std::function<void()> > cbs;
        listExpiredCb(cbs);     // 获取过期的定时器回调函数
        if(!cbs.empty()) {
            //SYLAR_LOG_DEBUG(g_logger) << "on timer cbs.size=" << cbs.size();
            schedule(cbs.begin(), cbs.end());
            cbs.clear();
        }

        // 遍历 epoll 返回的事件
        for(int i = 0; i < rt; ++i) {
            epoll_event& event = events[i];
            if(event.data.fd == m_tickleFds[0]) {
                uint8_t dummy;
                // 读取管道数据,唤醒线程
                while(read(m_tickleFds[0], &dummy, 1) == 1);
                continue;
            }

            // 获取和事件相关的 FdContext 对象
            FdContext* fd_ctx = (FdContext*)event.data.ptr;
            FdContext::MutexType::Lock lock(fd_ctx->mutex);

            // 处理错误和挂起事件
            if(event.events & (EPOLLERR | EPOLLHUP)) {
                event.events |= (EPOLLIN | EPOLLOUT);   // 标记为可读和可写
            }
            int real_events = NONE;
            if(event.events & EPOLLIN) {
                real_events |= READ;    // 如果有读事件,标记为 READ
            }
            if(event.events & EPOLLOUT) {
                real_events |= WRITE;   // 如果有写事件,标记为 WRITE
            }

            // 如果 fd_ctx 不包含这些事件,则跳过
            if((fd_ctx->m_events & real_events) == NONE) {
                continue;
            }

            // 更新 epoll 事件,移除已处理的事件
            int left_events = (fd_ctx->m_events & ~real_events);
            int op = left_events ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
            event.events = EPOLLET | left_events;

            // 修改 epoll 事件
            int rt2 = epoll_ctl(m_epfd, op, fd_ctx->fd, &event);
            if(rt2) {
                SYLAR_LOG_ERROR(g_logger) << "epoll_ctl(" << m_epfd << ", "
                    << op << ", " << fd_ctx->fd << ", " << event.events << "):"
                    << rt2 << " (" << errno << ") (" << strerror(errno) << ")";
                continue;
            }

            // 触发事件回调
            if(real_events & READ) {
                fd_ctx->triggerEvent(READ);
                --m_pendingEventCount;
            }
            if(real_events & WRITE) {
                fd_ctx->triggerEvent(WRITE);
                --m_pendingEventCount;
            }
        }

        // 获取当前协程并切换
        Fiber::ptr cur = Fiber::GetThis();
        auto raw_ptr = cur.get();
        cur.reset();

        // 将当前协程放回调度器,进行协程切换
        raw_ptr->swapOut();
    }
}