又来聊协程,这次我们手撸一个,C语言的

上次写了一篇用协程扫描SSH协议的文。两周过去了没人看~
人都是有分享欲的~即使没人看,也想写。
又不喜欢CSDN,就只有b站还勉强对我胃口的样子。
话不多说,本文看点:
C++有栈协程。纯手撸不调库。实现又小巧
看完对栈帧的理解又增加了
又名:把栈放进堆区要分几步
要是再有人说局部变量一定在栈里面,就用这篇文章糊他脸(滑稽)
文末附上源码的github,欢迎白嫖。点赞的都是我好兄弟
思路
嗯,理解这篇文章需要对调用栈有个基本的了解~
作者懒,不会从开天辟地讲起,再加上有好多讲得不错的
贴个连接:
[知乎]x86-64 下函数调用及栈帧原理——冷风寒雨宿天涯
https://zhuanlan.zhihu.com/p/27339191
当时吧,我也是瞎看网页,看到大家讲栈帧,就想~我可以为每一个协程在堆区创建一个内存区域,然后将指向栈顶的rsp寄存器指向堆区,接下来,这个函数压入栈中的数据就都在堆区了。
函数的栈移到了堆区,那么每个协程的调用栈就可以互不干扰了。每个函数有独立的调用栈,这样我们只需要切换不同的调用栈和寄存器状态即可在函数间跳来跳去了。
卧槽,我可真是个天才,要是我早生几年——不扯远了,让我们详细的捋捋需要干的事情:
暂停一个任务(yield)
恢复一个暂停中的任务
建立一个新任务
建立一个任务
来看看我用来描述任务的结构体

记得控制进程的叫PCB,那么我这里保存协程状态的就叫CCB好了。
第一个值是指向给调用栈分配的地址的指针,我们说了,要把栈放进堆里~
接下来是调用栈分配的大小,
再接下来是寄存器,amd64一共有16个64位寄存器,其中rsp寄存器指向栈顶。
再接下来是RIP,RIP寄存器指向下一条需要执行的指令的地址。我们需要把这个值保存下来,恢复的时候方便跳转过去。
任务的状态,定义了三个,未开始,就绪(可以调度或正在运行),结束中(将在下一次调度循环清理掉)。当然,理论上还应该有个阻塞状态,以后我实现了同步的那些东西和异步IO之后可能会加上去。
最后是函数的指针和函数参数的vector
接着再来看一个结构体,这个结构体表示当前”调度循环“的上下文。每个系统线程应该对应一个。

这里面保存了:
yield_flag,协程yield的时候将这个标志置位,这样我就知道他是yield了还是return了。哈哈。接下来是保存所有协程CCB的list,保存系统栈各个寄存器的数组,毕竟我还要切回来的嘛。最后是指向当前正在运行的协程的迭代器。
还有一个成员函数用来建立一个新的协程。这个函数做的事情呢,在堆中分配调用栈空间,然后将这个ccb加入到list中。
接下来,在调度循环中,每次我们取出一个协程,如果它没有开始运行,那么就运行它,如果结束了,就释放内存。如果是就绪状态(yield出来了,可以恢复运行)就恢复它。如此轮换往复:

可以看到,这里内联了vc编译器风格的汇编。值得一提的是:msvc编译器并不允许在64位平台的代码中内联汇编,这导致我的程序必须使用clang编译器来编译。为了编译出和msvc兼容的库文件,我踩了不少坑,有空我会专门写一篇文章讲讲其中的坑点。
我们来看这三行汇编。mov rsp, [rsp_] 表示把rsp_这个局部变量的值加载到rsp寄存器中。编译的过程中,clang会自动把变量名替换成一个类似于 [rbp-0x30]这样的形式,非常的方便。
可以看到,我们把栈顶指针rsp指向了堆区,把函数的返回地址压入了栈中,然后跳转到了函数的地址。这样函数的对局部变量压栈的时候就会压入堆中,函数运行完之后,会通过ret指令返回到我压入栈中的FINISH_LABLE所在代码的位置。
yield和resume

yield的过程也很简单,依次保存每一个寄存器的值,然后跳转到YIELD_LABLE的位置。注意在jmp指令的下一行有一个recover label。恢复的时候将跳回到这里,从而无缝连接。
特别注意的是:yield函数由用户调用,这里是从好几层函数中跳出来,而且此时的rbp和rsp寄存器指向的是堆区中的地址,yield之后跳转到的函数则在栈中。所以跳转完成之后,我们首先需要恢复寄存器的状态。
上文中我们说到,我们的”调度循环“每次取出一个任务,然后执行它。
在这个循环的开头,我保存了每一个寄存器的状态,在这个循环的末尾,我恢复所有寄存器的状态并跳转到开头,从而实现一个完整的循环。而YIELD_LABLE正是放在了这个循环的末尾。yield之后,能马上读取循环开头的状态,来修复”破损“的栈。
需要特别注意,这种通过汇编跳转的循环体中,不要创建任何对象,因为jmp指令进行跳转,编译器并不会知道这里是”循环的结束“,所以析构函数不会被调用,而构造函数会被反复调用。
至于resume,就是yield的反过程,取出每一个寄存器的值,然后跳转回原来的位置。
好了,末尾附上项目地址:
https://github.com/newtoncy/cpp_coroutine