Linux C/C++ 学习日记(32):协程(二):Ntyco源码解析

注:该文用于个人学习记录和知识交流,如有不足,欢迎指点。

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" 的高效模式:

  1. 当协程执行 I/O 操作(如nty_recv/nty_accept)时,若 I/O 未就绪(如无数据可读),协程会:
    • 将自身注册到epoll的等待队列(通过nty_schedule_sched_wait)。
    • 然后插入wait树
    • 调用yield让出 CPU,进入WAIT_READ/WAIT_WRITE状态。
  2. 调度器通过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 步执行:

  1. 处理过期睡眠协程:遍历睡眠红黑树,取出 "睡眠时间已到" 的协程,通过nty_coroutine_resume切换上下文,让协程执行;协程执行中若挂起(yield),会切回调度器上下文,继续下面的调度
  2. 执行就绪队列协程:从就绪队列取出协程,通过nty_coroutine_resume切换上下文,让协程执行;协程执行中若挂起(yield),会切回调度器上下文,继续下面的调度。
  3. 处理 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)完全遵循 "生产→缓冲→消费" 的逻辑:

  1. 生产者生产:遍历sleep_tree唤醒到期协程、遍历wait_tree唤醒 IO 就绪协程,将它们生产ready_queue
  2. 消费者消费:从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调度。

  1. 处理过期睡眠协程:遍历睡眠红黑树,取出 "睡眠时间已到" 的协程,通过nty_coroutine_resume切换上下文,让协程执行;协程执行中若挂起(yield),会切回调度器上下文,继续下面的调度
  2. 执行就绪队列协程:从就绪队列取出协程,通过nty_coroutine_resume切换上下文,让协程执行;协程执行中若挂起(yield),会切回调度器上下文,继续下面的调度。
  3. 处理 IO 事件:通过epoll_wait等待 IO 就绪(超时时间由 "最近到期的睡眠协程" 决定),根据fd在等待红黑树中取出对应的协程,通过nty_coroutine_resume切换上下文,让协程执行;协程执行中若挂起(yield),会切回调度器上下文,继续下面的调度。

3. 调度触发条件

  • 协程主动挂起:协程执行 IO 操作(如 recv)、调用sleepyield,会主动放弃 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;
}
相关推荐
西岸行者5 天前
学习笔记:SKILLS 能帮助更好的vibe coding
笔记·学习
悠哉悠哉愿意5 天前
【单片机学习笔记】串口、超声波、NE555的同时使用
笔记·单片机·学习
别催小唐敲代码5 天前
嵌入式学习路线
学习
毛小茛5 天前
计算机系统概论——校验码
学习
babe小鑫5 天前
大专经济信息管理专业学习数据分析的必要性
学习·数据挖掘·数据分析
winfreedoms5 天前
ROS2知识大白话
笔记·学习·ros2
在这habit之下5 天前
Linux Virtual Server(LVS)学习总结
linux·学习·lvs
我想我不够好。5 天前
2026.2.25监控学习
学习
im_AMBER5 天前
Leetcode 127 删除有序数组中的重复项 | 删除有序数组中的重复项 II
数据结构·学习·算法·leetcode
CodeJourney_J5 天前
从“Hello World“ 开始 C++
c语言·c++·学习