libco分析

创建和运行分析

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <queue>
#include "co_routine.h"
using namespace std;

struct stTask_t
{
	int id;
};
struct stEnv_t
{
	stCoCond_t* cond;
	queue<stTask_t*> task_queue;
};
void* Producer(void* args)
{
	co_enable_hook_sys();
	stEnv_t* env=  (stEnv_t*)args;
	int id = 0;
	while (true)
	{
		stTask_t* task = (stTask_t*)calloc(1, sizeof(stTask_t));
		task->id = id++;
		env->task_queue.push(task);
		printf("%s:%d produce task %d\n", __func__, __LINE__, task->id);
		co_cond_signal(env->cond);
		poll(NULL, 0, 1000);//协程切换
	}
	return NULL;
}
void* Consumer(void* args)
{
	co_enable_hook_sys();
	stEnv_t* env = (stEnv_t*)args;
	while (true)
	{
		if (env->task_queue.empty())
		{
			co_cond_timedwait(env->cond, -1);
			continue;
		}
		stTask_t* task = env->task_queue.front();
		env->task_queue.pop();
		printf("%s:%d consume task %d\n", __func__, __LINE__, task->id);
		free(task);
	}
	return NULL;
}
int main()
{
	stEnv_t* env = new stEnv_t;
	env->cond = co_cond_alloc();

	stCoRoutine_t* consumer_routine;
	co_create(&consumer_routine, NULL, Consumer, env);
	co_resume(consumer_routine);

	stCoRoutine_t* producer_routine;
	co_create(&producer_routine, NULL, Producer, env);//创建一个协程
	co_resume(producer_routine);//切换运行该协程
	
	co_eventloop(co_get_epoll_ct(), NULL, NULL);
	return 0;
}

以上是libco的协程创建和使用,分析producer协程的创建和运行就能对libco大概了解

三个重要的结构体

stCoRoutineEnv_t: 协程管理器,协助管理协程的切换运行

stCoRoutine_t: 协程控制块,一个协程实体,存储了协程运行context,运行函数,参数,堆栈等内容

coctx_t:协程运行context,用于保存运行的寄存器,函数栈上下文

协程的创建过程

协程的创建过程主要是初始化stCoRoutineEnv_t:stCoRoutine_t: ,创建的流程如下图所示, 完整图跳转

首先调用1.co_create()创建协程

stCoRoutine_t* producer_routine;
co_create(&producer_routine, NULL, Producer, env);

该函数内会判断当前线程是否已经创建协程管理器,如果没有调用1.1 co_init_current_thread_env创建,然后调用1.2 co_create_env()进行创建协程控制块

int co_create( stCoRoutine_t **ppco,const stCoRoutineAttr_t *attr,pfn_co_routine_t pfn,void *arg )
{
	if( !co_get_curr_thread_env() ) 
	{	//1.1 该线程的协程管理器未创建,创建
		co_init_curr_thread_env();
	}
	//1.2 创建协程控制块
	stCoRoutine_t *co = co_create_env( co_get_curr_thread_env(), attr, pfn,arg );
	*ppco = co;
	return 0;
}

1.1 co_init_current_thread_env中主要做了三件事,1.1.1中使用alloc创建协程管理器env用于管理当前线程下的所有协程,1.1.2创建了一个协程控制块,这里当前看成主协程,为其创建一个控制块, 1.1.3把主协程放在协程调用栈底,libco是非对称协程模型,允许协程下创建子协程,子协程运行完毕之后退栈后父协程会继续运行,就是会用到协程调用栈去管理

