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