设计思路
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
类解析
(一)继承自 Scheduler
和 TimerManager
-
IOManager
继承自Scheduler
,意味着它本身就具备了协程调度的能力,能够在事件发生时调度协程继续执行。Scheduler
负责协程的创建、切换和调度。 -
IOManager
继承自TimerManager
,意味着它也能够处理定时器事件,可以在 I/O 事件发生时执行定时器任务(比如执行超时操作)。
(二) 事件类型 Event
-
IOManager
定义了一个Event
枚举类型,表示 I/O 事件的类型。常见的有:READ
:读事件,用于监听文件描述符是否可以读取数据。WRITE
:写事件,用于监听文件描述符是否可以写入数据。NONE
:表示没有任何事件。
-
Event
使用0x0
、0x1
、0x4
等十六进制值的设计方式,是通过位运算(bitmask)来高效管理和组合多个事件。每个事件都对应一个特定的位,可以通过位运算来检查、添加、删除和组合多个事件。这种设计方式既简洁又高效,适合用于大规模的事件管理。 -
如果我们希望同时注册
READ
和WRITE
事件,可以使用 位或(|
) 操作将这两个事件合并:
Event events = READ | WRITE; // 同时注册 READ 和 WRITE 事件
最终,events
的值为 0x5
(二进制 0101
),表示同时注册了 READ
和 WRITE
两个事件。
- 要检查某个特定事件是否在
Event
变量中,可以使用 位与(&
) 操作。例如,检查events
是否包含READ
事件:
if (events & READ)
{
// 表示 events 中包含 READ 事件
}
(三)FdContext
结构体
这个结构体表示一个文件描述符(FD)的上下文信息,主要是为了存储与该文件描述符相关的事件和协程等信息。
fd
:文件描述符本身,表示需要监控的 I/O 对象。m_events
:已经注册的事件,存储当前文件描述符上需要处理的事件类型(如读事件、写事件等)。read
和write
:分别表示与读事件和写事件相关的上下文信息。每个事件都有一个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 事件(如 READ
或 WRITE
),并将事件的回调函数或协程与事件关联。整个过程涉及检查、注册、调度等多个步骤,确保事件能够正确地触发并处理。
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 开始,依次递增。 如果进程已经打开了文件描述符0
、1
、2
和3
,并关闭了文件描述符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.ptr
为fd_ctx
,即将文件描述符对应的上下文结构体FdContext
指针存入epoll_event
的data.ptr
字段。这样,当epoll_wait
返回时,我们可以通过data.ptr
获取到该事件的上下文(即文件描述符的FdContext
),进而获取事件的相关信息。 -
int rt = epoll_ctl(m_epfd, op, fd, &epevent);
通过
epoll_ctl
系统调用将事件注册到epoll
中:m_epfd
:epoll
的文件描述符,表示当前进程正在使用的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();
}
}