void co_init_curr_thread_env()
{
	// 1.1.1 alloc 协程管理器
	gCoEnvPerThread = (stCoRoutineEnv_t*)calloc( 1, sizeof(stCoRoutineEnv_t) );
	stCoRoutineEnv_t *env = gCoEnvPerThread;

	env->iCallStackSize = 0;
	
	// 1.1.2 创建主线程控制块
	struct stCoRoutine_t *self = co_create_env( env, NULL, NULL,NULL );
	self->cIsMain = 1;

	env->pending_co = NULL;
	env->occupy_co = NULL;

	coctx_init( &self->ctx );
	
	// 初始化当前线程下协程的调用链
	env->pCallStack[ env->iCallStackSize++ ] = self;

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

·1.2 co_create_env()中创建了协程实体,在1.2.1创建协程实体后,把运行函数和参数保存操作,然后进行1.2.2分配堆内存给该协程作为其函数运行栈:堆内存的大小由创建协程时传入的参数attr进行指定计算,一种是共享栈, 另一种是非共享,因为libco中默认每个协程使用128kB堆,1w协程数就占用了1.28G,然而很多时候每个协程运行时用不到这么多,所以让协程可以共享一块栈,在协程运行被切换掉的时候,将其所用到的栈部分保存出去;最后是1.2.3对协程的基本属性做的一些初始化

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

	//1.2.1 创建协程实体
	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;

	//1.2.2 把堆内存分配给该协程作为其运行栈
	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;

	//1.2.3 设置一些额外的管理属性
	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_resume()进行,主要流程如下图:

协程的切换使用2.co_resume()进行

stCoRoutine_t* producer_routine;
co_create(&producer_routine, NULL, Producer, env);
co_resume(producer_routine);

libco协程的切换使用ret指令更改了指令寄存器%eip的值实现跳转到协程函数入口,如下图是一个完整的函数调用过程中的栈帧变换,在第6步函数调用完毕后退栈时会使用ret指令将保存的ret_addr给保存到%eip中,回到上一个函数执行,如果将这个ret_addr构造好,那么ret指令就可以返回跳转到指定的地方(也是一种缓冲区溢出攻击的手段),libco就是用这种方式做协程切换。

函数调用栈帧图

2. co_resume()中,其首先使用2.1 coctx_make()构造了一个切换堆栈

void co_resume( stCoRoutine_t *co )
{
	stCoRoutineEnv_t *env = co->env;
	stCoRoutine_t *lpCurrRoutine = env->pCallStack[ env->iCallStackSize - 1 ];
	if( !co->cStart )
	{
		// 2.1 构造一个切换堆栈
		coctx_make( &co->ctx,(coctx_pfn_t)CoRoutineFunc,co,0 );
		co->cStart = 1;
	}
	env->pCallStack[ env->iCallStackSize++ ] = co;
	co_swap( lpCurrRoutine, co );


}

2.1 coctx_make()的内容如下所示,把分配给该协程的堆内存的高位当成栈底,然后将协程运行函数CoRoutineFunc()设置成ret_addr,设置好该函数的调用参数,这个栈帧就构造完成了

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(coctx_param_t);
  sp = (char*)((unsigned long)sp & -16L);


  // 设置调用参数
  coctx_param_t* param = (coctx_param_t*)sp;
  param->s1 = s;
  param->s2 = s1;
  
  //设置ret_addr, 将调用函数设置为CoRoutineFunc
  void** ret_addr = (void**)(sp - sizeof(void*) * 2);
  *ret_addr = (void*)pfn;

  memset(ctx->regs, 0, sizeof(ctx->regs));

  // 保存构造好的栈顶
  ctx->regs[kESP] = (char*)(sp) - sizeof(void*) * 2;
  return 0;
}

最终所得到的构造栈帧如下图,sizeof(void*) * 2 导致多了一个空白位,后续说它的作用

            =====堆顶/栈底====
            |pading| 			内存对齐填充区
            |s2|				参数s2的值
            |s1|				参数s1的值
            |  |				空白位
esp->       |CoRoutineFunc|		ret_addr 

如果我们将esp指向图中的位置,那么只要在此处运行ret那么就会将eip指向CoRoutineFunc()就可以运行该函数;这一步是在2.2 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;
	
	// 2.2.1 如果使用了协程共享栈,保存被切换出的协程已使用的共享栈
	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);
		}
	}

	// 2.2.2 切换协程
	coctx_swap(&(curr->ctx),&(pending_co->ctx) );

	// 2.2.3 协程运行完毕,恢复旧协程的堆栈
	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);
		}
	}
}

