设计
IO协程调度模块,整个项目里最重要的模块~
和 协程调度模块 相比,增加了 IO 事件的 触发条件。
基本原理
先将套接字设置成非阻塞状态,然后将套接字与回调函数绑定,接下来进入一个基于IO多路复用的事件循环,等待事件发生,然后调用对应的回调函数。
class IOManager : public Scheduler {
public:
typedef std::shared_ptr<IOManager> ptr;
typedef RWMutex RWMutexType;
...
}
封装 调度信息
/**
* @brief IO事件,继承自epoll对事件的定义
* @details 这里只关心socket fd的读和写事件,其他epoll事件会归类到这两类事件中
*/
enum Event{
NONE = 0x0,
READ = 0x1,
WRITE = 0x4,
};
事件句柄 - 事件类型 - 回调函数 (三元组)⭐
struct FdContext{
typedef Mutex MutexType;
/**
* @brief 事件上下文类
* @details fd的每个事件都有一个事件上下文,保存这个事件的回调函数以及执行回调函数的调度器
* sylar对fd事件做了简化,只预留了读事件和写事件,所有的事件都被归类到这两类事件中
*/
struct EventContext{
///执行事件回调的调度器
Scheduler* scheduler = nullptr;
/// 回调协程
Fiber::ptr fiber;
/// 回调函数
std::function<void()> cb;
};
EventContext& getEventContex(Event event);
void resetEventContext(EventContext& ctx);
void triggerEvent(Event event);
// 读事件上下文
EventContext read;
// 写事件上下文
EventContext write;
// 事件关联的句柄
int fd = 0;
// 注册事件
Event events = NONE;
MutexType mutex;
};
IOManager 成员变量
/// epoll 文件句柄
int m_epfd = 0;
/// pipe 文件句柄,fd[0]读端,fd[1]写端
int m_tickleFds[2];
/// 当前等待执行的IO事件数量
std::atomic<size_t> m_pendingEventCount = {0};
/// IOManager的Mutex
RWMutexType m_mutex;
/// socket事件上下文的容器
std::vector<FdContext *> m_fdContexts;
IOManager 构造函数
/**
* @brief 构造函数
* @param[in] threads 线程数量
* @param[in] use_caller 是否将调用线程包含进去
* @param[in] name 调度器的名称
*/
IOManager::IOManager(size_t threads, bool use_caller, const std::string &name)
: Scheduler(threads, use_caller, name)
{
// 创建epoll实例
m_epfd = epoll_create(5000);
SYLAR_ASSERT(m_epfd > 0);
// 创建pipe,获取m_tickleFds[2],其中m_tickleFds[0]是管道的读端,m_tickleFds[1]是管道的写端
int rt = pipe(m_tickleFds);
SYLAR_ASSERT(!rt);
// 注册pipe读句柄的可读事件,用于tickle调度协程,通过epoll_event.data.fd保存描述符
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多路复用,如果管道可读,idle中的epoll_wait会返回
rt = epoll_ctl(m_epfd, EPOLL_CTL_ADD, m_tickleFds[0], &event);
SYLAR_ASSERT(!rt);
contextResize(32);
// 这里直接开启了Schedluer,也就是说IOManager创建即可调度协程
start();
}
重写 Scheduler 的 tickle(),通过 pipe 写端写入数据,通知 读端唤醒 epoll_wait ⭐⭐⭐⭐⭐
/**
* @brief 通知调度器有任务要调度
* @details 写pipe让idle协程从epoll_wait退出,待idle协程yield之后Scheduler::run就可以调度其他任务
* 如果当前没有空闲调度线程,那就没必要发通知
*/
void IOManager::tickle() {
if(!hasIdleThreads()) {
return;
}
int rt = write(m_tickleFds[1], "T", 1);
return;
}
重写 Scheduler 的 idle(),通过 epoll_wait(),等待事件发生。
我的困惑:如果线程在消费其他任务,没有协程在 idle,那么定时器或者IO事件就没法及时响应了。 我的想法,需要增加时间片调度算法,不让线程运行某一协程过久。⭐⭐⭐(待优化)
/**
* @brief idle协程
* @details 对于IO协程调度来说,应阻塞在等待IO事件上,idle退出的时机是epoll_wait返回,对应的操作是tickle或注册的IO事件就绪
* 调度器无调度任务时会阻塞idle协程上,对IO调度器而言,idle状态应该关注两件事,一是有没有新的调度任务,对应Schduler::schedule(),
* 如果有新的调度任务,那应该立即退出idle状态,并执行对应的任务;二是关注当前注册的所有IO事件有没有触发,如果有触发,那么应该执行
* IO事件对应的回调函数
*/
void IOManager::idle() {
// 一次epoll_wait最多检测256个就绪事件,如果就绪事件超过了这个数,那么会在下轮epoll_wati继续处理
const uint64_t MAX_EVENTS = 256;
epoll_event * events = new epoll_event[MAX_EVENTS]();
std::shared_ptr<epoll_event> shared_events(events, [](epoll_event *ptr){
delete[] ptr;
});
while(true){
if(stopping()) {
SYLAR_LOG_DEBUG(g_logger) << "name=" << getName() << "idle stopping exit";
break;
}
// 阻塞在 epoll_wait 上,等待事件发生。
static const int MAX_TIMEOUT = 5000; // 5秒
int rt = epoll_wait(m_epfd, events, MAX_EVNETS, MAX_TIMEOUT);
if(rt < 0){
if(errno == EINTR){
continue;
}
SYLAR_LOG_ERROR(g_logger) << "epoll_wait(" << m_epfd << ") (rt="
<< rt << ") (errno=" << errno << ") (errstr:" << strerror(errno) << ")";
break;
}
for(int i = 0 ; i < rt; ++i){
epoll_event& event = events[i];
if(event.data.fd == m_tickleFds[0]){
// ticklefd[0]用于通知协程调度,这时只需要把管道里的内容读完即可,本轮idle结束Scheduler::run会重新执行协程调度
uint8_t dummy[256];
while(read(m_tickleFds[0], dummy, sizeof(dummy)) > 0);
continue;
}
FdContext *fd_ctx = (FdContext*)event.data.ptr;
FdContext::MutexType::Lock lock(fd_ctx->mutex);
// 判断 event.events
/**
* EPOLLERR: 出错,比如写读端已经关闭的pipe
* EPOLLHUP: 套接字对端关闭
* 出现这两种事件,应该同时触发fd的读和写事件,否则有可能出现注册的事件永远执行不到的情况
*/
if (event.events & (EPOLLERR | EPOLLHUP)) {
event.events |= (EPOLLIN | EPOLLOUT) & fd_ctx->events;
}
int real_events = NONE;
if(event.events & EPOLLIN){
real_events |= READ;
}
if(event.events & EPOLLOUT){
real_events |= WRITE;
}
// 如果触发的事件 和 fd_ctx记录的不同,退出
if ((fd_ctx->events & real_events) == NONE) {
continue;
}
// 剔除已经发生的事件,将剩下的事件重新加入epoll_wait,
// 如果剩下的事件为0,表示这个fd已经不需要关注了,直接从epoll中删除
int left_events = (fd_ctx->events & ~real_events);
int op = left_events ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
event.events = EPOLLET | left_events;
int rt2 = epoll_ctl(m_epfd, op, fd_ctx->fd, &event);
if (rt2) {
SYLAR_LOG_ERROR(g_logger) << "epoll_ctl(" << m_epfd << ", "
<< (EpollCtlOp)op << ", " << fd_ctx->fd << ", " << (EPOLL_EVENTS)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;
}
} // end for
/**
* 一旦处理完所有的事件,idle协程yield,这样可以让调度协程(Scheduler::run)重新检查是否有新任务要调度
* 上面triggerEvent实际也只是把对应的fiber重新加入调度,要执行的话还要切换到调度协程
*/
Fiber::GetThis()->yield();
} // end while;
}
注册事件addEvent,删除事件delEvent,取消事件cancelEvent,取消全部事件cancelAll
/**
* @brief 添加事件
* @details fd描述符发生了event事件时执行cb函数
* @param[in] fd socket句柄
* @param[in] event 事件类型
* @param[in] cb 事件回调函数,如果为空,则默认把当前协程作为回调执行体
* @return 添加成功返回0,失败返回-1
*/
int IOManager::addEvent(int fd, Event event, std::function<void()> cb) {
// 找到fd对应的FdContext,如果不存在,那就分配一个
FdContext *fd_ctx = nullptr;
RWMutexType::ReadLock lock(m_mutex);
if ((int)m_fdContexts.size() > fd) {
fd_ctx = m_fdContexts[fd];
lock.unlock();
} else {
lock.unlock();
RWMutexType::WriteLock lock2(m_mutex);
contextResize(fd * 1.5);
fd_ctx = m_fdContexts[fd];
}
// 同一个fd不允许重复添加相同的事件
FdContext::MutexType::Lock lock2(fd_ctx->mutex);
if (SYLAR_UNLIKELY(fd_ctx->events & event)) {
SYLAR_LOG_ERROR(g_logger) << "addEvent assert fd=" << fd
<< " event=" << (EPOLL_EVENTS)event
<< " fd_ctx.event=" << (EPOLL_EVENTS)fd_ctx->events;
SYLAR_ASSERT(!(fd_ctx->events & event));
}
// 将新的事件加入epoll_wait,使用epoll_event的私有指针存储FdContext的位置
int op = fd_ctx->events ? EPOLL_CTL_MOD : EPOLL_CTL_ADD;
epoll_event epevent;
epevent.events = EPOLLET | fd_ctx->events | event;
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 << ", "
<< (EpollCtlOp)op << ", " << fd << ", " << (EPOLL_EVENTS)epevent.events << "):"
<< rt << " (" << errno << ") (" << strerror(errno) << ") fd_ctx->events="
<< (EPOLL_EVENTS)fd_ctx->events;
return -1;
}
// 待执行IO事件数加1
++m_pendingEventCount;
// 找到这个fd的event事件对应的EventContext,对其中的scheduler, cb, fiber进行赋值
fd_ctx->events = (Event)(fd_ctx->events | event);
FdContext::EventContext &event_ctx = fd_ctx->getEventContext(event);
SYLAR_ASSERT(!event_ctx.scheduler && !event_ctx.fiber && !event_ctx.cb);
// 赋值scheduler和回调函数,如果回调函数为空,则把当前协程当成回调执行体
event_ctx.scheduler = Scheduler::GetThis();
if (cb) {
event_ctx.cb.swap(cb);
} else {
event_ctx.fiber = Fiber::GetThis();
SYLAR_ASSERT2(event_ctx.fiber->getState() == Fiber::RUNNING, "state=" << event_ctx.fiber->getState());
}
return 0;
}
/**
* @brief 删除事件
* @param[in] fd socket句柄
* @param[in] event 事件类型
* @attention 不会触发事件
* @return 是否删除成功
*/
bool IOManager::delEvent(int fd, Event event) {
// 找到fd对应的FdContext
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 (SYLAR_UNLIKELY(!(fd_ctx->events & event))) {
return false;
}
// 清除指定的事件,表示不关心这个事件了,如果清除之后结果为0,则从epoll_wait中删除该文件描述符
Event new_events = (Event)(fd_ctx->events & ~event);
int op = new_events ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
epoll_event epevent;
epevent.events = EPOLLET | new_events;
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 << ", "
<< (EpollCtlOp)op << ", " << fd << ", " << (EPOLL_EVENTS)epevent.events << "):"
<< rt << " (" << errno << ") (" << strerror(errno) << ")";
return false;
}
// 待执行事件数减1
--m_pendingEventCount;
// 重置该fd对应的event事件上下文
fd_ctx->events = new_events;
FdContext::EventContext &event_ctx = fd_ctx->getEventContext(event);
fd_ctx->resetEventContext(event_ctx);
return true;
}
/**
* @brief 取消事件
* @param[in] fd socket句柄
* @param[in] event 事件类型
* @attention 如果该事件被注册过回调,那就触发一次回调事件
* @return 是否删除成功
*/
bool IOManager::cancelEvent(int fd, Event event) {
// 找到fd对应的FdContext
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 (SYLAR_UNLIKELY(!(fd_ctx->events & event))) {
return false;
}
// 删除事件
Event new_events = (Event)(fd_ctx->events & ~event);
int op = new_events ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
epoll_event epevent;
epevent.events = EPOLLET | new_events;
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 << ", "
<< (EpollCtlOp)op << ", " << fd << ", " << (EPOLL_EVENTS)epevent.events << "):"
<< rt << " (" << errno << ") (" << strerror(errno) << ")";
return false;
}
// 删除之前触发一次事件
fd_ctx->triggerEvent(event);
// 活跃事件数减1
--m_pendingEventCount;
return true;
}
/**
* @brief 取消所有事件
* @details 所有被注册的回调事件在cancel之前都会被执行一次
* @param[in] fd socket句柄
* @return 是否删除成功
*/
bool IOManager::cancelAll(int fd) {
// 找到fd对应的FdContext
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->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 << ", "
<< (EpollCtlOp)op << ", " << fd << ", " << (EPOLL_EVENTS)epevent.events << "):"
<< rt << " (" << errno << ") (" << strerror(errno) << ")";
return false;
}
// 触发全部已注册的事件
if (fd_ctx->events & READ) {
fd_ctx->triggerEvent(READ);
--m_pendingEventCount;
}
if (fd_ctx->events & WRITE) {
fd_ctx->triggerEvent(WRITE);
--m_pendingEventCount;
}
SYLAR_ASSERT(fd_ctx->events == 0);
return true;
}
IOManager的析构函数实现和stopping重载。 对于IOManager的析构,首先要等Scheduler调度完所有的任务,然后再关闭epoll句柄和pipe句柄,然后释放所有的FdContext 对于stopping,IOManager在判断是否可退出时,还要加上所有IO事件都完成调度的条件:
IOManager::~IOManager() {
stop();
close(m_epfd);
close(m_tickleFds[0]);
close(m_tickleFds[1]);
for (size_t i = 0; i < m_fdContexts.size(); ++i) {
if (m_fdContexts[i]) {
delete m_fdContexts[i];
}
}
}
bool IOManager::stopping() {
// 对于IOManager而言,必须等所有待调度的IO事件都执行完了才可以退出
return m_pendingEventCount == 0 && Scheduler::stopping();
}
知识点
epoll 是线程安全的 ⭐⭐⭐⭐⭐
int ep_fd = epoll_create(1);
epoll_event ev;
// ev.events 事件有多种, EPOLLIN EPOLLOUT EPOLLET(边缘触发) EPOLLLT(水平触发)
ev.events = EPOLLIN | EPOLLOUT | EPOLLET;
// ev.data 联合体, 只能设置一个 fd, ptr
ev.data.ptr = xxx;
// op EPOLL_ADD EPOLL_MOD EPOLL_DEL
int rt = epoll_ctl(ep_fd, op, fd, &ev);
epoll_event* ev = new epoll_event[MAX_EPOLL]();
uint64_t TIMEOUT = -1; // -1表示一直阻塞,大于0 表示等待事件的毫秒数
epoll_wait(ep_fd, ev, MAX_EPOLL, TIMEOUT);
设置 fd 为非阻塞
fcntl(fd, F_SETFL, O_NONBLOCK); //读写操作会立即返回,不会阻塞等待
挖坑
- 学习 Linux 底层源码(epoll相关的)⭐⭐⭐⭐⭐