协程实现的基础

20 篇文章 0 订阅

转自:http://blog.csdn.net/kobejayandy/article/details/41790943

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

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

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

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

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

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

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

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

 

栈帧结构

栈帧结构

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

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. struct mcontext {  
  2. /* 
  3. * The first 20 fields must match the definition of 
  4. * sigcontext. So that we can support sigcontext 
  5. * and ucontext_t at the same time. 
  6. */  
  7. int mc_onstack; /* XXX - sigcontext compat. */  
  8. int mc_gs;  
  9. int mc_fs;  
  10. int mc_es;  
  11. int mc_ds;  
  12. int mc_edi;  
  13. int mc_esi;  
  14. int mc_ebp;  
  15. int mc_isp;  
  16. int mc_ebx;  
  17. int mc_edx;  
  18. int mc_ecx;  
  19. int mc_eax;  
  20. int mc_trapno;  
  21. int mc_err;  
  22. int mc_eip;  
  23. int mc_cs;  
  24. int mc_eflags;  
  25. int mc_esp; /* machine state */  
  26. int mc_ss;  
  27.   
  28. int mc_fpregs[28]; /* env87 + fpacc87 + u_long */  
  29. int __spare__[17];  
  30. };  
  31.   
  32. struct ucontext {  
  33. /* 
  34. * Keep the order of the first two fields. Also, 
  35. * keep them the first two fields in the structure. 
  36. * This way we can have a union with struct 
  37. * sigcontext and ucontext_t. This allows us to 
  38. * support them both at the same time. 
  39. * note: the union is not defined, though. 
  40. */  
  41. sigset_t uc_sigmask;  
  42. mcontext_t uc_mcontext;  
  43.   
  44. struct __ucontext *uc_link;  
  45. stack_t uc_stack;  
  46. int __spare__[8];  
  47. };  

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

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. int getmcontext(mcontext_t*);/*保存当前上下文的声明*/  

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. /*保存当前上下文的汇编实现*/  
  2. .globl GET  
  3. GET:  
  4. movl 4(%esp), %eax  
  5.   
  6. movl %fs, 8(%eax)  
  7. movl %es, 12(%eax)  
  8. movl %ds, 16(%eax)  
  9. movl %ss, 76(%eax)  
  10. movl %edi, 20(%eax)  
  11. movl %esi, 24(%eax)  
  12. movl %ebp, 28(%eax)  
  13. movl %ebx, 36(%eax)  
  14. movl %edx, 40(%eax)  
  15. movl %ecx, 44(%eax)  
  16.   
  17. movl $1, 48(%eax) /* %eax */  
  18. movl (%esp), %ecx /* %eip */  
  19. movl %ecx, 60(%eax)  
  20. leal 4(%esp), %ecx /* %esp */  
  21. movl %ecx, 72(%eax)  
  22.   
  23. movl 44(%eax), %ecx /* restore %ecx */  
  24. movl $0, %eax  
  25. ret  

上述分别是保存上下文的C接口声明和汇编实现。根据第4行汇编代码可以看出,GET函数所需要的参数值被保存到%eax,之所以根据4(%esp)来寻址,是因为这时候栈指针指向的是保存返回地址的内存地址。接着将各个寄存器的值保存到参数值指向的mcontext结构体,结合下struct mcontext以及代码里的移位看就可以了,这里就不多说了。唯一比较难理解的可能就是%eip寄存器值的获取了。由于这时候要保存的是调用GET函数的过程的上下文,这时候%eip寄存器保存的并不是调用GET函数过程的下一条指令的值,GET函数栈帧的返回地址才是调用GET函数过程返回后应该往下执行的下一条指令,因此可以看到上面汇编代码18行是将栈指针指向内存保存的值做为%eip的值保存起来。

[plain]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. .globl SET  
  2. SET:  
  3. movl 4(%esp), %eax  
  4.   
  5. movl 8(%eax), %fs  
  6. movl 12(%eax), %es  
  7. movl 16(%eax), %ds  
  8. movl 76(%eax), %ss  
  9. movl 20(%eax), %edi  
  10. movl 24(%eax), %esi  
  11. movl 28(%eax), %ebp  
  12. movl 36(%eax), %ebx  
  13. movl 40(%eax), %edx  
  14. movl 44(%eax), %ecx  
  15.   
  16. movl 72(%eax), %esp  
  17. pushl 60(%eax) /* new %eip */  
  18. movl 48(%eax), %eax  
  19. ret  

至于恢复上下文的SET函数,要说的就是它是如何来改变%eip寄存器的值。根据上面第17行的汇编代码,它只是将新的%eip的值压栈而已,并不是直接赋予ip寄存器。我们这里再看一下当执行到ret后会怎么样。ret可以等效于这句指令–pop %eip。当SET函数返回后即将刚刚压栈的新的%eip的值恢复到ip寄存器当中去了。

使用汇编实现的GET和SET函数,实际上就可以进行上下文的保存和恢复了。但是要实现协程这还不够,协程跟线程一样,都是提供一个函数做为入口,那我们还需要为协程构建好调用其函数入口的准备,即参数压栈,栈指针的指向,还有返回地址的保存等。

[cpp]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. void makecontext(ucontext_t *ucp, void (*func)(void), int argc, ...)  
  2. {  
  3.   int *sp;  
  4.   sp = (int*)ucp->uc_stack.ss_sp+ucp->uc_stack.ss_size/4;  
  5.   sp -= argc;  
  6.   sp = (void*)((uintptr_t)sp - (uintptr_t)sp%16); /* 16-align for OS X */  
  7.   memmove(sp, &argc+1, argc*sizeof(int));  
  8.   *--sp = 0;    /* return address */  
  9.   ucp->uc_mcontext.mc_eip = (long)func;  
  10.   ucp->uc_mcontext.mc_esp = (int)sp;  
  11. }  

第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、付费专栏及课程。

余额充值