注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。
Ntyco源码:
https://github.com/wangbojing/NtyCo
一、实现的思路:
1. scheduler负责给协程分配CPU


2. 上下文切换如何实现
本质:
- resume:调度器从CPU中取走其当前的执行状态然后保存下来,接着把协程保存的之前的执行状态再移动到CPU当中(代表开始执行协程)。
- yield:协程从CPU中取走其当前的执行状态然后保存下来,接着把调度器保存的之前的执行状态再移动到CPU当中(代表开始执行调度器)。
2.1 汇编

2.2 ucontext(见上篇博文)
二、 代码结构
1. 核心数据结构
(1)协程状态(nty_coroutine_status)
定义了协程的生命周期状态,包括:
- 初始化相关:
NTY_COROUTINE_STATUS_NEW(新建)、NTY_COROUTINE_STATUS_READY(就绪)- 运行相关:
NTY_COROUTINE_STATUS_BUSY(运行中)、NTY_COROUTINE_STATUS_EXITED(已退出)- I/O 等待相关:
NTY_COROUTINE_STATUS_WAIT_READ(等待读)、NTY_COROUTINE_STATUS_WAIT_WRITE(等待写)- 其他状态:
SLEEPING(睡眠)、CANCELLED(取消)、DETACH(分离)等。
(2)调度器(nty_schedule)
每个线程有一个调度器(通过pthread_key_t实现线程局部存储),负责协程的管理和调度,核心成员包括:
- 上下文信息(
ctx):使用ucontext_t(系统提供)或自定义nty_cpu_ctx(汇编实现)保存调度器自身的上下文。- 队列 / 链表:
ready(就绪协程队列)、defer(延迟执行队列)、busy(运行中协程链表)。- 红黑树:
sleeping(睡眠协程,按唤醒时间排序)、waiting(I/O 等待协程,按 FD 和事件排序)。- I/O 多路复用:
poller_fd(epoll 实例 FD)、eventlist(epoll 事件列表),用于监听 I/O 事件。

(3)协程(nty_coroutine)
单个协程的核心信息,包括:
- 上下文(
ctx):保存协程的执行状态(寄存器、栈指针等)。- 执行信息:
func(协程函数)、arg(参数)、stack(栈空间)、stack_size(栈大小)。- 状态与调度:
status(当前状态)、sched(所属调度器)、各类队列节点(用于加入就绪 / 等待队列)。- I/O 信息:
fd(关联的文件描述符)、events(等待的事件,如POLLIN)、io(I/O 操作的缓存和结果)。

2. 核心机制
(1)上下文切换
协程的切换是核心,代码提供了两种实现:
- 基于
ucontext:使用系统调用getcontext/swapcontext保存和切换上下文(简单但可能效率较低)。- 自定义汇编 :通过汇编直接操作寄存器(
esp/ebp/eip等),实现轻量切换(_switch函数),支持 x86 和 x86_64 架构。切换逻辑:
nty_coroutine_yield:协程主动让出 CPU,保存自身上下文,切换到调度器上下文。nty_coroutine_resume:调度器唤醒协程,恢复协程上下文,切换到协程执行。
(2)I/O 多路复用与协程调度
通过
epoll监听 I/O 事件,结合协程实现 "非阻塞 I/O + 主动让出 CPU" 的高效模式:
- 当协程执行 I/O 操作(如nty_
recv/nty_accept)时,若 I/O 未就绪(如无数据可读),协程会:
- 将自身注册到
epoll的等待队列(通过nty_schedule_sched_wait)。- 然后插入wait树
- 调用
yield让出 CPU,进入WAIT_READ/WAIT_WRITE状态。- 调度器通过
epoll_wait等待 I/O 事件,当事件触发(如数据到达):
- 调度器从wait树中根据fd找到对应协程,通过
resume恢复执行。- 协程得到执行后,删除注册的epoll事件,移除其所在的wait树,然后执行普通的io操作(recv/accept)
(3)系统调用钩子(COROUTINE_HOOK)
通过
dlsym重写系统调用(如socket/read/send),实现对用户透明的协程化:
- 钩子函数会先检查当前是否在协程环境中(通过
nty_coroutine_get_sched)。- 若在协程中,自动将阻塞式 I/O 转为协程式的io。
- 若不在协程中,直接调用原始系统调用,保证兼容性。
3. 主要功能函数
- 协程管理 :
nty_coroutine_create(创建协程)、nty_coroutine_free(释放协程)、nty_coroutine_sleep(协程睡眠)。- I/O 操作 :
nty_socket(创建非阻塞 socket)、nty_accept/nty_recv/nty_send(协程化的 I/O 函数)。- 调度器运行 :
nty_schedule_run(调度主循环,遍历三种状态结构、选择协程执行)。
4. 适用场景
该协程库适用于高并发网络编程(如 HTTP 服务器、TCP 代理),通过以下特性提升性能:
- 避免线程切换的开销(协程切换在用户态,成本远低于线程)。
- 高效处理大量 I/O 等待(通过 epoll 复用,协程在 I/O 就绪前不占用 CPU)。
- 透明兼容现有系统调用(钩子机制无需修改原有代码逻辑)。
总结
这个协程库通过 "用户态上下文切换 + epoll 多路复用 + 系统调用钩子" 的组合,实现了轻量、高效的并发模型,适合 I/O 密集型场景。核心优势是低开销的协程切换和对阻塞 I/O 的自动适配,让开发者可以用同步代码的逻辑编写异步高性能程序。
三、调度器(nty_schedule)的执行策略
调度器是协程的 "管理者",核心职责是维护协程状态、触发上下文切换、处理 IO 与睡眠等待,执行策略围绕 "高效管理 + 按需调度" 设计。
1. 协程存储与状态管理
- 用不同数据结构分类管理协程,保证操作高效:
- 就绪协程:
TAILQ队列(FIFO,快速入队 / 出队),存储等待执行的协程。- 睡眠协程:
RB树(按睡眠时间排序),快速查找 最近到期" 的协程。- IO 等待协程:
RB树(按文件描述符 fd 排序),快速通过 fd 查找并唤醒等待 IO 的协程。- 忙碌协程:
LIST链表,存储正在执行的协程。

2. 核心调度循环(nty_schedule_run)
调度器通过无限循环实现持续调度,每次循环分 3 步执行:
- 处理过期睡眠协程:遍历睡眠红黑树,取出 "睡眠时间已到" 的协程,通过
nty_coroutine_resume切换上下文,让协程执行;协程执行中若挂起(yield),会切回调度器上下文,继续下面的调度- 执行就绪队列协程:从就绪队列取出协程,通过
nty_coroutine_resume切换上下文,让协程执行;协程执行中若挂起(yield),会切回调度器上下文,继续下面的调度。- 处理 IO 事件:通过
epoll_wait等待 IO 就绪(超时时间由 "最近到期的睡眠协程" 决定),根据fd在等待红黑树中取出对应的协程,通过nty_coroutine_resume切换上下文,让协程执行;协程执行中若挂起(yield),会切回调度器上下文,继续下面的调度。
调度模式有两种(Ntyco采取的是多状态运行模式)
2.1 生产者消费者模式

逻辑代码:


生产者-消费者模式
1. 角色对应
- 生产者 :
sleep_tree(睡眠红黑树)和wait_tree(IO 等待红黑树)。
sleep_tree生产 "睡眠到期" 的协程,将其从睡眠状态转为就绪状态,放入ready_queue。wait_tree生产 "IO 就绪" 的协程,将其从 IO 等待状态转为就绪状态,放入ready_queue。- 缓冲区 :
ready_queue(就绪队列)。存储所有待执行的协程,是生产者和消费者之间的 "数据中介"。- 消费者 :
CPU运行(调度器的执行逻辑)。从ready_queue中取出协程,通过nty_coroutine_resume切换上下文,让协程执行。2. 流程匹配
代码中调度器的核心循环(
nty_schedule_run)完全遵循 "生产→缓冲→消费" 的逻辑:
- 生产者生产:遍历
sleep_tree唤醒到期协程、遍历wait_tree唤醒 IO 就绪协程,将它们生产 到ready_queue。- 消费者消费:从
ready_queue中取出协程,让 CPU 执行(nty_coroutine_resume)。这种 "多生产者(sleep_tree、wait_tree)→ 共享缓冲区(ready_queue)→ 消费者(CPU 运行)" 的结构,是典型的生产者 - 消费者模式。
2.2 多状态运行模式

