zoukankan      html  css  js  c++  java
  • 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
    ", __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
    ", __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];
    }
    
  • 相关阅读:
    ok
    Android设备激活量超10亿 3个月增长1亿台
    JavaWeb--文件的上传和下载
    JavaWeb--Listener
    JavaWeb--JSTL
    JavaWeb--自定义标签4--EL自定义函数
    JavaWeb--自定义标签4--带父标签的自定义标签
    codeforces 437B. The Child and Set 解题报告
    codeforces 437A. The Child and Homework 解题报告
    codeforces 435 B. Pasha Maximizes 解题报告
  • 原文地址:https://www.cnblogs.com/ishen/p/14547013.html
Copyright © 2011-2022 走看看