libco源码分析

最近接触到了libco,这是tencent开源的一个协程库,可以很方便的在c++中使用协程。而且封装了systemcall和epoll。比较好奇上下文切换相关的内容,就去看了看。库设计的还是很精巧的,相对于其它的c++的协程库,这个库同时考虑了协程的内存消耗和协程切换的平衡(添加了一个数量可调的共享的栈列表,部分情况拷贝栈,部分情况不用拷贝(刚好和上一个一样的协程的时候))

给源码fork了一份,给几个地方加了点注释:https://github.com/zhjzhjxzhl/libco 

主要数据结构:

1、coctx_t  协程上下文,包括通用寄存器,和使用的栈的内容存储

2、stStackMem 协程运行的栈,主协程是系统栈,其它协程都是在这个栈列表里运行的,是一个取模来决定用哪个栈的,多个协程可以对应一个栈,这样主要是为了节省内存,开更多协程,还有就是协程的上下文栈复制,也就是memcopy的一个开销。而且都是在切换的时候,才决定要不要拷贝的,也可能出现还是原来协程的情况。

3、stTimeOut    封装中对于read,write等函数,做了一个timeout的支持,这个由主协程来负责时间管理,如果过期或者收到读写事件,都会返回到协程去执行的。

协程的切换,都放到了coctx_swap.S文件里,这里主要是利用了x86函数调用栈时返回地址处于栈顶,且ret指令,会去执行当前栈顶的地址所指向的指令,这个特性来做的切换。具体的注释如下(只注释了x86 32位的)

.globl coctx_swap
  #if !defined( __APPLE__ ) && !defined( __FreeBSD__ )
  .type coctx_swap, @function
  #endif
  coctx_swap:
   
  #if defined(__i386__)
  //此时上一个函数的压栈已经完成了,当前栈结构是 sp->返回地址 sp+4->参数1的地址,也就是当前协程 sp+8参数2的地址,也就是目标协程
  //因为x86的push esp,mov ebp=esp是加在下一个函数里的,所以此处还没有执行。
  leal 4(%esp), %eax //参数一,也就是当前协程地址放到eax
  movl 4(%esp), %esp // 参数一的地址放进esp,因为这个结构,刚好是要切换出去的协程的的寄存器缓存地址
  leal 32(%esp), %esp //parm a : &regs[7] + sizeof(void*) //给寄存器的地址+8,因为栈只能从高往低操作,而堆是从低往高的,所以挪到末尾,因为push操作会减sp的值
   
  pushl %eax //esp ->parm a //参数一地址压栈,主要是为了保存住sp+4的地址
   
  pushl %ebp
  pushl %esi
  pushl %edi
  pushl %edx
  pushl %ecx
  pushl %ebx
  pushl -4(%eax) //缓存的返回地址压栈
   
 
  movl 4(%eax), %esp //parm b -> &regs[0] //相当于sp+8,就是目标协程的地址放到eax
   
  popl %eax //ret func addr //最后压入的是返回地址
  popl %ebx
  popl %ecx
  popl %edx
  popl %edi
  popl %esi
  popl %ebp
  popl %esp //前一个栈的参数1地址,也就是sp+的地址,下面在push %eax,那么sp刚好回到原位,而且返回地址刚好处于当前的栈顶。x86 ret函数之前,会插入sp=bp,bp=pop
  pushl %eax //set ret func addr 下面在push %eax,那么sp刚好回到原位,而且返回地址刚好处于当前的栈顶。x86 ret函数之前,会插入sp=bp,bp=pop
  //反汇编的的时候,这里看到的是leaveq,就是这个意思
   
  xorl %eax, %eax
  ret //现在栈顶是返回地址,所有的寄存器都恢复了,sp也指向了共享栈的位置,ret操作,直接执行栈顶指向的代码,就ok了。

协程初始化的部分,在coctx.cpp 的 coctx_make函数中,在代码执行先,先给esp和eip赋值好,

#if defined(__i386__)
  int coctx_init( coctx_t *ctx )
  {
  memset( ctx,0,sizeof(*ctx));
  return 0;
  }
  int coctx_make( coctx_t *ctx,coctx_pfn_t pfn,const void *s,const void *s1 )
  {
  //make room for coctx_param
  char *sp = ctx->ss_sp + ctx->ss_size - sizeof(coctx_param_t); //将sp对到栈顶,然后留出参数的位置,8个字节
  sp = (char*)((unsigned long)sp & -16L); //这一步是处理内存对齐的问题,猜测
   
   
  coctx_param_t* param = (coctx_param_t*)sp ; //将参数放到栈顶,之后函数调用方便
  param->s1 = s;
  param->s2 = s1;
   
  memset(ctx->regs, 0, sizeof(ctx->regs));
   
  ctx->regs[ kESP ] = (char*)(sp) - sizeof(void*); //sp往下移动,为返回地址预留空间。
  ctx->regs[ kEIP ] = (char*)pfn; //对应的下一条指令的位置,也就是函数指针的位置
   
  return 0;
  }

那么在栈运行的时候,就可以切换到自己定义的栈上来,而eip指向的是 co_rountine.cpp中的CoRoutingFunc中,而其中的co->pfn()中会调用一些导致协程切换的函数。

猜你喜欢

转载自blog.csdn.net/zhjzhjxzhl/article/details/79792497
今日推荐