cpp
void nty_schedule_run(void) {
// 获取当前线程的调度器
nty_schedule *sched = nty_coroutine_get_sched();
if (sched == NULL) return ; // 调度器为空,直接返回
// 调度循环:直到所有协程处理完毕(nty_schedule_isdone返回true)
while (!nty_schedule_isdone(sched)) {
// 1. 处理过期睡眠协程:唤醒所有已到期的睡眠协程并执行
nty_coroutine *expired = NULL;
while ((expired = nty_schedule_expired(sched)) != NULL) {
nty_coroutine_resume(expired); // 恢复协程执行
}
// 2. 处理就绪队列协程:按FIFO顺序执行就绪协程
nty_coroutine *last_co_ready = TAILQ_LAST(&sched->ready, _nty_coroutine_queue);
while (!TAILQ_EMPTY(&sched->ready)) {
nty_coroutine *co = TAILQ_FIRST(&sched->ready); // 取出队列头部协程
TAILQ_REMOVE(&co->sched->ready, co, ready_next); // 从就绪队列移除
// 若协程已连接关闭,释放资源并退出循环
if (co->status & BIT(NTY_COROUTINE_STATUS_FDEOF)) {
nty_coroutine_free(co);
break;
}
nty_coroutine_resume(co); // 恢复协程执行
if (co == last_co_ready) break; // 执行到队列尾部,退出循环
}
// 3. 处理IO事件:唤醒IO就绪的协程并执行
nty_schedule_epoll(sched); // 等待IO事件,获取新事件数
while (sched->num_new_events) {
int idx = --sched->num_new_events; // 从最后一个事件开始处理
struct epoll_event *ev = sched->eventlist+idx; // 当前IO事件
int fd = ev->data.fd; // IO事件对应的fd
int is_eof = ev->events & EPOLLHUP; // 是否为连接关闭事件
if (is_eof) errno = ECONNRESET; // 连接关闭,设置错误码为连接重置
// 查找该fd对应的IO等待协程
nty_coroutine *co = nty_schedule_search_wait(fd);
if (co != NULL) {
if (is_eof) {
co->status |= BIT(NTY_COROUTINE_STATUS_FDEOF); // 标记为连接关闭
}
nty_coroutine_resume(co); // 恢复协程执行
}
is_eof = 0; // 重置连接关闭标记
}
}
nty_schedule_free(sched); // 所有协程处理完毕,释放调度器资源
return ;
}
多状态运行模式:
简单点说,就是依次遍历sleep、ready、wait三个结构体,若有满足需求的,就从结构体里面取走,然后resume调度。
- 处理过期睡眠协程:遍历睡眠红黑树,取出 "睡眠时间已到" 的协程,通过
nty_coroutine_resume切换上下文,让协程执行;协程执行中若挂起(yield),会切回调度器上下文,继续下面的调度- 执行就绪队列协程:从就绪队列取出协程,通过
nty_coroutine_resume切换上下文,让协程执行;协程执行中若挂起(yield),会切回调度器上下文,继续下面的调度。- 处理 IO 事件:通过
epoll_wait等待 IO 就绪(超时时间由 "最近到期的睡眠协程" 决定),根据fd在等待红黑树中取出对应的协程,通过nty_coroutine_resume切换上下文,让协程执行;协程执行中若挂起(yield),会切回调度器上下文,继续下面的调度。
3. 调度触发条件
- 协程主动挂起:协程执行 IO 操作(如 recv)、调用
sleep或yield,会主动放弃 CPU,切回调度器。 - 事件唤醒触发:三个结构体里面有需要执行的协程就执行。
- 协程退出触发:协程执行完函数后标记EXITED,yield之后,调度器会判断它的状态,看是否需要释放。
简单点讲:就是当前CPU运行在调度器,然后调度器遍历到有需要执行的协程就resume调度
四、协程(nty_coroutine)的执行流程
协程的生命周期围绕 "状态转换" 展开,从创建到退出全程由调度器管理,核心流程分 5 个阶段:
1. 创建与初始化
- 调用
nty_coroutine_create:分配协程内存和栈空间,初始化状态为NEW,绑定执行函数func和参数arg,加入调度器的就绪队列。
新加入的协程都是在就绪队列里面被取走的
- 首次执行前初始化:调度器判断协程当前状态是否为NEW,若是通过
nty_coroutine_init设置协程上下文(绑定栈、执行入口_exec),状态改为READY。
2. 调度与执行
- 调度器取出协程,调用
nty_coroutine_resume切换上下文,协程开始执行func。 - 执行中若未触发挂起,会一直运行到函数结束,标记状态为
EXITED+DETACH,然后调用yield切回调度器。
3. 挂起场景与状转换
协程执行中会因 3 种情况挂起,状态切换为对应类型:
- IO 等待:执行 Hook 后的 IO 函数(如
nty_recv),向 epoll 注册事件后,状态改为WAIT_READ/WAIT_WRITE,加入 IO 等待红黑树,调用yield挂起。 - 睡眠:调用
nty_coroutine_sleep,状态改为SLEEPING,加入睡眠红黑树,调用yield挂起。
协程挂起前(调用yield前),至少会插入3个结构体当中的一个
4. 唤醒与恢复
- 睡眠过期:调度器遍历睡眠红黑树,找到 "睡眠时间已到" 的协程,然后resume调度
- 新插入,位于ready队列,调度器遍历到,先给它
nty_coroutine_init,然后resume调度。 - IO 就绪:epoll 检测到 fd 可读 / 可写,调度器从 IO 红黑树找到对应协程,然后resume调度
协程唤醒后(被yeild分配到CPU),会先移除其在所有结构体中的节点(比如它是在wait树中唤醒的,那它会移除其在wait树中的节点,如果该协程也在sleep树中,它也会选择移除其在sleep树中的节点.....。其他情况同理)。
恢复执行:调度器下次循环时,通过resume切换上下文,协程从挂起点继续执行。
5. 退出与释放
- 协程执行完
func后,标记EXITED+FDEOF+DETACH,调用yield切回调度器。 - 调度器在
resume返回(即协程调用yield返回调度器当前的执行状态)后,检测到EXITED状态,调用nty_coroutine_free释放协程内存和栈空间。

