小白学协程笔记4-tencent libco解析-2021-2-22


前言

本文对腾讯微信的协程库libco进行了简单解析,如有不当,请指正。


一、结构体定义

1.协程栈定义

libco使用的协程栈定义如下,支持独立栈和共享栈两种方式。

struct stStackMem_t
{
	stCoRoutine_t* occupy_co; // 当前正在使用该共享栈的协程
	int stack_size;   // 栈的大小
	char* stack_bp;   // stack_buffer + stack_size 栈底
	char* stack_buffer; // 栈的内容,也就是栈顶
};

/*
* 共享栈,这里的共享栈是个数组,每个元素分别是个共享栈
*/
struct stShareStack_t
{
	unsigned int alloc_idx; // 应该是目前正在使用的那个共享栈的index
	int stack_size; // 共享栈的大小,这里的大小指的是一个stStackMem_t*的大小
	int count;   // 共享栈的个数,共享栈可以为多个,所以以下为共享栈的数组
	stStackMem_t** stack_array; //栈的内容,这里是个数组,元素是stStackMem_t*
};

2.协程定义

协程结构体定义如下:

//协程
struct stCoRoutine_t
{
	stCoRoutineEnv_t *env;  // 协程所在的运行环境,可以理解为,该协程所属的协程管理器
	
	pfn_co_routine_t pfn; // 协程所对应的函数
	void *arg; // 函数参数
	coctx_t ctx; // 协程上下文,包括寄存器和栈
 
	// 以下用char表示了bool语义,节省空间
	char cStart;          // 是否已经开始运行了
	char cEnd;            // 是否已经结束
	char cIsMain;         // 是否是主协程
	char cEnableSysHook;  // 是否要打开钩子标识,默认是关闭的
	char cIsShareStack;   // 是否要采用共享栈

	void *pvEnv;

	//char sRunStack[ 1024 * 128 ];
	stStackMem_t* stack_mem; // 栈内存


	//save satck buffer while conflict on same stack_buffer;
	char* stack_sp; 
	unsigned int save_size; // save_buffer的长度
	char* save_buffer; // 当协程挂起时,栈的内容会栈暂存到save_buffer中

	stCoSpec_t aSpec[1024]; 
};

其中采用独立栈时使用stack_mem成员,采用共享栈时使用stack_sp、save_size和save_buffer成员。

3.协程上下文定义

协程上下文定义如下:

/*
* 协程上下文
*/
struct coctx_t
{
#if defined(__i386__)
	void *regs[ 8 ]; // i386架构下需要8个寄存器
#else
	void *regs[ 14 ]; // 
#endif
	size_t ss_size; // 栈空间的大小
	char *ss_sp;   // 栈空间
};

其中64位机器使用时regs存放了14个CPU寄存器地址,对应关系如下:

//-------------
// 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]: ret |  //ret func addr
//    | regs[10]: rdx |
//    | regs[11]: rcx | 
//    | regs[12]: rbx |
//hig | regs[13]: rsp |

4.协程环境定义

协程环境结构体定义如下:

/*
* 线程所管理的协程的运行环境
* 一个线程只有一个这个属性
*/
struct stCoRoutineEnv_t
{
	// 这里实际上维护的是个调用栈
	// 最后一位是当前运行的协程,前一位是当前协程的父协程(即,resume该协程的协程)
	// 可以看出来,libco只能支持128层协程的嵌套调用。这个绝对够了
	stCoRoutine_t *pCallStack[ 128 ]; 

	int iCallStackSize; // 当前调用栈长度

	stCoEpoll_t *pEpoll;  //主要是epoll,作为协程的调度器

	//for copy stack log lastco and nextco
	stCoRoutine_t* pending_co;  
	stCoRoutine_t* occupy_co;
};

stCoRoutineEnv_t其实就相当于协程的调度器,其中的pCallStack表示当前运行的协程。libco是一个非对称协程库,一个协程挂起以后,cpu控制权只能转到调用它的协程。所以pCallStack是按照调用关系存放的一组协程,其中pCallStack[0]是主协程,pCallStack[0]调用pCallStack[1],pCallStack[1]调用pCallStack[2].…以此类推。