2.2.1 中如果使用了协程共享栈就保存被切换出的协程已使用的共享栈,然后最重要的是2.2.2 coctx_swap()其做做了协程切换,跑去运行协程,执行完后再执行2.2.3(2.2.3的执行在后面再讨论); 2.2.2 coctx_swap()中做的事情很简单, 将当前运行的堆栈保存起来,然后将栈顶寄存器%esp指向2.1 coctx_make()构造好的栈帧中

coctx_swap:
#if defined(__i386__)
	// 保存当前协程的寄存器
    movl 4(%esp), %eax
    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  // ! 更改esp寄存器指向构造好的栈帧中

    // 切换到新的协程运行函数运行
    ret

此时esp指向了CoRoutineFunc(), 所以 ret之后就跳转到了CoRoutineFunc() 同时esp会上移到空白位,这个空白位的作用参考上面的函数调用栈帧图的过程2和3,它相当于一个的假的ret_addr位,起填充作用

            =====堆顶/栈底====
            |pading|            内存对齐填充区
            |s2|                参数s2的值
            |s1|                参数s1的值
esp->       |  |                空白位

eip寄存器: CoRoutineFunc()
参数1: s1

然后运行CoRoutineFunc(),其汇编代码会做push ebp等操作,esp下移,当需要用到第一个参数s1的时候,编译好的汇编中使用的是esp + 8去获取第一个参数,这就是空白位的作用。

			=====堆顶/栈底====
            |pading|            内存对齐填充区
            |s2|                参数s2的值
esp+8       |s1|                参数s1的值
            |  |                空白位
esp->       |ebp|
			

eip寄存器: CoRoutineFunc()
参数1: s1

**2.2.2.1 CoRoutineFunc() **中获取协程,执行协程函数,

static int CoRoutineFunc( stCoRoutine_t *co,void * )
{
	if( co->pfn )
	{
		co->pfn( co->arg ); //执行协程函数
	}
	co->cEnd = 1;

	stCoRoutineEnv_t *env = co->env;

	co_yield_env( env ); // 切换

	return 0;
}

协程函数执行完毕之后会使用co_yield_env(),切换回上一个协程的运行2.2.3 协程运行完毕,恢复旧协程的堆栈

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--;

	co_swap( curr, last);
}

这个切换回去的过程虽然也使用的co_swap(),但是在coctx_swap(),对esp进行更改的值是我们之前调用coctx_swap()保存下的esp,所以ret后会回到2.2.3

共享栈

libco中可以提供函数共享栈,使用的方式如下:

	//创建内存块
	stShareStack_t* share_stack= co_alloc_sharestack(1, 1024 * 128);
	stCoRoutineAttr_t attr;
	attr.stack_size = 0;
	attr.share_stack = share_stack;

	stCoRoutine_t* co[2];
	int routineid[2];
	for (int i = 0; i < 2; i++)
	{
		routineid[i] = i;
		//使用内存块创建线程
		co_create(&co[i], &attr, RoutineFunc, routineid + i);
		co_resume(co[i]);
	}
	//运行
	co_eventloop(co_get_epoll_ct(), NULL, NULL);

co_alloc_sharestack(int count, int stack_size)创建了一块内存池stShareStack_t* share_stack,其结构如下图所示:

其中stShareStack_t->alloc_idx这个值在后续创建协程初始化给其对应的堆栈会使用到,使用它对内存池进行轮转分配

static stStackMem_t* co_get_stackmem(stShareStack_t* share_stack)
{
	if (!share_stack)
	{
		return NULL;
	}
	//RoundRobin allocate
	int idx = share_stack->alloc_idx % share_stack->count;
	share_stack->alloc_idx++;

	return share_stack->stack_array[idx];
}
posted @ 2021-03-17 13:09  woder  阅读(698)  评论(0编辑  收藏  举报