五、hook的设计
代码提供hook选项
COROUTINE_HOOK开启的话:(默认开启)
在协程函数中accept之类的API自动转换为协程式的API。
- 这样子能快速应用于原有项目,提升一大截性能
- 同时一些封装的库(如mysql)内部的send和recv通过hook都可以直接切换成协程式的
本质:通过dlsym重写系统调用(如socket/read/send),实现对用户透明的协程化:dlsym: 取出系统级别对应的函数如read,赋值给read_f。
实现将nty_read改成函数名read,只需把nty_read原来里面的read改为read_f即可。
钩子函数会先检查当前是否在协程环境中(通过
nty_coroutine_get_sched)。
- 若在协程中,自动将阻塞式 I/O 转为协程式的io。
- 若不在协程中,直接调用原始系统调用,保证兼容性。
cpp
#ifdef COROUTINE_HOOK
socket_t socket_f = NULL;
read_t read_f = NULL;
recv_t recv_f = NULL;
recvfrom_t recvfrom_f = NULL;
write_t write_f = NULL;
send_t send_f = NULL;
sendto_t sendto_f = NULL;
accept_t accept_f = NULL;
close_t close_f = NULL;
connect_t connect_f = NULL;
int init_hook(void) {
socket_f = (socket_t)dlsym(RTLD_NEXT, "socket");
read_f = (read_t)dlsym(RTLD_NEXT, "read");
recv_f = (recv_t)dlsym(RTLD_NEXT, "recv");
recvfrom_f = (recvfrom_t)dlsym(RTLD_NEXT, "recvfrom");
write_f = (write_t)dlsym(RTLD_NEXT, "write");
send_f = (send_t)dlsym(RTLD_NEXT, "send");
sendto_f = (sendto_t)dlsym(RTLD_NEXT, "sendto");
accept_f = (accept_t)dlsym(RTLD_NEXT, "accept");
close_f = (close_t)dlsym(RTLD_NEXT, "close");
connect_f = (connect_t)dlsym(RTLD_NEXT, "connect");
}
cpp
int accept(int fd, struct sockaddr *addr, socklen_t *len) {
if (!accept_f) init_hook();
nty_schedule *sched = nty_coroutine_get_sched();
if (sched == NULL) {
return accept_f(fd, addr, len);
}
int sockfd = -1;
int timeout = 1;
nty_coroutine *co = nty_coroutine_get_sched()->curr_thread;
while (1) {
struct pollfd fds;
fds.fd = fd;
fds.events = POLLIN | POLLERR | POLLHUP;
nty_poll_inner(&fds, 1, timeout);
sockfd = accept_f(fd, addr, len);
if (sockfd < 0) {
if (errno == EAGAIN) {
continue;
} else if (errno == ECONNABORTED) {
printf("accept : ECONNABORTED\n");
} else if (errno == EMFILE || errno == ENFILE) {
printf("accept : EMFILE || ENFILE\n");
}
return -1;
} else {
break;
}
}
int ret = fcntl(sockfd, F_SETFL, O_NONBLOCK);
if (ret == -1) {
close(sockfd);
return -1;
}
int reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
return sockfd;
}
上面的accept实现的功能跟nty_accept()相同
cpp
int nty_accept(int fd, struct sockaddr *addr, socklen_t *len) {
int sockfd = -1;
int timeout = 1;
nty_coroutine *co = nty_coroutine_get_sched()->curr_thread;
while (1) {
struct pollfd fds;
fds.fd = fd;
fds.events = POLLIN | POLLERR | POLLHUP;
nty_poll_inner(&fds, 1, timeout);
sockfd = accept(fd, addr, len);
if (sockfd < 0) {
if (errno == EAGAIN) {
continue;
} else if (errno == ECONNABORTED) {
printf("accept : ECONNABORTED\n");
} else if (errno == EMFILE || errno == ENFILE) {
printf("accept : EMFILE || ENFILE\n");
}
return -1;
} else {
break;
}
}
int ret = fcntl(sockfd, F_SETFL, O_NONBLOCK);
if (ret == -1) {
close(sockfd);
return -1;
}
int reuse = 1;
setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, (char *)&reuse, sizeof(reuse));
return sockfd;
}