二、API定义

1.协程调度器初始化及获取函数

协程调度器stCoRoutineEnv_t对象初始化及获取函数如下所示:

// 初始化当前线程的协程管理器
void co_init_curr_thread_env()
{
	//当前的线程的ID
	pid_t pid = GetPid();	
	
	g_arrCoEnvPerThread[ pid ] = (stCoRoutineEnv_t*)calloc( 1,sizeof(stCoRoutineEnv_t) );
	
	stCoRoutineEnv_t *env = g_arrCoEnvPerThread[ pid ];

	// 当前协程数为0
	env->iCallStackSize = 0;
    
	// 创建一个协程
	struct stCoRoutine_t *self = co_create_env( env, NULL, NULL,NULL );
	
	self->cIsMain = 1;	// 标识是一个主协程

	env->pending_co = NULL; // 初始化为 null
	env->occupy_co = NULL;  // 初始化为 null

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

	// 初始化协程管理器的时候,会把主协程放在第一个
	env->pCallStack[ env->iCallStackSize++ ] = self;

	stCoEpoll_t *ev = AllocEpoll();
	SetEpoll( env,ev );
}


// 获取当前线程的协程管理器
stCoRoutineEnv_t *co_get_curr_thread_env()
{
	return g_arrCoEnvPerThread[ GetPid() ];
}

可以看到,初始化stCoRoutineEnv_t 结构体时也会初始化主协程,并将主协程放在pCallStack的第一个位置。

2.协程创建函数

协程创建函数定义如下:

/**
* 根据协程管理器env, 新建一个协程
* 
* @param env - (input) 协程所在线程的环境
* @param attr - (input) 协程属性,目前主要是共享栈 
* @param pfn - (input) 协程所运行的函数
* @param arg - (input) 协程运行函数的参数
*/
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 )
	{
		memcpy( &at,attr,sizeof(at) );
	}


	if( at.stack_size <= 0 )
	{
		at.stack_size = 128 * 1024; // 默认的为128k
	}
	else if( at.stack_size > 1024 * 1024 * 8 )
	{
		at.stack_size = 1024 * 1024 * 8;
	}

	/* 地址对齐 */
	if( at.stack_size & 0xFFF ) 
	{
		at.stack_size &= ~0xFFF;
		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;

	// 设置该协程的context
	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;	// 默认不开启hook
	lp->cIsShareStack = at.share_stack != NULL;

	// 仅在共享栈的时候有意义
	lp->save_size = 0;
	lp->save_buffer = NULL;

	return lp;
}
/**
* 创建一个协程对象
* 
* @param ppco - (output) 协程的地址,未初始化,需要在此函数中将其申请内存空间以及初始化工作
* @param attr - (input) 协程属性,目前主要是共享栈 
* @param pfn - (input) 协程所运行的函数
* @param arg - (input) 协程运行函数的参数
*/
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 = co;
	return 0;
}

3.协程启动/恢复函数

协程启动/恢复函数定义如下:

/*
* 语义:继续运行协程
* 实际上:
* @param co - (input) 要切换的协程
*/
void co_resume( stCoRoutine_t *co )
{
	stCoRoutineEnv_t *env = co->env;

	// 找到当前运行的协程, 从数组最后一位拿出当前运行的协程,如果目前没有协程,那就是主线程
	stCoRoutine_t *lpCurrRoutine = env->pCallStack[ env->iCallStackSize - 1 ];

	if( !co->cStart )
	{
		// 如果当前协程还没有开始运行,为其构建上下文
		coctx_make( &co->ctx,(coctx_pfn_t)CoRoutineFunc,co, 0 );
		co->cStart = 1;
	}

	// 将指定协程放入线程的协程队列末尾
	env->pCallStack[ env->iCallStackSize++ ] = co;
	
	// 将当前运行的上下文保存到lpCurrRoutine中,同时将协程co的上下文替换进去
	// 执行完这一句,当前的运行环境就被替换为 co 了
	co_swap( lpCurrRoutine, co );
}

