协程实现的基础与方式
2015-09-29 11:08
417 查看
协程实现的基础与方式
协程实现的基础
1. 寄存器%ebp和%esp
一直对寄存器ESP和EBP的概念总是有些混淆,查看定义ESP是栈顶指针,EBP是存取堆栈指针。还是不能很透彻理解。之后借于一段汇编代码,总算是对两者有个比较清晰的理解。
下面是按调用约定__stdcall 调用函数test(int p1,int p2)的汇编代码
假设执行函数前堆栈指针ESP为NN
push p2 ;参数2入栈, ESP -= 4h , ESP = NN - 4h
push p1 ;参数1入栈, ESP -= 4h , ESP = NN - 8h
call test ;压入返回地址 ESP -= 4h, ESP = NN - 0Ch
;//进入函数内
{
push ebp ;保护先前EBP指针, EBP入栈, ESP-=4h, ESP = NN - 10h
mov ebp, esp ;设置EBP指针指向栈顶 NN-10h
mov eax, dword ptr [ebp+0ch] ;ebp+0ch为NN-4h,即参数2的位置
mov ebx, dword ptr [ebp+08h] ;ebp+08h为NN-8h,即参数1的位置
sub esp, 8 ;局部变量所占空间ESP-=8, ESP = NN-18h
...
add esp, 8 ;释放局部变量, ESP+=8, ESP = NN-10h
pop ebp ;出栈,恢复EBP, ESP+=4, ESP = NN-0Ch
ret 8 ;ret返回,弹出返回地址,ESP+=4, ESP=NN-08h, 后面加操作数8为平衡堆栈,ESP+=8,ESP=NN, 恢复进入函数前的堆栈.
}
原来ESP就是一直指向栈顶的指针,而EBP只是存取某时刻的栈顶指针,以方便对栈的操作,如获取函数参数、局部变量等。
2. 协程实现的基础
协程可以认为是一种用户态的线程,与系统提供的线程不同点是,它需要主动让出CPU时间,而不是由系统进行调度,即控制权在程序员手上。
既然看成是用户态线程,那必然要求程序员自己进行各个协程的调度,这样就必须提供一种机制供编写协程的人将当前协程挂起,即保存协程运行场景的一些数据,调度器在其他协程挂起时再将此协程运行场景的数据恢复,以便继续运行。这里我们将协程运行场景的数据称为上下文。
在linux里,有getcontext和swapcontext等接口来获取当前的上下文数据和切换上下文。那如果没有提供相应的接口,又该如何来实现呢?
其实说到底,保存下上文数据,不外乎就是保存下当前运行的栈空间的数据,还有cpu各个寄存器相应的值。只要我们能够将其保存下来,在特定的时刻恢复回去就可以了。
有人用c提供的接口setjmp和longjmp来实现协程的切换和恢复,但这里要介绍另外一种方式,即用汇编来保存/恢复cpu寄存器的值。
用汇编的方式依赖于特定的平台,这里举例的是i386 32位的*nix平台。
在开始贴代码前,要先说一个概念–栈帧。
ia32程序用程序栈来支持过程调用。机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。为单个过程分配的那部分栈称为栈帧。栈帧的最顶端以两个指针界定,寄存器%ebp为帧指针,而寄存器%esp为栈指针。当程序执行时,栈指针可以移动,因此大多数信息的访问都是相对于帧指针的。
这里我们可以看到,在调用一个函数前,都会先将各个参数、调用者在被调用函数返回时执行的下一条指令的地址–返回地址压栈,被调用函数在开始前会将%ebp的值保存,然后将当前%esp的值赋予%ebp。弄明白帧指针和栈指针的作用,以及返回地址等如何通过%ebp来获取的话,对下面保存当前上下文的汇编代码理解比较有帮助。
ucontext结构体主要关心的为uc_mcontext和uc_stack这两个成员,其中uc_stack指向一段内存,这段内存做为协程的运行栈;而uc_context为mcontext类型,各个成员保存着CPU同名的寄存器值。
intgetmcontext(mcontext_t*);/*保存当前上下文的声明*/
上述分别是保存上下文的C接口声明和汇编实现。根据第4行汇编代码可以看出,GET函数所需要的参数值被保存到%eax,之所以根据4(%esp)来寻址,是因为这时候栈指针指向的是保存返回地址的内存地址。接着将各个寄存器的值保存到参数值指向的mcontext结构体,结合下struct
mcontext以及代码里的移位看就可以了,这里就不多说了。唯一比较难理解的可能就是%eip寄存器值的获取了。由于这时候要保存的是调用GET函数的过程的上下文,这时候%eip寄存器保存的并不是调用GET函数过程的下一条指令的值,GET函数栈帧的返回地址才是调用GET函数过程返回后应该往下执行的下一条指令,因此可以看到上面汇编代码18行是将栈指针指向内存保存的值做为%eip的值保存起来。
至于恢复上下文的SET函数,要说的就是它是如何来改变%eip寄存器的值。根据上面第17行的汇编代码,它只是将新的%eip的值压栈而已,并不是直接赋予ip寄存器。我们这里再看一下当执行到ret后会怎么样。ret可以等效于这句指令–pop %eip。当SET函数返回后即将刚刚压栈的新的%eip的值恢复到ip寄存器当中去了。
使用汇编实现的GET和SET函数,实际上就可以进行上下文的保存和恢复了。但是要实现协程这还不够,协程跟线程一样,都是提供一个函数做为入口,那我们还需要为协程构建好调用其函数入口的准备,即参数压栈,栈指针的指向,还有返回地址的保存等。
第6到第9行实现了用户指定参数的入栈,第11行将返回地址指定为0.实际上linux实现的makecontext接口会根据ucontext结构体uc_link指向的值来进行设定,可以让其返回到另外一个协程继续执行。
12、13行分别设定了ip寄存器和栈指针的值,这就指定了协程开始运行的指令地址和所使用的栈空间。
makecontext函数的调用往往会伴随着SET函数的调用,由于makecontext已经指定好用户传进来的函数入口地址和栈空间的起始地址了,而SET函数返回后就会开始执行用户指定的函数了,协程开始了。
注:上述引用代码均来自于开源项目libtask。
协程实现的方式
c/c++不直接支持协程语义,但有不少开源的协程库,如:
Protothreads:一个“蝇量级” C 语言协程库
libco:来自腾讯的开源协程库libco介绍,官网
coroutine:云风的一个C语言同步协程库,详细信息
目前看到大概有四种实现协程的方式:
第一种:利用glibc 的 ucontext组件(云风的库)
第二种:使用汇编代码来切换上下文(实现c协程)
第三种:利用C语言语法switch-case的奇淫技巧来实现(Protothreads)
第四种:利用了 C 语言的 setjmp 和 longjmp( 一种协程的 C/C++ 实现,要求函数里面使用 static local 的变量来保存协程内部的数据
协程实现的基础
1. 寄存器%ebp和%esp
一直对寄存器ESP和EBP的概念总是有些混淆,查看定义ESP是栈顶指针,EBP是存取堆栈指针。还是不能很透彻理解。之后借于一段汇编代码,总算是对两者有个比较清晰的理解。
下面是按调用约定__stdcall 调用函数test(int p1,int p2)的汇编代码
假设执行函数前堆栈指针ESP为NN
push p2 ;参数2入栈, ESP -= 4h , ESP = NN - 4h
push p1 ;参数1入栈, ESP -= 4h , ESP = NN - 8h
call test ;压入返回地址 ESP -= 4h, ESP = NN - 0Ch
;//进入函数内
{
push ebp ;保护先前EBP指针, EBP入栈, ESP-=4h, ESP = NN - 10h
mov ebp, esp ;设置EBP指针指向栈顶 NN-10h
mov eax, dword ptr [ebp+0ch] ;ebp+0ch为NN-4h,即参数2的位置
mov ebx, dword ptr [ebp+08h] ;ebp+08h为NN-8h,即参数1的位置
sub esp, 8 ;局部变量所占空间ESP-=8, ESP = NN-18h
...
add esp, 8 ;释放局部变量, ESP+=8, ESP = NN-10h
pop ebp ;出栈,恢复EBP, ESP+=4, ESP = NN-0Ch
ret 8 ;ret返回,弹出返回地址,ESP+=4, ESP=NN-08h, 后面加操作数8为平衡堆栈,ESP+=8,ESP=NN, 恢复进入函数前的堆栈.
}
原来ESP就是一直指向栈顶的指针,而EBP只是存取某时刻的栈顶指针,以方便对栈的操作,如获取函数参数、局部变量等。
2. 协程实现的基础
协程可以认为是一种用户态的线程,与系统提供的线程不同点是,它需要主动让出CPU时间,而不是由系统进行调度,即控制权在程序员手上。
既然看成是用户态线程,那必然要求程序员自己进行各个协程的调度,这样就必须提供一种机制供编写协程的人将当前协程挂起,即保存协程运行场景的一些数据,调度器在其他协程挂起时再将此协程运行场景的数据恢复,以便继续运行。这里我们将协程运行场景的数据称为上下文。
在linux里,有getcontext和swapcontext等接口来获取当前的上下文数据和切换上下文。那如果没有提供相应的接口,又该如何来实现呢?
其实说到底,保存下上文数据,不外乎就是保存下当前运行的栈空间的数据,还有cpu各个寄存器相应的值。只要我们能够将其保存下来,在特定的时刻恢复回去就可以了。
有人用c提供的接口setjmp和longjmp来实现协程的切换和恢复,但这里要介绍另外一种方式,即用汇编来保存/恢复cpu寄存器的值。
用汇编的方式依赖于特定的平台,这里举例的是i386 32位的*nix平台。
在开始贴代码前,要先说一个概念–栈帧。
ia32程序用程序栈来支持过程调用。机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。为单个过程分配的那部分栈称为栈帧。栈帧的最顶端以两个指针界定,寄存器%ebp为帧指针,而寄存器%esp为栈指针。当程序执行时,栈指针可以移动,因此大多数信息的访问都是相对于帧指针的。
这里我们可以看到,在调用一个函数前,都会先将各个参数、调用者在被调用函数返回时执行的下一条指令的地址–返回地址压栈,被调用函数在开始前会将%ebp的值保存,然后将当前%esp的值赋予%ebp。弄明白帧指针和栈指针的作用,以及返回地址等如何通过%ebp来获取的话,对下面保存当前上下文的汇编代码理解比较有帮助。
struct mcontext { /* * The first 20 fields must match the definition of * sigcontext. So that we can support sigcontext * and ucontext_t at the same time. */ int mc_onstack; /* XXX - sigcontext compat. */ int mc_gs; int mc_fs; int mc_es; int mc_ds; int mc_edi; int mc_esi; int mc_ebp; int mc_isp; int mc_ebx; int mc_edx; int mc_ecx; int mc_eax; int mc_trapno; int mc_err; int mc_eip; int mc_cs; int mc_eflags; int mc_esp; /* machine state */ int mc_ss; int mc_fpregs[28]; /* env87 + fpacc87 + u_long */ int __spare__[17]; }; struct ucontext { /* * Keep the order of the first two fields. Also, * keep them the first two fields in the structure. * This way we can have a union with struct * sigcontext and ucontext_t. This allows us to * support them both at the same time. * note: the union is not defined, though. */ sigset_t uc_sigmask; mcontext_t uc_mcontext; struct __ucontext *uc_link; stack_t uc_stack; int __spare__[8]; };
ucontext结构体主要关心的为uc_mcontext和uc_stack这两个成员,其中uc_stack指向一段内存,这段内存做为协程的运行栈;而uc_context为mcontext类型,各个成员保存着CPU同名的寄存器值。
intgetmcontext(mcontext_t*);/*保存当前上下文的声明*/
/*保存当前上下文的汇编实现*/ .globl GET GET: movl 4(%esp), %eax movl %fs, 8(%eax) movl %es, 12(%eax) movl %ds, 16(%eax) movl %ss, 76(%eax) movl %edi, 20(%eax) movl %esi, 24(%eax) movl %ebp, 28(%eax) movl %ebx, 36(%eax) movl %edx, 40(%eax) movl %ecx, 44(%eax) movl $1, 48(%eax) /* %eax */ movl (%esp), %ecx /* %eip */ movl %ecx, 60(%eax) leal 4(%esp), %ecx /* %esp */ movl %ecx, 72(%eax) movl 44(%eax), %ecx /* restore %ecx */ movl $0, %eax ret
上述分别是保存上下文的C接口声明和汇编实现。根据第4行汇编代码可以看出,GET函数所需要的参数值被保存到%eax,之所以根据4(%esp)来寻址,是因为这时候栈指针指向的是保存返回地址的内存地址。接着将各个寄存器的值保存到参数值指向的mcontext结构体,结合下struct
mcontext以及代码里的移位看就可以了,这里就不多说了。唯一比较难理解的可能就是%eip寄存器值的获取了。由于这时候要保存的是调用GET函数的过程的上下文,这时候%eip寄存器保存的并不是调用GET函数过程的下一条指令的值,GET函数栈帧的返回地址才是调用GET函数过程返回后应该往下执行的下一条指令,因此可以看到上面汇编代码18行是将栈指针指向内存保存的值做为%eip的值保存起来。
.globl SET SET: movl 4(%esp), %eax movl 8(%eax), %fs movl 12(%eax), %es movl 16(%eax), %ds movl 76(%eax), %ss movl 20(%eax), %edi movl 24(%eax), %esi movl 28(%eax), %ebp movl 36(%eax), %ebx movl 40(%eax), %edx movl 44(%eax), %ecx movl 72(%eax), %esp pushl 60(%eax) /* new %eip */ movl 48(%eax), %eax ret
至于恢复上下文的SET函数,要说的就是它是如何来改变%eip寄存器的值。根据上面第17行的汇编代码,它只是将新的%eip的值压栈而已,并不是直接赋予ip寄存器。我们这里再看一下当执行到ret后会怎么样。ret可以等效于这句指令–pop %eip。当SET函数返回后即将刚刚压栈的新的%eip的值恢复到ip寄存器当中去了。
使用汇编实现的GET和SET函数,实际上就可以进行上下文的保存和恢复了。但是要实现协程这还不够,协程跟线程一样,都是提供一个函数做为入口,那我们还需要为协程构建好调用其函数入口的准备,即参数压栈,栈指针的指向,还有返回地址的保存等。
void makecontext(ucontext_t *ucp, void (*func)(void), int argc, ...) { int *sp; sp = (int*)ucp->uc_stack.ss_sp+ucp->uc_stack.ss_size/4; sp -= argc; sp = (void*)((uintptr_t)sp - (uintptr_t)sp%16); /* 16-align for OS X */ memmove(sp, &argc+1, argc*sizeof(int)); *--sp = 0; /* return address */ ucp->uc_mcontext.mc_eip = (long)func; ucp->uc_mcontext.mc_esp = (int)sp; }
第6到第9行实现了用户指定参数的入栈,第11行将返回地址指定为0.实际上linux实现的makecontext接口会根据ucontext结构体uc_link指向的值来进行设定,可以让其返回到另外一个协程继续执行。
12、13行分别设定了ip寄存器和栈指针的值,这就指定了协程开始运行的指令地址和所使用的栈空间。
makecontext函数的调用往往会伴随着SET函数的调用,由于makecontext已经指定好用户传进来的函数入口地址和栈空间的起始地址了,而SET函数返回后就会开始执行用户指定的函数了,协程开始了。
注:上述引用代码均来自于开源项目libtask。
协程实现的方式
c/c++不直接支持协程语义,但有不少开源的协程库,如:
Protothreads:一个“蝇量级” C 语言协程库
libco:来自腾讯的开源协程库libco介绍,官网
coroutine:云风的一个C语言同步协程库,详细信息
目前看到大概有四种实现协程的方式:
第一种:利用glibc 的 ucontext组件(云风的库)
第二种:使用汇编代码来切换上下文(实现c协程)
第三种:利用C语言语法switch-case的奇淫技巧来实现(Protothreads)
第四种:利用了 C 语言的 setjmp 和 longjmp( 一种协程的 C/C++ 实现,要求函数里面使用 static local 的变量来保存协程内部的数据
相关文章推荐
- Unity-EasyTouch插件之ReservedArea的运用(主要是避免JoyStick与Touch的矛盾)
- Android应用程序如何调用支付宝接口
- c/c++生成不重复的字符串(6个字符组成,可表示的个数可以扩充),简易版数据库主键
- Unity-EasyTouch插件之ReservedArea的运用(主要是避免JoyStick与Touch的矛盾)
- Android 使用高德SDK实现导航笔记
- mysql 索引设计准则
- PCM与DSD究竟是什么??
- jQuery.validate验证上传文件大小
- PAT(甲级)1090
- 在Ubuntu中安装Redis
- Retrofit 1.9源码学习
- 《modern operating system》 chapter 5 Input and output 注意事项
- 城市天气三小时预报,天气预报接口实现
- 企业中如何给数据库用户授权
- apache搭建完成测试PHP页面
- tomcat性能调优
- MyEclipse 或 Eclipse 调使用内存的大小
- memcache
- 分类设计
- MySQL不能启动 Can't start server : Bind on unix socke