协程(coroutine)是一种将epoll异步事件转化为同步事件的编程模式。它的出现是近几年的事,是与go语言一起提出的一种编程模型。因为异步事件编程的可读性比较差,于是就有了协程。协程也称为用户模式进程。协程的调度类似于Linux内核对进程的调度。1.无论是协程、进程还是线程,它们都有运行的功能和相关的上下文。函数是它们运行的??代码,上下文是它们的运行状态。pthread库将线程函数定义为void*(*run)(void*),它是一个函数指针,其参数和返回值都是void*:这样定义的线程函数可以传递任何类型的参数给它,并且可以从中获取任何类型的返回值。这个函数是线程要运行的函数。如果是进程,main()函数就是它要运行的进程函数。任何不使用fork()系统调用的进程都从main()函数开始运行。fork()系统调用后的(父)子进程会运行fork()返回后的代码,例如:pid_tcpid=fork();if(-1==cpid)printf("forkerror\n");elseif(0==cpid){//子进程的代码}else{//父进程的下一段代码}协程类似于进程和线程,也有运行的功能。另外,不管是进程、线程还是协程,都有一个运行状态上下文:这个上下文中最重要的数据就是栈!Linux内核进程的内存布局函数的局部变量分配在栈上,函数调用的返回地址也在栈上,各种寄存器也存放在栈上。对于一个正在运行的函数,栈必须是独立的,不能与其他函数共享:因为正在运行的函数会随时修改栈上的数据。无论是线程、进程还是协程都是如此。虽然全局变量和堆内存在同一个进程的不同线程之间是共享的,但是栈是不能共享的。在Linux上,线程和进程基本上是一回事,只是它们共享全局变量和堆。在linux内核中,都是用上图的数据结构来描述的:1)最开始是4096字节(1个内存页),后来扩展到8k字节(2个页)。2)8k内存的低地址是进程的描述结构,是main()函数运行时需要的信息。这8k内存的高地址就是进程在内核中运行时(比如执行系统调用时)的(内核)栈。这两部分的总和就是流程的上下文。因此,在为Linux内核编写模块时,代码中不能使用较大的局部变量,以免覆盖进程的描述结构!字符缓冲区[4096];这样的代码不能写在内核中,因为局部变量的内存分配在栈上,而内核分配给每个进程的栈很小(8k)。这个buf数组占4k,如果函数调用稍微复杂一点,可能会覆盖掉低地址的进程结构。Linux内核在调度进程时,不断地切换上图中的数据结构,从而实现多个进程的交替运行。因为调度间隔远小于人眼所能感知的时间间隔,即使在单核CPU上,在人看来也是多个进程同时运行。2.协程的实现如果多个协程要在用户态交替运行,每个协程必须配备不同的栈。多个协程属于同一个进程,进程栈的位置是由操作系统预先分配的。因此,在为每个协程配置栈时,每个栈的内存范围必须在进程栈的范围内。有栈的协程的内存布局如上图所示:你想在“进程”的栈上为协程提前开辟多少空间?每个协程预留的栈有多大?如果预留量小,也会出现协程函数的局部变量覆盖协程描述结构的情况。如果预留量很大,则同一进程支持的协程总数会减少。而且,程序员的用户态代码通常比内核代码更广泛。写一段用户态代码,不要让我这样打开buffercharbuf[1024*1024],行不行?没有一个程序员愿意像写内核驱动那样手忙脚乱地写用户代码。所以,有栈协程的坏处就非常明显了!1)首先,每个进程支持的协程数量是有限的,不是无限的。大多数情况下,虽然用户代码要开启的协程数不会超过上限,但毕竟是有限集,不是可数集。这个对用户代码的限制还是比较大的。有这样的限制,每次创建协程的时候都需要检查是否成功。代码是这样的:intret=coroutine_create();if(ret<0){printf("error\n");return-1;}而不是这样:coroutine_create();否则代码不完整,因为没有处理异常。2)万一协程函数中有复杂的递归,协程栈溢出,那么可能会覆盖多个协程的数据,导致程序挂掉。可以想见,这个挂位几乎可以肯定不是第一幕!这种BUG查起来还是很麻烦的。第一个场景不挂的内存bug都是C语言很难发现的bug,而且很可能是随机的然后,就会出现stacklesscoroutine。3.Stackless协程Stackless协程的实现也很简单,切换协程前将当前协程的栈数据保存到堆中即可。每个协程的上下文是malloc()申请的堆内存,在上下文中预留了一块空间,切换协程时(当前协程的)栈数据就保存在这个预留空间中。当协程被调度再次运行时,最后的堆栈数据从(协程的)上下文复制到进程堆栈,协程可以再次运行。Stackless协程的内存布局如上图所示。协程0被挂起,协程1被调度运行:1)首先将进程栈上的数据复制到协程0的上下文中,此时进程栈上的数据就是协程0的所有栈数据。协程的上下文是malloc()申请的堆内存。如果栈数据太大,可以使用realloc()重新分配更大的内存。这就打破了协程栈大小固定的缺陷。每个协程可以使用的堆栈大小仅受进程堆栈大小的限制。2)当协程栈不再受限时,可以创建的协程数只受进程堆大小的限制。只有整个进程的堆内存耗尽后,协程的创建和运行才会继续进行。我附加到scf编译器框架的协程的实现是stacklesscoroutine,它在scf/coroutine目录下。2021年5月思考这些问题,给出了解决的代码,github和gitee的scf代码都有。从2022年开始,我就没有更新github上的代码了。目前gitee上的scf是最新的。
