ntyco协程的理解

早在很久之前第一份工作,就接触使用过协程,对它只有只可理解不可言说的概念,这里通过对ntyco协程代码的理解,梳理一下自己对协程的理解。

1:协程的概念

总是把协程俗称为用户态线程,我的理解是,协程依托于线程上执行子例程,由用户态把控协程的调度及上下文。

2:协程的性能

协程的性能差不多和线程池的性能相同,差不到哪里去(多线程有线程调度切换开销,协程有管理节点的内存消耗)

总结:协程同步的变成方式,可以达到异步的性能。

3:什么时候使用协程

百科上说:适合于用来实现彼此熟悉的程序组件,如合作式多任务,迭代器,无限列表和管道。

我想了想,这个使用场景其实还是没有明确的理解:能使用协程解决的问题,一般都可用多线程线程池的方式去替代。

协程可以处理一些上下文相关的io任务,不用关心同步资源共享的问题,处理IO密集型任务,对计算机cpu消耗不高的任务。

协程可以配合网络IO一起使用,IO的底层控制其实是epoll,所以性能上的提升应该也有限制。

4:ntyco中协程的实现

1:协程实现关注的技术点:

1:协程的结构如何定义。

2:协程的内存如何存储,初始化寄存器。

3:协程的如何调度切换。

4:如果管理协程状态调度节点,协程的使用逻辑(模拟文件描述符用epoll管理)。

2:具体分析协程的实现

1:概述:从业务流程思考

1:创建协程

2:任务的执行,调用回调函数和函数参数

3:多个协程,多个回调函数之间的切换。 ==》保存上一个协程的信息,和当前执行的协程,实现切换

4:协程的销毁,资源释放。

2:协程的结构

1:协程的节点:

​ 寄存器中保存节点正在执行函数的上下文,执行的回调函数,函数参数

​ 依托于线程,要保存线程栈空间的大小,栈地址

​ 其他:协程的创建时间戳,协程的id,函数名,网络io处理相关等。

typedef void(* proc_coroutine)(void*);
typedef struct _nty_coroutine{
	nty_cpu_ctx ctx;        //寄存器中的信息
	proc_coroutine * func;  //真正执行的回调函数
	void * arg;				//回调函数对应的参数

	void *stack;			//线程栈的管理
	size_t stack_size;      
	size_t last_stack_size;

	uint64_t birth;     //协程的创建时间,一些标识
	uint64_t id;
	char funcname[64];
	
	uint32_t ops;				 //正在运行标志
	nty_coroutine_status status; //协程当前的状态

	struct _nty_schedule *sched;   	//协程的管理节点
}nty_coroutine;

1.1 寄存器中保存上下文关系的结构定义: (由汇编代码实现寄存器的切换,实现协程的切换)
//寄存器中保存的cpu中的设置:
typedef struct _nty_cpu_ctx {
	void *esp; //
	void *ebp;
	void *eip;
	void *edi;
	void *esi;
	void *ebx;
	void *r1;
	void *r2;
	void *r3;
	void *r4;
	void *r5;
} nty_cpu_ctx;
2:协程的管理节点:

​ 协程的状态: 就绪,等待,睡眠,正在执行

​ 用两个红黑树管理睡眠和等待的协程,用队列管理准备就绪的协程,用链表管理正在执行的协程(其实一个元素就够了)

​ 其他管理:创建时间,个数,协程切换时间,内存地址,页大小(管理内存)

​ 保存正在执行的寄存器上下文,执行中。 ==》其实也就是当前协程

//协程的管理节点
typedef struct _nty_schedule{
	nty_cpu_ctx ctx;					//最近一次resume的协程的内容
	nty_coroutine *curr_thread;			//当前执行的协程     ==》配合控制协程的切换

	void *	stack;
	size_t 	stack_size;					 //每个协程分配的内存的大小
	int		page_size;                   //内存分页的大小   与栈内存做对比处理

	uint64_t 	birth;
	int 		spawned_coroutines;
	uint64_t 	default_timeout;

	//epoll处理 用eventfd 文件描述符的形式管理了红黑树节点
	int poller_fd;
	int eventfd;              //好像没有用到该节点   不用保存加入epoll中的fd
	struct epoll_event eventlist[NTY_CO_MAX_EVENTS];	//触发事件
	int num_new_events;        							//触发事件个数

	//用到的相关结构红黑树:
	nty_coroutine_rbtree_sleep sleeping;
	nty_coroutine_rbtree_wait waiting;

	//用到的相关队列
	nty_coroutine_queue ready;
	//管理正在执行的链表
	nty_coroutine_link busy;
}nty_schedule;

