基于libco的c++协程实现3(libco源码-resume/swap实现)

这一篇的主题是启动/唤醒,切换协程,这是比较难的一部分。

启动/唤醒(resume)

void co_resume( stCoRoutine_t *co )
{
	if( !co->cStart )
	{
		// 初始化对应的堆栈空间
		coctx_make( &co->ctx, (coctx_pfn_t)CoRoutineFunc, co, 0 );
		// 第一次启动要设置为1, 作为是否要初始化堆栈区的标志
		co->cStart = 1;
	}

	// 从协程中获取env,就是那个每个线程共享的变量,其数组的最后有效的一个就是当前的协程
	stCoRoutineEnv_t* env = co->env;
	stCoRoutine_t* lpCurrRoutine = env->pCallStack[env->iCallStackSize - 1];
	// resume 把新的协程加入数组
	env->pCallStack[ env->iCallStackSize++ ] = co;
	// 新的协程和当前协程切换
	co_swap( lpCurrRoutine, co );
}

resume有两种情况

一种是第一次的启动,另一种是唤醒,以上代码调整了顺序。

启动:如果是第一次需要做一些额外的初始化,包括初始化co->ctx的堆栈空间置为标志

启动/唤醒:都需要从调用栈数组中取出当前协程与即将切入协程交换。

coctx_t 如何实现保存cpu的上下文,使用到coctx_make

int coctx_make(coctx_t* ctx, coctx_pfn_t pfn, const void* s, const void* s1) 
{
	// sp 指向堆顶,一定要减去void*的大小,否则越界
	char* sp = ctx->ss_sp + ctx->ss_size - sizeof(void*);
	// sp 指向堆顶16位对齐后位置 gcc 默认的堆对齐是16字节对齐,sp的指针与操作后往下移
	sp = (char*)((unsigned long)sp & -16LL);
	// 置位寄存器为0
	memset(ctx->regs, 0, sizeof(ctx->regs));
	// sp位置为指向pfn指针
	void** ret_addr = (void**)(sp);
	*ret_addr = (void*)pfn;
	// 设置rsp为返回的的寄存器13 rsp寄存器
	ctx->regs[kRSP] = sp;
	// 设置ret寄存器为pfn地址
	ctx->regs[kRETAddr] = (char*)pfn;
	// 设置第一个参数 rdi
	ctx->regs[kRDI] = (char*)s;
	// 设置第二个参数 rsi,目前没有使用
	ctx->regs[kRSI] = (char*)s1;
	return 0;
}

关于第一句sp指针的位置,为什么是这样呢,请看下图:

 不难理解,我们原来co_create 调用了co_create_env,创建了stack_mem的buffer,而我们就是想用这块buffer,来作为我们存储栈的地方。而因为栈指针都是从高地址往低地址走,这就是我们的sp为什么要对应堆顶的原因。

sp = (char*)((unsigned long)sp & -16LL);

这个的目的是为了字节对齐,16的二进制是10000,一个数与-16相与,相当于屏蔽了低四位,也就是栈sp往下走到16字节对齐的地方。

关于coctx_t结构

struct coctx_t
{
#if defined(__i386__)
	void *regs[ 8 ];
#else
	void *regs[ 14 ];
#endif
	size_t ss_size;	// 栈的大小
	char *ss_sp;	// 栈的buffer
	
};

这里存储了regs[14],本来CPU通用寄存器有16个,这个只定义了14个。

16个cpu的通用寄存器:

 libco定义的reg[14]

//-------------
// 64 bit
  low | regs[0]: r15 |
//    | regs[1]: r14 |
//    | regs[2]: r13 |
//    | regs[3]: r12 |
//    | regs[4]: r9  |
//    | regs[5]: r8  |
//    | regs[6]: rbp |
//    | regs[7]: rdi | 
//    | regs[8]: rsi |
//    | regs[9]: rax | 
//    | regs[10]: rdx |
//    | regs[11]: rcx |
//    | regs[12]: rbx |
 higt | regs[13]: rsp |
enum {
  kRDI = 7,     // 第一个参数
  kRSI = 8,     // 第二个参数
  kRETAddr = 9, // 返回地址
  kRSP = 13,    // 栈指针
};

其实,cpu的寄存器有很多,有45个,但是通用寄存器16个,还有各种各样的寄存器,具体可以看看知乎的 一口气看完45个寄存器,CPU核心技术大揭秘 - 知乎

而协程其实只使用到了通用的寄存器,浮点运算使用到的libco也不做处理,据说libaco这方面做了,有兴趣的可以去围观。

co_swap

