在学习IO协程调度器之前必须掌握协程调度器,此外还需要对epoll
相关接口非常熟悉,可以参考man 7 epoll
在协程调度模块中,调度器对协程的调度是无条件执行的,在调度器已经启动调度的情况下,任务一旦添加成功,就会排队等待调度器执行。调度器不支持删除调度任务,并且调度器在正常退出之前一定会执行完全部的调度任务。
IO
协程调度器直接继承协程调度器实现,因此支持协程调度器的全部功能。此外IO
协程调度还增加了IO
事件调度的功能,这个功能针对socket fd
。IO
协程调度支持为socket fd
注册可读和可写事件的回调函数或协程,当描述符可读或可写时,执行对应的回调函数或协程
IO
事件调度功能对服务器开发至关重要,因为服务器通常需要处理大量来自客户端的socket fd
,使用IO
事件调度可以将开发者从判断socket fd
是否可读或可写的工作中解放出来,使得程序员只需要关心socket fd
的IO
操作
https://github.com/huxiaohei/tiger.git
tiger
的IO
协程调度模块基于epoll
实现,只支持Linux
平台。
对每个socket fd
支持两类事件,一类是可读事件EPOLLIN
,一类是可写事件EPOLLOUT
。当然epoll
本身除了支持了EPOLLIN
和EPOLLOUT
两类事件外,还支持EPOLLRDHUP
,EPOLLERR
,EPOLLHUP
等。为了简化操作,tiger
是将所有事件全部归为EPOLLIN
和EPOLLOUT
中,也就是所有的事件都可以表示为可读或可写事件,甚至有的事件还可以同时表示可读及可写事件,比如EPOLLERR
事件发生时,socket fd
将同时触发可读和可写事件
enum EventStatus {
NONE = 0x0,
READ = 0x001,
WRITE = 0x004
};
对于IO
协程调度来说,每次调度都包含一个三元组信息(描述符-事件类型-回调信息
),我们通过一个结构体Context
来存储三原组信息。因为一个描述符可能同时注册读和写事件,因此分别使用read
和write
属性来保存读和写的回调信息,从而在不同事件类型被触发时执行对应回调
typedef struct {
struct EventContext {
Scheduler::ptr scheduler;
pid_t thread_id;
Coroutine::ptr co;
std::function<void()> cb;
};
MutexLock mutex;
int fd = 0;
EventStatus statuses = EventStatus::NONE;
EventContext read;
EventContext write;
EventContext &get_event_context(const EventStatus status);
void reset_event_context(EventContext &ctx);
void trigger_event(const EventStatus status);
} Context;
IO
协程调度器会将全部的三元组信息存储在属性m_contexts
中,m_contexts
的类型为std::vector
。IO
调度器直接使用三元组的fd
的值作为m_contexts
数组的下标,这样可以快速找到一个fd
对应的Context
对象。由于关闭的fd
会被重复利用,所以这里也不用担心m_contexts
数组膨胀太快,或是利用率低的问题
IO
协程调度器提供一个添加注册事件的接口bool IOManager::add_event(int fd, EventStatus status, std::function
,接口需要指定fd
,事件类型,回调函数三个参数。
首先我们通过fd
从m_contexts
中取出一个Context
对象(如果fd
大于m_contexts
的长度,表明m_contexts
需要先进行扩容),然后使用status
和cb
两个参数来重新初始化Context
对象
注册事件即向一个epoll
实例中的一个fd
添加一个epoll_event
,然后使用epoll_event.data.ptr
指针指向初始化好的Context
对象,以便后续epoll_wait
返回时能拿到fd
的上下文信息即Context
,并且执行其中的回调函数或协程
bool IOManager::add_event(int fd, EventStatus status, std::function<void()> cb) {
ReadWriteLock::ReadLock rlock(m_lock);
Context *ctx = nullptr;
if ((int)m_contexts.size() > fd) {
ctx = m_contexts[fd];
rlock.unlock();
} else {
rlock.unlock();
ReadWriteLock::WriteLock wlock(m_lock);
fd_context_resize(fd * 1.5);
ctx = m_contexts[fd];
}
MutexLock::Lock mlock(ctx->mutex);
if (ctx->statuses & status) {
TIGER_LOG_E(SYSTEM_LOG) << "[add_event fail"
<< " statuses:" << ctx->statuses
<< " status:" << status << "]";
return false;
}
int op = ctx->statuses ? EPOLL_CTL_MOD : EPOLL_CTL_ADD;
epoll_event event;
event.events = ctx->statuses | status | EPOLLET;
event.data.ptr = ctx;
if (epoll_ctl(m_epfd, op, fd, &event)) {
TIGER_LOG_E(SYSTEM_LOG) << "[epoll_ctl fail"
<< " epfd" << m_epfd
<< " op" << op
<< " fd" << fd
<< " errno" << strerror(errno) << "]";
return false;
}
++m_appending_event_cnt;
ctx->statuses = (EventStatus)(ctx->statuses | status);
auto &event_context = ctx->get_event_context(status);
event_context.scheduler = Scheduler::GetThreadScheduler();
if (cb) {
event_context.cb.swap(cb);
} else {
event_context.co = Coroutine::GetRunningCo();
event_context.thread_id = Thread::CurThreadId();
}
return true;
}
了解上面注册事件的流程后,删除事件其实与上面的流程非常的类似。这里就不过多的解释了。
bool IOManager::del_event(int fd, EventStatus status) {
ReadWriteLock::ReadLock rlock(m_lock);
if ((int)m_contexts.size() <= fd) {
TIGER_LOG_E(SYSTEM_LOG) << "[del_event fail"
<< " fd:" << fd
<< " size:" << m_contexts.size() << "]";
return false;
}
auto ctx = m_contexts[fd];
if (!(ctx->statuses & status)) {
TIGER_LOG_E(SYSTEM_LOG) << "[del_event fail"
<< " fd:" << fd
<< " statuses:" << ctx->statuses
<< " status:" << status << "]";
return false;
}
rlock.unlock();
MutexLock mlock(ctx->mutex);
EventStatus new_status = (EventStatus)(ctx->statuses & ~status);
int op = new_status ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
epoll_event event;
event.events = EPOLLET | new_status;
event.data.ptr = ctx;
if (epoll_ctl(m_epfd, op, fd, &event)) {
TIGER_LOG_E(SYSTEM_LOG) << "[epoll_ctl fail"
<< " epfd:" << m_epfd
<< " op:" << op
<< " fd:" << fd
<< " errno:" << strerror(errno) << "]";
return false;
}
--m_appending_event_cnt;
ctx->statuses = new_status;
auto &event_context = ctx->get_event_context(new_status);
ctx->reset_event_context(event_context);
return true;
}
取消事件与删除事件也非常的类似,区别只在于取消事件在删除epoll_event
上的事件之后,会主动触发一次该事件的回调,在此也不过多解释
bool IOManager::cancel_event(int fd, EventStatus status) {
ReadWriteLock::ReadLock rlock(m_lock);
if ((int)m_contexts.size() <= fd) {
TIGER_LOG_E(SYSTEM_LOG) << "[cancel_event fail"
<< " fd:" << fd
<< " size:" << m_contexts.size() << "]";
return false;
}
auto ctx = m_contexts[fd];
if (!(ctx->statuses & status)) {
TIGER_LOG_E(SYSTEM_LOG) << "[cancel_event fail"
<< " fd:" << fd
<< " statuses:" << ctx->statuses
<< " status:" << status << "]";
return false;
}
rlock.unlock();
MutexLock::Lock mlock(ctx->mutex);
EventStatus new_status = (EventStatus)(ctx->statuses & ~status);
int op = new_status ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
epoll_event event;
event.events = EPOLLET | new_status;
event.data.ptr = ctx;
if (epoll_ctl(m_epfd, op, fd, &event)) {
TIGER_LOG_E(SYSTEM_LOG) << "[epoll_ctl fail"
<< " epfd:" << m_epfd
<< " op:" << op
<< " fd:" << fd
<< " errno:" << strerror(errno) << "]";
return false;
}
--m_appending_event_cnt;
ctx->trigger_event(status);
return true;
}
IO
协程调度器为了解决协程调度器在idle
状态下忙等待导致CPU
占用率高的问题。IO
协程调度器使用一对管道fd
来tickle
调度协程,当调度器空闲时idle
协程通过epoll_wait
阻塞在管道的读描述符上,等待管道的可读事件。添加新任务时tickle
方法写管道,idle
协程检测到管道可读后退出,调度器执行调度
void IOManager::idle() {
epoll_event *events = new epoll_event[256]();
int rt = 0;
std::vector<std::function<void()>> cbs;
while (true) {
rt = 0;
do {
static const int EPOLL_MAX_TIMEOUT = 5000;
time_t next_time = next_timer_left_time();
rt = epoll_wait(m_epfd, events, 256, next_time > EPOLL_MAX_TIMEOUT ? EPOLL_MAX_TIMEOUT : (int)next_time);
if (rt > 0 || next_timer_left_time() <= 0 || is_stopping()) break;
} while (true);
all_expired_cbs(cbs);
schedules(cbs.begin(), cbs.end());
cbs.clear();
for (int i = 0; i < rt; ++i) {
epoll_event &event = events[i];
if (event.data.fd == m_tickle_fds[0]) {
uint8_t dummy;
while (read(m_tickle_fds[0], &dummy, 1) == 1)
continue;
continue;
}
Context *ctx = (Context *)event.data.ptr;
MutexLock::Lock lock(ctx->mutex);
if (event.events & (EPOLLERR | EPOLLHUP)) {
event.events |= EPOLLIN | EPOLLOUT;
}
EventStatus real_status = EventStatus::NONE;
if (event.events & EPOLLIN) {
real_status = EventStatus::READ;
}
if (event.events & EPOLLOUT) {
real_status = EventStatus::WRITE;
}
if ((EventStatus)(ctx->statuses & real_status) == EventStatus::NONE)
continue;
EventStatus left_status = (EventStatus)(ctx->statuses & ~real_status);
int op = left_status ? EPOLL_CTL_MOD : EPOLL_CTL_DEL;
event.events = EPOLLET | left_status;
if (epoll_ctl(m_epfd, op, ctx->fd, &event)) {
TIGER_LOG_E(SYSTEM_LOG) << "[epoll_ctl fail"
<< " epfd:" << m_epfd
<< " op:" << op
<< " fd:" << ctx->fd
<< " errno:" << strerror(errno) << "]";
continue;
}
if (real_status & EventStatus::READ) {
ctx->trigger_event(EventStatus::READ);
--m_appending_event_cnt;
}
if (real_status & EventStatus::WRITE) {
ctx->trigger_event(EventStatus::WRITE);
--m_appending_event_cnt;
}
}
{
MutexLock::Lock lock(m_mutex);
if (is_stopping() && m_tasks.size() == 0) {
if (main_thread_id() == Thread::CurThreadId()) {
if (thread_cnt() == 0) break;
} else {
break;
}
}
}
Coroutine::Yield();
}
}