3:协程的创建及初始化

1:初始化管理节点 nty_schedule,一组协程节点管理只需要一个

在协程节点中实际触发。

pthread_key_create,pthread_setspecific和pthread_getpecific实现线程中不同的函数共享数据的方式,并设置资源的释放 ==》这里可以用函数的返回值返回结构形式代替,自己管理释放。
在这里插入图片描述

2:协程节点的初始化。 ==》为协程分配内存,处理寄存器节点执向该内存。

创建一个协程节点,申请内存,放入就绪队列中等待触发。

使寄存器节点中信息指向该内存。 ==》这里的内存与寄存器还没有关联,需要做一定的处理

ESP 专门用作堆栈指针,被形象地称为栈顶指针,堆栈的顶部是地址小的区域,压入堆栈的数据越多,ESP也就越来越小。在32位平台上,ESP每次减少4字节。

esp:寄存器存放当前线程的栈顶指针
ebp:寄存器存放当前线程的栈底指针
eip:寄存器存放下一个CPU指令存放的内存地址,当CPU执行完当前的指令后,从EIP寄存器中读取下一条指令的内存地址,然后继续执行。
3:触发执行
1:主要的控制调度的两个函数,内部都是同样的线程切换逻辑。
void nty_coroutine_yield(nty_coroutine *co); //该协程睡眠     修改运行标志,切换下一个运行
	//调用switch函数进行切换
int nty_coroutine_resume(nty_coroutine *co); //恢复协程的执行  根据标志,进行初始化寄存器位置,切换执行,资源释放。  根据页管理内存
	//resume对没有执行过的协程的处理 初始化寄存器位置
    //resume对已经执行过的协程的处理 直接调用switch函数进行切换
2:核心模块:汇编实现的协程切换(切换寄存器中的信息)

操作的结构:实际上就是每个协程节点中保存的寄存器信息。

与当前寄存器中的信息进行交换处理:

x86_64 的寄存器有1664位寄存器,分别是:%rax, %rbx, %rcx, %esi, %edi, %rbp, %rsp, %r8, %r9, %r10, %r11, %r12, %r13, %r14, %r15。

%rax 作为函数返回值使用的。
%rsp 栈指针寄存器,指向栈顶
%rdi, %rsi, %rdx, %rcx, %r8, %r9 用作函数参数,依次对应第1参数,第2参数。。。
%rbx, %rbp, %r12, %r13, %r14, %r15 用作数据存储,遵循调用者使用规则,换句话说,就是随便用。调用子函数之前要备份它,以防它被修改
%r10, %r11 用作数据存储,就是使用前要先保存原值。

//实现两部分功能  先保存寄存器中的值,再恢复到寄存器中  
//第一个参数要运行的协程, 第二个为正在运行的协程
int _switch(nty_cpu_ctx *new_ctx, nty_cpu_ctx *cur_ctx);

__asm__ (
"    .text                                  \n"
"       .p2align 4,,15                                   \n"
".globl _switch                                          \n"
".globl __switch                                         \n"
"_switch:                                                \n"
"__switch:                                               \n"
"       movq %rsp, 0(%rsi)      # save stack_pointer     \n"
"       movq %rbp, 8(%rsi)      # save frame_pointer     \n"
"       movq (%rsp), %rax       # save insn_pointer      \n"
"       movq %rax, 16(%rsi)                              \n"
"       movq %rbx, 24(%rsi)     # save rbx,r12-r15       \n"
"       movq %r12, 32(%rsi)                              \n"
"       movq %r13, 40(%rsi)                              \n"
"       movq %r14, 48(%rsi)                              \n"
"       movq %r15, 56(%rsi)                              \n"
"       movq 56(%rdi), %r15                              \n"
"       movq 48(%rdi), %r14                              \n"
"       movq 40(%rdi), %r13     # restore rbx,r12-r15    \n"
"       movq 32(%rdi), %r12                              \n"
"       movq 24(%rdi), %rbx                              \n"
"       movq 8(%rdi), %rbp      # restore frame_pointer  \n"
"       movq 0(%rdi), %rsp      # restore stack_pointer  \n"
"       movq 16(%rdi), %rax     # restore insn_pointer   \n"
"       movq %rax, (%rsp)                                \n"
"       ret                                              \n"
);

