协程实现的基础

协程可以认为是一种用户态的线程,与系统提供的线程不同点是,它需要主动让出CPU时间,而不是由系统进行调度,即控制权在程序员手上。

既然看成是用户态线程,那必然要求程序员自己进行各个协程的调度,这样就必须提供一种机制供编写协程的人将当前协程挂起,即保存协程运行场景的一些数据,调度器在其他协程挂起时再将此协程运行场景的数据恢复,以便继续运行。这里我们将协程运行场景的数据称为上下文。

在linux里,有getcontext和swapcontext等接口来获取当前的上下文数据和切换上下文。那如果没有提供相应的接口,又该如何来实现呢?

其实说到底,保存下上文数据,不外乎就是保存下当前运行的栈空间的数据,还有cpu各个寄存器相应的值。只要我们能够将其保存下来,在特定的时刻恢复回去就可以了。

有人用c提供的接口setjmp和longjmp来实现协程的切换和恢复,但这里要介绍另外一种方式,即用汇编来保存/恢复cpu寄存器的值。

用汇编的方式依赖于特定的平台,这里举例的是i386 32位的*nix平台

在开始贴代码前,要先说一个概念--栈帧

ia32程序用程序栈来支持过程调用。机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。为单个过程分配的那部分栈称为栈帧。下图描绘了linux下栈帧的通用结构。栈帧的最顶端以两个指针界定,寄存器%ebp为帧指针,而寄存器%esp为栈指针。当程序执行时,栈指针可以移动,因此大多数信息的访问都是相对于帧指针的。

 

栈帧结构

这里我们可以看到,在调用一个函数前,都会先将各个参数、调用者在被调用函数返回时执行的下一条指令的地址--返回地址压栈,被调用函数在开始前会将%ebp的值保存,然后将当前%esp的值赋予%ebp。弄明白帧指针和栈指针的作用,以及返回地址等如何通过%ebp来获取的话,对下面保存当前上下文的汇编代码理解比较有帮助。

ucontext结构体主要关心的为uc_mcontext和uc_stack这两个成员,其中uc_stack指向一段内存,这段内存做为协程的运行栈;而uc_context为mcontext类型,各个成员保存着CPU同名的寄存器值。

上述分别是保存上下文的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函数返回后就会开始执行用户指定的函数了,协程开始了。

标签: , ,
文章分类 Programming, Unix/Linux

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*