libco ——从协程的使用来理清逻辑(系列专栏精华)

协程的创建:co_create()

前面系列的文章已经提到过来,libco协程使用的第一步就是使用co_create()函数创建一个协程,其定义在co_routine.cpp文件中:

int co_create( stCoRoutine_t **ppco,const stCoRoutineAttr_t *attr,pfn_co_routine_t pfn,void *arg );

我们首先来解释以下参数,然后再去看函数的定义:

  • ppco :这是一个输出参数,co_create会创建一个协程控制块CPB,可以类比于进程控制块PCB
  • attr :输入参数,还记得stCoRoutineAttr_t是个什么结构么?其用于指定要创建协程的属性。
  • pfn:其本质是void *(*pfn_co_routine_t)( void * ),也就是协程执行的任务函数
  • arg:传递给任务函数的参数

知道了这些参数,我们再来看一下co_create干了什么事情:

int co_create( stCoRoutine_t **ppco,const stCoRoutineAttr_t *attr,pfn_co_routine_t pfn,void *arg )
{
	//如果没初始化当前线程环境就初始化当前环境
	if( !co_get_curr_thread_env() ) 
	{
		co_init_curr_thread_env();
	}
	//创建协程对象
	stCoRoutine_t *co = co_create_env( co_get_curr_thread_env(), attr, pfn,arg );
	//ppco作为输出参数,得到这个协程对象的地址
	*ppco = co;
	return 0;
}

初始化线程环境:co_init_curr_thread_env()

可以看到,co_create会先判断当前的环境是否初始化,判断标准是一个全局变量gCoEnvPerThread,其是一个stCoRoutineEnv_t类型的指针:

static __thread stCoRoutineEnv_t* gCoEnvPerThread = NULL;

stCoRoutineEnv_t *co_get_curr_thread_env()
{
	return gCoEnvPerThread;
}

如果当前线程环境未初始化,那么就调用co_init_curr_thread_env()函数去初始化:

void co_init_curr_thread_env()
{
	//申请内存
	gCoEnvPerThread = (stCoRoutineEnv_t*)calloc( 1, sizeof(stCoRoutineEnv_t) );
	stCoRoutineEnv_t *env = gCoEnvPerThread;

	//初始化当前栈指针未栈底
	env->iCallStackSize = 0;
	//创建主协程
	struct stCoRoutine_t *self = co_create_env( env, NULL, NULL,NULL );
	self->cIsMain = 1;

	//初始化主协程共享栈相关指针未空
	env->pending_co = NULL;
	env->occupy_co = NULL;

	//初始化主协程CPU上下文
	coctx_init( &self->ctx );

	//将主协程放入协程跟踪栈的栈底
	env->pCallStack[ env->iCallStackSize++ ] = self;

	//创建一个事件循环
	stCoEpoll_t *ev = AllocEpoll();
	//这里先不解释,暂且跳过,可以理解为开启调度器的事件循环
	SetEpoll( env,ev );
}

创建协程对象:co_create_env()

大致的一个流程我大致写了以下注释,其中调用了co_create_env这个函数去创建一个协程对象,那我们来看一下这个函数的声明:

struct stCoRoutine_t *co_create_env( stCoRoutineEnv_t * env, const stCoRoutineAttr_t* attr,
		pfn_co_routine_t pfn,void *arg );

可以看到,其参数都跟co_create是一样的,我就不多介绍了,直接来看它做了什么:

