boost 协程中 arm64 和 x86_64 架构汇编实现
boost 协程上下文切换 make_arm64_aapcs_elf_gas.S 和 jump_arm64_aapcs_elf_gas.S 以及 make_x86_64_aapcs_elf_gas.S 和 jump_x86_64_aapcs_elf_gas.S 汇编源码分析
源码目录:boost-1.58.0\libs\context\src\asm
make_arm64_aapcs_elf_gas.S
1 | /* |
jump_arm64_aapcs_elf_gas.S
1 | /* |
make_x86_64_sysv_elf_gas.S
1 | /* |
jump_x86_64_sysv_elf_gas.S
1 | /* |
x86_64 架构上下文切换
实现协程最核心的部分就是栈切换了,其他的和非阻塞 io 的编程方式没什么区别。
栈切换,libc 中有一个实现,swapcontext,但是已经被标准移除了,未来是否可用不得而知,自己实现需要写汇编代码,这是一个很困难的任务,因为既要熟悉不同 cpu 指令集又要熟悉不同平台的标准,好在从 boost library 的协程实现中找到了已经写好了的栈切换汇编代码,利用这些汇编代码可以在 c 语言中实现栈切换。
这段代码是在 s_task 协程库中发现的,s_task 很好,还可以和 libuv 结合,如果没有特殊要求,可以直接使用了,但是如果想根据自己工作中的业务逻辑做定制,还是需要掌握原理,并且不清楚原理,可能不能用最恰当的方式使用,实现最好的设计,出了问题也不知道该怎么查和用什么办法查。
附上 s_task 和 boost library 的地址,感兴趣的可以去研究一下。
https://github.com/xhawk18/s_task
https://github.com/boostorg/context
栈切换代码在源码的 asm 目录中,实际上在 c 语言中对应两个函数,
1 | typedef void* fcontext_t; |
这两个函数是什么意思,怎么用,看了 s_task 中的代码,但是开始的时候还是没看懂,于是想从汇编的角度入手,最终通过 x86_64 的汇编代码 (make_x86_64_sysv_elf_gas.S jump_x86_64_sysv_elf_gas.S) 弄清楚了这两个函数的用法,结论在本文最末尾,如果只想看结果,可以跳到最后面。
直接看代码注释吧。
make_x86_64_sysv_elf_gas.S
1 | 1 /* |
jump_x86_64_sysv_elf_gas.S
1 | 1 /* |
简单测试一下
1 | 1 #include <stdio.h> |
1 | % gcc -O t.c asm/make_gas.S asm/jump_gas.S |
结论:
make_fcontext 中的 sp 是栈顶指针,size 是栈空间的大小,虽然 cpu 可能支持两种方向,但目前我还没有听过栈方向递增的系统,如果是递减栈,应该把栈顶指针设置成最高地址,比如申请了一段内存作为函数栈,大小 1024,地址在 void *p 中,那么应该这样用
1 | make_fcontext(p+1024, 1024, fn); |
fn 是协程任务启动的时候运行的函数,和线程创建的时候指定的函数类似。make_fcontext 的返回值就是一个运行 (栈空间) 上下文,它将被用于 jump_context 中的 to 参数,这样可以启动这个任务。启动任务之后调用 fn,fn 是 jump_context 中调用的,函数参数传递的实际上是两部分,第一部分,fcontext_t 类型,是栈切换之前的运行上下文,第二个参数是
jump_fcontext 中传递的 vp 参数。jump_fcontext 的返回值也分为两部分,第一部分是栈切换之前的运行上下文,第二部分是传递的参数 vp(这个 vp 不是本任务 jump_fcontext 调用的 vp,而是其他任务切换到本任务的时候调用 jump_fcontext 传递的 vp),这是因为 jump_fcontext 不是普通的函数,普通的函数在一个栈空间上执行,而 jump_fcontext 是跨越栈空间的,函数调用前,要保存现场,然后再恢复,jump_fcontext 也要保存现场,但是恢复并不是在这个函数调用中恢复的,这个调用已经一去不复返了,只负责跳走,不负责回来,甚至可能永远也回不来了,与其说它回来了,不如说是别的地方跳过来了,jump_fcontext “返回” 的就是跳过来的那个人的信息,它的运行上下文 (保存着它的运行现场,cpu 寄存器,栈空间),还有它调用 jump_fcontext 跳过来时传递的参数 vp。
一个任务只要执行,它的运行上下文就是在变化的,也就是说只要一个任务一运行,那么保存它的运行上下文的变量 (fcontext_t) 就失效了,也就是每运行一次就要更新一次,什么时候更新呢,就是本次运行暂停的时候更新,也就是调用 jump_fcontext 跳到其他地方的时候,也就是说目标任务要更新本任务的运行上下文变量,目标任务激活的时候表现为从它的 jump_fcontext 返回 (实际上不是它返回,而是本务跳到那去执行),返回值中的 fcontext_t 就是跳之前的运行上下文,也就是本任务的最新的运行上下文,为了更新本任务的运行上下文变量,需要把本任务的运行上下文变量的地址传递给目标任务,这需要借助 jump_fcontext 的 vp 参数,它也会出现在目标任务返回的 transfer_t 中,这就是参数 vp 和返回值 transfer_t 的作用吧,当然借助 vp 还可以传递更多需要交互的信息。
每一个普通任务都是通过 make_fcontext 创造出来的,但主任务 (main 函数) 不是,主任务切换到了其他任务执行,如果想切换回主任务,就必须获取主任务的运行上下文,fn 函数本来只需要自己运行的数据就够了,而它的参数 transfer_t 还带一个 fcontext,就是干这件事用的,它是最新的切换之前的运行上下文,谁启动的它,就更新谁,一个任务切换到一个曾经运行过的任务的时候一定是跳到 jump_fcontext 的下条指令,可以通过” 返回” 的 transfer_t 来实现更新,但是对于新任务,不会从 jump_fcontext 继续执行,不会得到” 返回” 的 transfer_t,就需要利用参数传递的 transfer_t 更新。
主任务启动 -> task0 -> jump_fcontext , task0 恢复执行,这时候返回的 transfer_t 不一定是主任务的运行上下文,因为它是切换 task0 任务,使 task0 再次运行的那个任务的运行上下文,那个任务未必是主任务,主任务只是第一次启动 task0 的时候的任务,并不一定是后续激活 task0 的任务。
如果 fn 函数运行完成,没有通过 jump_fcontext 切换任务,直接返回,那么结果是整个进程都退出了,因为它返回之后是返回到了 make_fcontext 的一段代码中,那段代码的操作是退出进程。从 gdb 里用 bt 可以看到,一个任务的调用者是 make_fcontext,而不是启动它或再次激活它的时候对应的函数,这是因为 make_fcontext 里面设置这个函数的时候同时设置好了它的返回地址,所以如果不想退出进程,一个任务完成的时候需要跳到其他任务,然后释放保存本任务的栈空间资源,需要注意的是释放是要在别的地方释放 (s_task 库中有一个 join 任务的地方),而不能在跳转之前释放,因为跳转的时候要保存当前运行上下文到当前的栈空间,如果释放了再跳转会造成非法地址的错误。