按照x86_64的寄存器定义,
    %rdi保存第一个参数的值,即new_ctx的值,
    %rsi保存第二个参数的值,即保存cur_ctx的值。
    X86_64每个寄存器是64bit,8byte。

Movq %rsp, 0(%rsi) 保存在栈指针到cur_ctx实例的rsp项
Movq %rbp, 8(%rsi)
Movq (%rsp), %rax #将栈顶地址里面的值存储到rax寄存器中。Ret后出栈,执行栈顶
Movq %rbp, 8(%rsi) #后续的指令都是用来保存CPU的寄存器到new_ctx的每一项中
Movq 8(%rdi), %rbp #将new_ctx的值
Movq 16(%rdi), %rax #将指令指针rip的值存储到rax中
Movq %rax, (%rsp) # 将存储的rip值的rax寄存器赋值给栈指针的地址的值。

Ret # 出栈,回到栈指针,执行rip指向的指令。
3:nty_coroutine_yield 和nty_coroutine_resume函数细节

只有这两个函数调用协程的切换,控制了协程的调度。

nty_coroutine_yield :保存寄存器中的值,让出cpu,执行到最近的resume

​ ==》这里应该获取一个协程,然后进行切换的。

nty_coroutine_resume:第一次的初始化,协程中初始化寄存器信息,切换cpu,执行该协程。

​ 整个调度控制由nty_coroutine_resume+协程数据结构控制。

4:调度控制 nty_schedule_run

1:判断管理节点中各数据结构中是否有相关的任务。

2:处理睡眠到时的协程,进行触发执行,红黑树结构

3:处理准备就绪的协程,进行触发执行,队列结构。

4:处理有事件触发的协程,进行触发执行,红黑树结构。 ==》多于网络io配合

nty_coroutine_resume 函数触发的执行。

5:资源的销毁:

1:协程节点的资源销毁:根据状态调度销毁函数。

2:管理节点的资源销毁:通过pthread_once函数进行设置,销毁堆内存。

4:协程与网络IO适配使用

ntyco中对网络通信相关的api做了相关的处理,实现了一套以协程为机制的套接字处理方案。

主要函数如下:

​ 其实主要业务逻辑就是把本次的fd进行加入epoll中,并管理自己的红黑树节点加入

处理已经触发的一个业务。

删除已经触发的业务的epoll监听和红黑树管理

static int nty_poll_inner(struct pollfd *fds, nfds_t nfds, int timeout) {

	if (timeout == 0)
	{
		return poll(fds, nfds, timeout);   //这里没有用到poll的相关处理
	}
	if (timeout < 0)
	{
		timeout = INT_MAX;
	}

	nty_schedule *sched = nty_coroutine_get_sched();
	nty_coroutine *co = sched->curr_thread;
	
	int i = 0;
	for (i = 0;i < nfds;i ++) {
	
		struct epoll_event ev;
		ev.events = nty_pollevent_2epoll(fds[i].events);   //与poll兼容,把事件转为epoll
		ev.data.fd = fds[i].fd;
		epoll_ctl(sched->poller_fd, EPOLL_CTL_ADD, fds[i].fd, &ev);  //fd加入epoll中

		co->events = fds[i].events;
		nty_schedule_sched_wait(co, fds[i].fd, fds[i].events, timeout); //把fd与协程关联,并加入红黑树中
	}
    //特别注意,这个很关键,虽然参数没变,但是其实内部已经切换了下一个协程,最近resume的协程
	nty_coroutine_yield(co); 

	for (i = 0;i < nfds;i ++) {
	
		struct epoll_event ev;
		ev.events = nty_pollevent_2epoll(fds[i].events);
		ev.data.fd = fds[i].fd;
		epoll_ctl(sched->poller_fd, EPOLL_CTL_DEL, fds[i].fd, &ev);  //已经执行了这个协程事件,所以在epoll中移除该节点

		nty_schedule_desched_wait(fds[i].fd);  //从事件触发wait红黑树中删除该协程
	}

	return nfds;
}

5:扩展及相关demo

扩展:可以使用多线程,多进程(绑核)的方式实现多协程。

ntyco中的demo运行可以适配理解协程的实现逻辑,尤其是网络io相关的demo。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值