最近接触到了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 : ®s[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 -> ®s[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()中会调用一些导致协程切换的函数。