void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co)
{
 	stCoRoutineEnv_t* env = co_get_curr_thread_env();
	//get curr stack sp
	char c;
	curr->stack_sp = &c;

	// 切换前的预备动作
	if (!pending_co->cIsShareStack) // 独立栈
	{
		env->pending_co = NULL;
		env->occupy_co = NULL;
	}
	else // 共享栈
	{
		env->pending_co = pending_co;
		// 获取原来占用共享栈的协程
		stCoRoutine_t* occupy_co = pending_co->stack_mem->occupy_co;
		// 切入协程的栈占有协程设置即将切入协程
		pending_co->stack_mem->occupy_co = pending_co;

		env->occupy_co = occupy_co;
		if (occupy_co && occupy_co != pending_co)
		{
			// 如果上一个使用协程不为空,且不是原来的协程,则需要把它的栈内容保存起来
			save_stack_buffer(occupy_co);
		}
	}

	//切换cpu上下文,实现协程切换
	coctx_swap(&(curr->ctx),&(pending_co->ctx) );

	//stack buffer may be overwrite, so get again;
	stCoRoutineEnv_t* curr_env = co_get_curr_thread_env();
	stCoRoutine_t* update_occupy_co =  curr_env->occupy_co;
	stCoRoutine_t* update_pending_co = curr_env->pending_co;
	
	if (update_occupy_co && update_pending_co && update_occupy_co != update_pending_co)
	{
		//resume stack buffer
		if (update_pending_co->save_buffer && update_pending_co->save_size > 0)
		{
			memcpy(update_pending_co->stack_sp, update_pending_co->save_buffer, update_pending_co->save_size);
		}
	}
}

又是一段好长的代码,分三个阶段

切换前:

切换前,获取当前的栈类型,是共享栈还是独立栈。

共享栈:所有协程共享该栈,这就会导致一个问题,当需要切换时,协程需要自己保存起来,否则栈就会被别人的栈盖掉,优点是我可以开辟很大的栈,这样就不太需要关心独立栈的大小问题。

独立栈:每个协程自己维护一个栈空间,所以需要分配好大小,速度比较快。不用每次自己保存和加载。

这就是代码中如果是共享栈,切换前需要保存到自己的空间里面的原因

其中:

char c;
curr->stack_sp = &c;

这个代码,用一种取巧的方式获取到了当前的栈帧,作为后面保存栈时知道要保存哪些数据用。

看看savebuffer时怎么搞的,就是利用了刚刚取到的sp,获取了长度后保存到协程自己的buffer。

看起来不太高效,据说libaco有高效的实现,我还没看。

void save_stack_buffer(stCoRoutine_t* occupy_co)
{
	// 获取当前协程的栈内存
	stStackMem_t* stack_mem = occupy_co->stack_mem;
	// 当前协程的堆顶(栈底) - 实际上运行到的栈位置,那个取地址的作用&c
	int len = stack_mem->stack_bp - occupy_co->stack_sp;
	// 先析构
	if (occupy_co->save_buffer)
	{
		free(occupy_co->save_buffer);
		occupy_co->save_buffer = NULL;
	}
	// 将对应的栈保存起来
	occupy_co->save_buffer = (char*)malloc(len); //malloc buf;
	occupy_co->save_size = len;
	memcpy(occupy_co->save_buffer, occupy_co->stack_sp, len);
}

切换:

这一下就来到了汇编的世界了,如果没有看懂,不急不急,偷偷告诉你,腾讯再2018年之前,这个汇编也是写得有bug的,别说,腾讯都跑了好几年的这二十来行程序,都有bug,你觉得一下子没看明白,那也正常。

#elif defined(__x86_64__)
	leaq (%rsp),%rax
    movq %rax, 104(%rdi)
    movq %rbx, 96(%rdi)
    movq %rcx, 88(%rdi)
    movq %rdx, 80(%rdi)
	movq 0(%rax), %rax
	movq %rax, 72(%rdi) 
    movq %rsi, 64(%rdi)
	movq %rdi, 56(%rdi)
    movq %rbp, 48(%rdi)
    movq %r8, 40(%rdi)
    movq %r9, 32(%rdi)
    movq %r12, 24(%rdi)
    movq %r13, 16(%rdi)
    movq %r14, 8(%rdi)
    movq %r15, (%rdi)
	xorq %rax, %rax

    movq 48(%rsi), %rbp
    movq 104(%rsi), %rsp
    movq (%rsi), %r15
    movq 8(%rsi), %r14
    movq 16(%rsi), %r13
    movq 24(%rsi), %r12
    movq 32(%rsi), %r9
    movq 40(%rsi), %r8
    movq 56(%rsi), %rdi
    movq 80(%rsi), %rdx
    movq 88(%rsi), %rcx
    movq 96(%rsi), %rbx
    leaq 8(%rsp), %rsp
	pushq 72(%rsi)

    movq 64(%rsi), %rsi
	ret
#endif

首先切换的目的,

 1、先将当前cpu寄存器的值保存到要切出去的协程,你看到的,要切出去的协程时第一个参数curr,他的寄存器时rdi,所以第一段就是把一堆当前寄存器保存到第一个参数的curr->ctx的reg中

2、把要切进来的协程的寄存器保存到当前的cpu寄存器,即将切入的协程时第二个参数,再rsi中,然后根据rsi的偏移,实际获得了pending_co->ctx->reg的数据写入到当前的cpu寄存器。这样就完美的实现了切换。

 切换后:

 注意,切换完后,cpu就跑到切入的协程去了,不会再走到红线以下了,要等待下一次协程resume才会走到下面去

下一次协程唤醒resume,则需要把刚刚保存再协程buffer中如果时共享栈的资源的给重新设置到共享栈中。

这就完成了协程的启动,切换,唤醒。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值