协程实现的基础

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

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

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

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

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

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

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

ia32程序用程序栈来支持过程调用。机器用栈来传递过程参数、存储返回信息、保存寄存器用于以后恢复,以及本地存储。为单个过程分配的那部分栈称为栈帧。下图描绘了linux下栈帧的通用结构。栈帧的最顶端以两个指针界定,寄存器%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同名的寄存器值。

int getmcontext(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


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值