4.协程挂起函数

协程挂起函数定义如下:

/*
*
* 主动将当前运行的协程挂起,并恢复到上一层的协程
*
* @param env 协程管理器 
*/
void co_yield_env( stCoRoutineEnv_t *env )
{
	// 这里直接取了iCallStackSize - 2,那么万一icallstacksize < 2呢?
	// 所以这里实际上有个约束,就是co_yield之前必须先co_resume, 这样就不会造成这个问题了

	// last就是 找到上次调用co_resume(curr)的协程
	stCoRoutine_t *last = env->pCallStack[ env->iCallStackSize - 2 ];

	// 当前栈
	stCoRoutine_t *curr = env->pCallStack[ env->iCallStackSize - 1 ];

	env->iCallStackSize--;

	// 把上下文当前的存储到curr中,并切换成last的上下文
	co_swap( curr, last);
}
void co_yield( stCoRoutine_t *co )
{
	co_yield_env( co->env );
}

5.协程切换函数

在co_yield_env和co_resume函数中都定义了co_swap函数,完成协程切换,定义如下:

/**
* 将原本占用共享栈的协程的内存保存起来。
* @param occupy_co 原本占用共享栈的协程
*/
void save_stack_buffer(stCoRoutine_t* occupy_co)
{
	///copy out
	stStackMem_t* stack_mem = occupy_co->stack_mem;
	// 计算出栈的大小
	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;

	// 将当前运行栈的内容,拷贝到save_buffer中
	memcpy(occupy_co->save_buffer, occupy_co->stack_sp, len);
}

/*
* 1. 将当前的运行上下文保存到curr中
* 2. 将当前的运行上下文替换为pending_co中的上下文

* @param curr
* @param pending_co 
*/
void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co)
{
 	stCoRoutineEnv_t* env = co_get_curr_thread_env();

	//get curr stack sp
	//这里非常重要!!!: 这个c变量的实现,作用是为了找到目前的栈底,因为c变量是最后一个放入栈中的内容。
	char c;
	curr->stack_sp= &c;

	if (!pending_co->cIsShareStack)
	{  
		// 如果没有采用共享栈,清空pending_co和occupy_co
		env->pending_co = NULL;
		env->occupy_co = NULL;
	}
	else 
	{   
		// 如果采用了共享栈
		env->pending_co = pending_co; 
		
		//get last occupy co on the same stack mem
		// occupy_co指的是,和pending_co共同使用一个共享栈的协程
		// 把它取出来是为了先把occupy_co的内存保存起来
		stCoRoutine_t* occupy_co = pending_co->stack_mem->occupy_co;
		
		//set pending co to occupy thest stack mem;
		// 将该共享栈的占用者改为pending_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);
		}
	}

	// swap context
	coctx_swap(&(curr->ctx),&(pending_co->ctx) );

	// 这个地方很绕,上一步coctx_swap会进入到pending_co的协程环境中运行
	// 到这一步,已经yield回此协程了,才会执行下面的语句
	// 而yield回此协程之前,env->pending_co会被上一层协程设置为此协程
	// 因此可以顺利执行: 将之前保存起来的栈内容,恢复到运行栈上

	//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);
		}
	}
}

在co_swap函数中,如果采用的是共享栈,则先采用save_stack_buffer保存共享栈数据到自己的栈空间。关于共享栈和非共享栈的介绍可以看:小白学协程笔记3-实现自己的协程库-2021-2-22。然后采用coctx_swap函数完成上下文切换,coctx_swap函数是通过汇编语言实现的,如何进行切换已经在这篇文章中介绍:小白学协程笔记2-c语言实现协程-2021-2-10coctx_swap会切换到指定协程运行。而当指定协程挂起时,会返回到coctx_swap的下一行代码运行。返回之后,若采用的是共享栈,则需要将数据从自己的栈,拷贝回共享栈中。


总结

本文对libco的代码进行了简单剖析,libco源码地址

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值