struct stCoRoutine_t *co_create_env( stCoRoutineEnv_t * env, const stCoRoutineAttr_t* attr,
		pfn_co_routine_t pfn,void *arg )
{
	//定义一个协程栈对象
	stCoRoutineAttr_t at;
	if( attr )
	{
		//如果有输入参数,那么就把输入参数的结构内容拷贝至at
		memcpy( &at,attr,sizeof(at) );
	}
	//定义栈大小
	if( at.stack_size <= 0 )
	{
		at.stack_size = 128 * 1024;
	}
	else if( at.stack_size > 1024 * 1024 * 8 )
	{
		at.stack_size = 1024 * 1024 * 8;
	}

	if( at.stack_size & 0xFFF ) 
	{
		at.stack_size &= ~0xFFF;
		//加上4MB
		at.stack_size += 0x1000;
	}

	//申请一个协程对象的空间
	stCoRoutine_t *lp = (stCoRoutine_t*)malloc( sizeof(stCoRoutine_t) );
	//清空
	memset( lp,0,(long)(sizeof(stCoRoutine_t))); 

	//初始化协程对象的运行环境、执行函数、参数
	lp->env = env;
	lp->pfn = pfn;
	lp->arg = arg;

	stStackMem_t* stack_mem = NULL;
	if( at.share_stack )
	{
		//如果是共享栈
		stack_mem = co_get_stackmem( at.share_stack);
		at.stack_size = at.share_stack->stack_size;
	}
	else
	{
		//独占栈
		//申请独占栈空间
		stack_mem = co_alloc_stackmem(at.stack_size);
	}
	lp->stack_mem = stack_mem;

	//初始化上下文的栈信息
	lp->ctx.ss_sp = stack_mem->stack_buffer;
	lp->ctx.ss_size = at.stack_size;
	//初始化其他辅助信息
	lp->cStart = 0;
	lp->cEnd = 0;
	lp->cIsMain = 0;
	lp->cEnableSysHook = 0;
	lp->cIsShareStack = at.share_stack != NULL;

	lp->save_size = 0;
	lp->save_buffer = NULL;

	return lp;
}

相关代码已经添加注释了,其实其干的事情就是申请一个协程对象的空间并初始化协程对象的各种属性。

那么我们现在回到co_init_curr_thread_env函数。

初始化CPU上下文:coctx_init

将主协程压栈之前,调用了coctx_init()去初始化了主协程的CPU上下文,其定义在了coctx.cpp文件中:

int coctx_init(coctx_t* ctx) {
  memset(ctx, 0, sizeof(*ctx));
  return 0;
}

其实它就是将内存初始化为0了。

之后代码把主协程放入了协程跟踪栈之中,形成了以下的结构:

在这里插入图片描述
然后创建了一个epoll事件循环,开启此事件循环。

小结

创建协程只要包括以下内容:

  1. 初始化当前的线程环境,其又包括:
    1.1 申请环境对象stCoRoutineEnv_t的内存
    1.2 创建主协程对象并压栈
    1.3 开启事件循环
  2. 创建协程对象

协程的启动:co_resume()

在调用co_create创建协程返回后,便可以调用co_resume函数将它启动了,该函数定义在co_routine.cpp函数中:

void co_resume( stCoRoutine_t *co );
  • co:传入的协程对象,既co_create的输出参数

co_resume的参数比较简单,就是一个指向协程的指针,它负责启动它。

博主说
这里有个值得思考的地方,为什么用resume这个单词而不是start这个单词?
我们先来看一下resume这个单词的意思:
在这里插入图片描述
拥抱新技术——协程一文中,我提到了libco是非对称协程,协程在让出CPU后要恢复执行的时候,还是要co_resume去恢复启动此协程。从语义上来说,start只有一次,而resume可以有多次。

好了,我们再来看一下co_resume函数做了啥:

void co_resume( stCoRoutine_t *co )
{
	//获得当前协程的环境
	stCoRoutineEnv_t *env = co->env;
	//获取当前运行的协程
	stCoRoutine_t *lpCurrRoutine = env->pCallStack[ env->iCallStackSize - 1 ];
	if( !co->cStart )
	{
		//加载CPU上下文信息
		coctx_make( &co->ctx,(coctx_pfn_t)CoRoutineFunc,co,0 );
		co->cStart = 1;
	}
	//将当前协程压入协程跟踪栈
	env->pCallStack[ env->iCallStackSize++ ] = co;
	//进行协程的切换,挂起当前协程,开始执行co协程
	co_swap( lpCurrRoutine, co );
}

这里重点关注以下两个函数coctx_makeco_swap

加载上下文信息:coctx_make()

首先,我们注意coctx_make的第二个参数,其是一个函数指针,我们先来剖析以下这个函数的信息:

static int CoRoutineFunc( stCoRoutine_t *co,void * )
{
	//如果传入了函数就执行
	if( co->pfn )
	{
		co->pfn( co->arg );
	}
	//设置执行标记为执行完
	co->cEnd = 1;
	//获取线程运行环境
	stCoRoutineEnv_t *env = co->env;
	//出让CPU资源
	co_yield_env( env );

	return 0;
}

可以看到,这个函数实际就是执行我们传入回调函数的地方,执行完函数之后,这个协程就会出让CPU资源

之后再来看一下函数coctx_make的定义,定义在coctx.cpp文件中:

int coctx_make(coctx_t* ctx, coctx_pfn_t pfn, const void* s, const void* s1) {
  //获得栈顶地址
  char* sp = ctx->ss_sp + ctx->ss_size - sizeof(void*);
  sp = (char*)((unsigned long)sp & -16LL);
  //初始化寄存器
  memset(ctx->regs, 0, sizeof(ctx->regs));
  //定义保存返回地址,其在栈顶
  void** ret_addr = (void**)(sp);
  //保存传入的函数pfn为返回地址
  *ret_addr = (void*)pfn;
  //保存regs[13]为当前协程的栈顶,以便下次恢复
  ctx->regs[kRSP] = sp;
  //regs[9]为执行函数的地址
  ctx->regs[kRETAddr] = (char*)pfn;
  //regs[7]为当前协程对象地址
  ctx->regs[kRDI] = (char*)s;
  ctx->regs[kRSI] = (char*)s1;
  return 0;
}

大致注释已经给出,其实这里就是加载当前协程上下文信息

协程的切换:co_swap()

co_swap函数的作用就是切换协程之间的上下文和栈信息

我们来看一下这个函数做了什么,先来看一下它的声明吧,在co_routine.cpp函数中:

void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co);
  • curr:当前协程
  • pending_to:要切换入的执行协程

我们再来看一下它究竟是如何切换栈信息和上下文信息的:

void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co);
{
	//得到当前线程环境
 	stCoRoutineEnv_t* env = co_get_curr_thread_env();

	//得到当前协程的栈指针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;
		//get last occupy co on the same stack mem
		stCoRoutine_t* occupy_co = pending_co->stack_mem->occupy_co;
		//set pending co to occupy thest stack mem;
		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);
		}
	}

	//切换上下文信息
	coctx_swap(&(curr->ctx),&(pending_co->ctx) );

	//切换新协程的栈信息以及运行环境的信息
	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)
	{
		//切换栈信息
		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);
		}
	}
}

libco精华 —— 上下文信息切换:coctx_swap()

切换栈信息部分很好理解,重点是切换上下文信息那里,也就是coctx_swap函数。

博主认为,coctx_swap才是整个libco的精华所在,在其内部采用了精简的汇编代码完成了协程在用户态的切换,而不用陷入到内核态去完成协程的调度。

我们来看一下这个汇编代码:

.globl coctx_swap
#if !defined( __APPLE__ )
.type  coctx_swap, @function
#endif
coctx_swap:   //函数coctx_swap

#if defined(__i386__)
    movl 4(%esp), %eax    //sp 将第一个参数也就是当前协程信息的地址保存下来
    movl %esp,  28(%eax)
    movl %ebp, 24(%eax)
    movl %esi, 20(%eax)
    movl %edi, 16(%eax)
    movl %edx, 12(%eax)
    movl %ecx, 8(%eax)
    movl %ebx, 4(%eax)
//上面会保存当前协程的寄存器信息
//++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
//下面加载将要执行的协程的寄存器信息
    movl 8(%esp), %eax
    movl 4(%eax), %ebx
    movl 8(%eax), %ecx
    movl 12(%eax), %edx
    movl 16(%eax), %edi
    movl 20(%eax), %esi
    movl 24(%eax), %ebp
    movl 28(%eax), %esp

	ret

上面的注释已经给出,可以结合下图进行看:
在这里插入图片描述

小结

协程的切换主要做了以下事情:

  1. 获取当前下运行环境以及当前正在进行的协程current
  2. 加载将要执行的pending_to协程的执行信息,包括执行的函数以及参数等
  3. pending_to协程压栈,进入协程跟踪栈
  4. 交换pending_to 和current的栈以及上下文信息

加入说current协程是协程A,pending_to协程是协程B,那么此时协程跟踪栈就会发生入下图的变化:
在这里插入图片描述

协程的挂起:co_yield_env()

刚刚在CoRoutineFunc函数中的最后会调用co_yield_env函数,出让CPU资源,那么它是怎么实现的呢?

在非对称协程理论中,yield和resume是个相对的操作。A协程resume启动了B协程,那么之后当B协程执行yield操作时才会返回到A协程。换句话说:只有被调协程yield让出CPU,调用者协程的co_swap函数才能返回到原点,即返回到原来co_resume的位置


yield释义:
在这里插入图片描述

我们现在来看一下,co_yield_env函数做了什么吧:

void co_yield_env( stCoRoutineEnv_t *env )
{
	//获取协程跟踪栈中的的倒数第一个和第二个元素
	stCoRoutine_t *last = env->pCallStack[ env->iCallStackSize - 2 ];
	stCoRoutine_t *curr = env->pCallStack[ env->iCallStackSize - 1 ];
	//协程栈元素减一
	env->iCallStackSize--;
	//交换当前协程curr以及last的信息,将CPU权力移交到last协程
	co_swap( curr, last);
}

那么,其实很简单了,经过出让资源后,当前协程跟踪栈内的情况就变成如下情况了:
在这里插入图片描述

协程的退出

这里讲的退出,有别于协程的挂起,是指协程任务结束后发生的过程。换而言之,就是协程任务函数内部执行了return语句,结束了它的生命周期。

同协程挂起一样,协程退出时也将CPU控制权移交给它的调用者,这也是通过co_yield_env函数来完成的。

最后,当任务结束后记得要调用co_free()或者co_release()销毁这个临时性的协程,否则将引起内存泄漏。

void co_free( stCoRoutine_t *co )
{
    if (!co->cIsShareStack) 
    {    
		//独占栈
		//释放协程栈信息
        free(co->stack_mem->stack_buffer);
        free(co->stack_mem);
    }   
    else 
    {
        if(co->save_buffer)
            free(co->save_buffer);

        if(co->stack_mem->occupy_co == co)
            co->stack_mem->occupy_co = NULL;
    }
	//释放协程对象
    free( co );
}

co_release也是调用co_free,就不放代码了

参考文献

[1] C++开源协程库libco详解
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
这个错误通常是由于系统找不到所需的共享库文件导致。共享库文件是一些可被多个程序共享使用的代码和数据的集合,它们在系统中被动态链接到程序中。 当你运行一个程序时,系统会尝试加载程序所依赖的共享库文件。如果系统找不到指定的共享库文件,就会出现"error while loading shared libraries"的错误。 对于你提到的错误信息"error while loading shared libraries: libco-64.so: cannot open shared object file: No such file or directory",它表示系统无法找到名为"libco-64.so"的共享库文件。 解决这个问题的方法通常有以下几种: 1. 检查共享库文件是否存在:确认"libco-64.so"文件是否存在于系统中。可以使用命令`ls /path/to/libco-64.so`来检查文件是否存在。 2. 检查共享库文件路径:确认程序的运行环境中是否包含了正确的共享库文件路径。可以使用命令`echo $LD_LIBRARY_PATH`来查看当前的共享库文件路径。 3. 更新共享库缓存:如果确认共享库文件存在且路径正确,可以尝试更新共享库缓存。可以使用命令`sudo ldconfig`来更新共享库缓存。 4. 安装缺失的共享库文件:如果确实缺少了指定的共享库文件,可以尝试安装该共享库文件。可以使用包管理器来安装缺失的共享库文件,例如在Ubuntu上可以使用`apt-get`命令,而在CentOS上可以使用`yum`命令。 希望以上解答对你有帮助!如果还有其他问题,请随时提问。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

shenmingik

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值