文章目录
  1. 1. resume yield
  2. 2. 传递变量
  3. 3. copy stack
  4. 4. lambda与函数指针

闲来无事,实现了一个协程。目前实现的特性有:

  1. 手动yield、resume
  2. 支持在yield的时候传递变量
  3. 使用copy stack来减少协程的大小,目前每个协程需要1928B内存

简单讲一下实现的方法。

resume yield

协程首要的一个特性就是可以自己挂起,自己恢复,因此我们可以把它当做一个可以暂停、可以多次返回值的函数。为此,需要保存这个函数的上下文,并且让函数运行在另外的栈上。

所谓函数上下文,就是把函数使用的寄存器保存一下;而保存栈的话,则是为了防止主程序来践踏协程栈,因此也需要另外保存。保存上下文有多种方法,自己用汇编写一个也很简单,或者用linux提供的ucontext,以及其他一些方法。

我这里使用了ucontext,它同时提供了上下文切换的功能,有四个函数:

1
2
3
4
getcontext(ucontext_t* ctx);
setcontext(ucontext_t* ctx);
makecontext(ucontext_t* ctx, void(*fn)(), int argc, ...)
swapcontext(ucontext_t* old, ucontext_t* ctx);

getcontext用来保存当前的上下文到ctx;setcontext是把当前的上下文设置为ctx;makecontext则是用来修改ctx,把ctx的入口改成函数fn,并且可以传递参数给fn;swapcontext实现上下文切换,保存当前上下文到old,切换到ctx。这里的swapcontext看起来有点多余,用get再set不就可以了吗?其实不是的,swapcontext实现的效果是“原子”切换上下文,而先get,保存了当前上下文,再切换到新的。当线程的上下文再切回到old的时候,如何是用get、set保存的,回到的是getcontext的下一条指令即setcontext函数;而用swapcontext保存的话,回到的则是swapcontext的下一条指令,也相当于从swapcontext函数返回,并继续执行。

那么swapcontext如何实现呢?这和我之前写kernel的时候写的线程切换也差不多。首先明确其功能,swapcontext是一个函数,用来保存caller的下一条指令地址,以及caller的其他寄存器。但是一般的函数调用首先就会修改sp、bp、ip,所以我们需要用汇编来写。ip保存caller下一条指令地址,其实就是返回地址,[sp]就是了;sp、bp直接保存,因为我们没有修改它的值。当然,这都是我猜的。。

顺便再猜一下makecontext的实现。把函数地址保存到ip,至于传参的话,就先把参数push到栈里,再留一个返回地址的占位,应该就能搞定了。

所以,有了ucontext之后,实现resume就是先swapcontext(主程序上下文,协程上下文),而yield就是反过来swapcontext(协程上下文,主程序上下文)。很简单。

传递变量

yield还有一个重要功能就是可以传递一个变量到主程序,我们可以用这种方式来实现generator。这里的传递不是函数返回,而是变量在不同上下文之后的传递,因为yield会切换到resume保存的上下文,我们只要把这个变量传给resume的上下文,让resume函数直接返回这个变量就ok了。那么,如何传递呢?无疑是要借助堆内存了,yield把变量放到堆中,把地址告诉resume,然后resume取出来,释放内存。so,地址要怎么传递?可以用一个类成员来传递这个地址;或者用上下文的rax寄存器来传递,反正rax本身就是用来传递参数的,只要在切换过程中不会破坏rax就可以了,实践证明是可行的。

不过传递还有一些问题,如果这个变量是指针?其实就不需要分配空间,直接用rax传递这个指针,如果是数组之类的,还需要做一些特化。

copy stack

协程栈也很有讲究。栈分很多中,平常使用的是静态栈,大小固定,缺点是浪费空间;segmented stack,按需分配,gcc支持;动态栈,栈不够用时重新分配一块,但是c++很难实现,因为只要有指向栈空间的指针,在移动之后就会立即失效;copy stack,用一个大的内存当做栈,用完之后把使用的栈保存到每一个协程。

这里就用了copy stack,每个协程保存一个栈基址,以及栈顶指针。另外定义一个大的公用栈,所有协程都用这一个共用栈;在使用之前,把协程栈的内容拷贝过去,用完再拷贝回来。

lambda与函数指针

这算是一个小问题,就是如何拿到lambda的函数指针,看起来用target函数很容易拿到,但这里有一个坑:就算是用std::function保存的lambda,target也是未必能拿到的函数指针的。这里还没有深入研究,我猜是因为target用了dynamic_cast,而那个指针其实是void (Lambda::)(Lambda),成员函数指针,用户几乎不可能拿到啊,回头再研究一下。

不过有一个解决方案,就是在std::function外面再套一层普通的函数,把这个函数指针当做ucontext的入口,然后把std::function当做参数传递给入口函数,这样就能解决lambda与函数指针之间的转换问题。

文章目录
  1. 1. resume yield
  2. 2. 传递变量
  3. 3. copy stack
  4. 4. lambda与函数指针