在前一篇文章《基于汇编的 C/C++ 协程 - 背景知识》中提到一个用于 C/C++ 的协程所须要实现的两大功能:html
其中调度,其实在技术实现上与其余的线程、进程调度没有什么特别的差别,同时也要看具体业务的需求。限制 C/C++ 协程应用的最大技术条件是上下文切换。理由在前文也说了。linux
既然本系列讲的是基于汇编的 C/C++ 协程,那么这篇文章咱们就来说讲使用汇编来进行上下文切换的原理。git
本文地址:http://www.javashuo.com/article/p-pqrzzlzl-da.htmlgithub
首先咱们须要明白上下文切换具体须要作什么工做。我想,看这篇文章的读者应该对编译原理和操做系统基础知识已经有必定的基础了吧?小程序
协程的切换要作的事情,和进程的切换,实际上是差很少的。这里咱们将本文涉及的要点提一下:segmentfault
当进程开始执行、以及进程执行结束的时候,操做系统还有别的工做:微信
当触发进程切换时(不管是进程调用阻塞的系统调用,可是操做系统主动触发 schedule),操做系统要作如下的几件事情:架构
没有调查就没有发言权,没有实验也就没有讲解权。实际上本人已经有实现的代码了。后文就以个人代码为脉络来讲明。oracle
相关说明:异步
程序入口参见 main.cpp 文件的第 67 至 91 行,_true_main()
函数。
建立协程使用的是 AMCCoroutineAdd()
函数,函数定义在这里。能够参照 struct _CoroutineInfo
结构体。
要执行协程,咱们须要为协程做如下准备:
协程执行起来就像进程同样,须要有堆栈来实现函数调用。线程的堆栈是由操做系统分配的;协程因为工做在用户态,所以只能由咱们写代码分配了。
在个人代码中,栈空间使用 mmap()
分配。固然也可使用 malloc()
——libco
就是这么作的。
栈空间的使用,是经过向栈寄存器
直接赋值来实现的。这在后面再讲。
协程函数入口其实就是提供的协程函数自己,所以咱们只须要直接将函数的地址直接保存下来就好了。
可是协程出口就比较复杂了。协程执行到出口位置时(也就是协程函数的 return
语句)即表明协程结束。此时协程库应该可以正确捕捉而且记录下协程结束的状态,而且正确的切换到下一个应当被切换的堆栈。
被切换至的堆栈,多是另外一个协程,也有多是协程库的调用线程。
这一段代码我使用太重定向协程函数返回地址来实现的,须要搭配汇编使用。能够参见代码中 _coroutine_did_end()
函数。该函数在协程初始化的时候,保存在了 func_ret_addr
成员变量中。
请注意这个变量在结构体中的偏移值:64,下文的 asm_amc_coroutine_enter()
汇编函数就用上了。
当切换协程时,须要切换函数的上下文。切换上下文也称为 “保存现场” 和 “恢复现场”。所谓的 “现场”,其实就是必要的 CPU 寄存器值,这些寄存器里就已经包含了协程的堆栈。
参考资料用户态调度要保存些什么中就说明了在 GCC 程序中,须要保存的寄存器内容(x86_64 / x64):
线程调用保存的环境更多,不过做为协程,咱们只须要保存上面这些寄存器就够了。
启动线程的入口是 AMCCoroutineRun()
函数。函数的基本逻辑以下:
asm_amc_coroutine_dump(g_pMainThreadInfo); // dump main thread again to get return point of this function. g_pMainThreadInfo->reg_rsp += 1 * sizeof(uint64_t); // ignore return address for function "asm_amc_coroutine_dump"
协程要求单线程执行。本文所谓的主线程,指的就是启动协程的线程。这两句的逻辑以下:
asm_amc_coroutine_dump()
将主线程的上下文保存在一个全局变量中asm_amc_coroutine_dump()
中保存的函数返回地址,使得全局变量中保存的是 AMCCoroutineRun()
的返回地址。调用汇编函数 asm_amc_coroutine_enter()
,直接进入协程。函数很简单:
asm_amc_coroutine_enter: movq (%rdi), %rbx movq 8(%rdi), %rsp movq 16(%rdi), %rbp push 64(%rdi) # create a function return point jmp 56(%rdi)
五句命令的含义分别是:
func_ret_addr
成员,将这个地址压入堆栈,使得协程函数结束时即进入相应的函数中,这样咱们就能够检测到一个协程已经执行完毕了。而因为协程是单线程运行的,所以咱们可使用全局变量判断出刚刚结束的是哪个协程。前文不是说了一大堆须要保存的上下文吗,为何这里赋值的寄存器那么少?很简单,协程尚未开始执行呢,那些寄存器都不用恢复,让协程直接用就好了。
注意,这个函数其实是不会返回的。返回到主线程的工做已经交给了被重定向了的 _coroutine_did_end()
函数来完成。
当切换协程时,调度函数须要获取 CPU 使用权,其实很简单:只是要求协程程序本身主动调用相关的函数,从而达到交出 CPU 使用权的目的。
参见 main.cpp 文件的第 33 至 62 行。这里定义了两个如出一辙的函数,至关于两个协程
做为 demo 程序,这里协程只调用了一个函数 AMCCoroutineSchedule()
提请切换协程。
这里调用的是汇编函数 asm_amc_coroutine_dump()
。实际上这个函数在前面保存主线程现场中已经使用过了,这里咱们再详细说明一下函数的实现:
asm_amc_coroutine_dump: movq %rbx, (%rdi) movq %rsp, 8(%rdi) movq %rbp, 16(%rdi) movq %r12, 24(%rdi) movq %r13, 32(%rdi) movq %r14, 40(%rdi) movq %r15, 48(%rdi) movq 16(%rsp), %rsi movq %rsi, 56(%rdi) retq
除了标号以外的最前面的七行很好理解,就是将必要的现场保存起来。至于倒数第2、三行的 movq 16(%rsp), %rsi
和 movq %rsi, 56(%rdi)
就很回味无穷啦。
寄存器 rsi
在 GCC 中是做为第二参数使用的。这个函数中没有第二个参数,所以就只是做为临时变量而已。16(%rsp)
这一句,和前文中 “保存主线程的现场” 中的第二句代码的做用殊途同归。
另外,协程上下文的保存,还包含函数外面的一句 C 代码:
g_pCurrentCoroutine->reg_rip = (uint64_t)(&&RETURN);
这句话把被切换掉的协程恢复的现场重定向为 AMCCoroutineSchedule()
的 return
语句。效果是跳过了下面的 asm_amc_coroutine_restore()
函数,避免重复调度。
本 demo 中没有实质性的调度,只是轮询而已,找到协程链上的下一个协程并执行。
这个过程就是下面两句:
g_pCurrentCoroutine = g_pCurrentCoroutine->p_next; asm_amc_coroutine_restore(g_pCurrentCoroutine);
只是简单的调用 asm_amc_coroutine_restore()
汇编函数的过程。这个汇编函数我就不贴上来了,由于其逻辑和前面的 asm_amc_coroutine_enter()
相同,只是保存的现场比较多而已。
前文说到,当协程结束的时候,会调用 return
返回。这个时候在汇编中作了如下的事情:
retq
返回(retq 同时会将返回地址出栈丢掉)这就是咱们前文中将协程返回地址重定向的原理基础。
协程结束后,会返回到 _coroutine_did_end()
函数中。这里须要注意的是,返回的位置是该函数的入口,所以反汇编会发现,这个函数还额外作了压栈的动做。不过不要紧,由于这个动做是在即将被销毁的协程堆栈中进行的,所以不用担忧内存泄露啥的。
这个函数作了如下几个操做:
调用汇编函数 asm_amc_coroutine_switch_sp_rip_to()
把当前的堆栈切换的主线程中。之因此要马上切换掉,是由于协程已经结束了,协程的资源也应该销毁。若是还在协程的堆栈上工做的话,那么堆栈销毁掉后会致使 segment fault。
这很好理解了,前面给协程分配了堆栈,用完了确定要还的。
若是还有其余未完成的协程,那就调度过去,和前文同样。
这里用的则是 asm_amc_coroutine_return_to_main()
汇编函数,和切换协程的函数就是差在第一句汇编语句上:
popq %rsi
这句话后面的注释也说了,其实仍是玩堆栈。这句话将这个汇编函数原来的返回地址出栈掉,采用以前重定向的地址——也就是主线程调用 AMCCoroutineRun()
以后的下一句代码
我的以为我关于协程的两篇文章恐怕看的人不多,或许如今用 C/C++ 写后台服务的人不多了吧,sad ……
计划这系列文章是分三个部分的,分别是: