当前位置: 首页 > Linux

基于汇编的C-C++协程-切换上下文

时间:2023-04-06 02:00:27 Linux

在之前的文章《基于汇编的 C/C++ 协程 - 背景知识》中提到了一个C/C++的协程需要实现的两个功能:协程调度上下文切换,调度,其实并没有什么特别的区别其他线程和进程调度的技术实现,还要看具体业务的需要。限制C/C++协程应用的最大技术条件是上下文切换。原因上面也说了。由于本系列讲的是基于汇编的C/C++协程,所以本篇我们会讲一下使用汇编进行上下文切换的原理。本文地址:https://segmentfault.com/a/1190000013177055参考资料基于epoll-interface设计一个类似libevent的异步I/O库linux平台学习x86汇编(十九):调用汇编函数的函数调用规则C语言中的x64x86和x64汇编调用C函数参数传递规则(GCC)从汇编角度分析C程序x86寄存器介绍协程分析上下文上下文切换Linux中的局部变量和栈x86-64寄存器和栈帧作为值标签usermodescheduling保存上下文切换的具体内容首先我们要明白上下文切换需要做什么。我想看到这篇文章的读者应该已经对编译原理和操作系统基础知识有了一定的基础吧?协程的切换要做的事情其实和进程的切换类似。这里提一下本文涉及的要点:进程的创建和删除。上下文,然后把CPU交给进程。如果进程执行结束,销毁进程资源并正确返回给调用者(如父进程)。进程调度时的上下文切换但操作系统主动触发调度),操作系统要做如下事情:抢占CPU使用权,保存当前用户进程的上下文,调用调度函数,找到下一个进程应该占用CPU时间片,恢复下一个进程的上下文,将CPU交还给待继续的进程Samplecode没有调查就没有发言权,没有实验就没有解释权。事实上,我已经有了实现它的代码。下面的文章将以我的代码作为上下文来说明。相关说明:代码仅支持x86_64或x64架构。本来打算继续开发和支持i386;但后来放弃了,因为看到微信已经大规模使用的协程库libco——这个我会在以后的文章中讲到。协程的创建和执行程序的入口,见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寄存器保存区切换协程时,需要切换函数的上下文。切换上下文也称为“保存上下文”和“恢复上下文”。所谓“site”其实就是必要的CPU寄存器值,里面已经包含了协程的栈。参考资料UserModeSchedulingWhattoSave解释了GCC程序(x86_64/x64)中需要保存的寄存器内容:rsp:stackpointer,指向栈顶,也就是下一个可用的栈地址。rbp:栈基指针,与rsp配合使用。在很多小程序中往往为0,但是我们必须保存。rbx,r12-r15:数据寄存器,也是必须保存的字段之一。rip:程序中要运行的下一条指令的地址。这是计算机执行程序的基础。线程调用保存的环境比较多,但是作为协程,我们只需要保存上面的寄存器即可。启动协程线程的入口点是AMCCoroutineRun()函数。函数的基本逻辑如下:保存主线程的场景asm_amc_coroutine_dump(g_pMainThreadInfo);//再次转储主线程以获得此函数的返回点。g_pMainThreadInfo->reg_rsp+=1*sizeof(uint64_t);//忽略函数的返回地址“asm_amc_coroutine_dump”协程需要单线程执行。本文中所谓的主线程是指启动协程的线程。这两句的逻辑如下:首先,asm_amc_coroutine_dump()将主线程的上下文保存在一个全局变量中。第二句将栈指针移动一个单位。实际上,函数asm_amc_coroutine_dump()中保存的函数返回地址被忽略了。AMCCoroutineRun()的返回地址保存在全局变量中。切换到要调用的协程上下文,调用汇编函数asm_amc_coroutine_enter()直接进入协程。函数很简单:asm_amc_coroutine_enter:movq(%rdi),%rbxmovq8(%rdi),%rspmovq16(%rdi),%rbppush64(%rdi)#创建函数返回点jmp56(%rdi)五个命令的意思是:将主线程的rbx寄存器值复制到协程中——其实这句话我也不是很明白,请指教。重定向栈地址——只有进入协程函数后才会使用这个栈。重定向栈基地址——同样是进入协程函数后使用,所以不影响这里的程序执行。这就是上面提到的func_ret_addr成员。这个地址被压入栈中,这样当协程函数结束时,就会进入对应的函数,这样我们就可以检测到有一个协程被执行了。而且由于协程是单线程运行的,我们可以使用全局变量来判断哪个协程刚刚结束。强制跳转到协程的入口点开始执行。上一篇文章不是说了很多需要保存的上下文吗,为什么这里分配的寄存器这么少?很简单,协程还没有开始执行,那些寄存器不用恢复,直接让协程使用就可以了。请注意,此函数实际上并不返回。返回主线程的工作已经交给了重定向的_coroutine_did_end()函数。切换协程时获取CPU使用权在切换协程时,调度函数需要获取CPU使用权,其实很简单:只需要??协程程序主动调用相关函数即可达到让出CPU使用权的目的。请参阅main.cpp文件的第33至62行。这里定义了两个相同的函数,作为演示程序相当于两个协程。这里协程只调用一个函数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)movq16(%rsp),%rsimovq%rsi,56(%rdi)retq的前七行是简单易懂除了标签,就是保存必要的场景。至于倒数第二行和第三行的movq16(%rsp),%rsi和movq%rsi,56(%rdi),就很耐人寻味了。寄存器rsi在GCC中用作第二个参数。这个函数没有第二个参数,所以只是作为临时变量使用。16(%rsp)这句和上一篇“保存主线程的场景”中代码的第二句效果是一样的。另外,协程上下文的保存还包含了一句函数外的C代码:g_pCurrentCoroutine->reg_rip=(uint64_t)(&&RETURN);这句话将被关闭的协程的恢复场景重定向到AMCCoroutineSchedule()的返回语句。作用是跳过后面的asm_amc_coroutine_restore()函数,避免重复调度。调度这个demo中没有实质性的调度,只是在协程链上轮询、寻找并执行下一个协程。下一个协程恢复上下文并交出CPU的过程就是下面两句: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()将当前栈切换到主线程。之所以马上切换,是因为协程已经结束,协程的资源也应该被销毁了。如果你还在协程的栈上工作,栈被销毁后会导致段错误。销毁协程的堆栈和其他资源很容易理解。栈是早先分配给协程的,用完了就得归还。其他协程的调度如果还有其他未完成的协程,则调度过去,同前。这里使用asm_amc_coroutine_return_to_main()汇编函数返回主线程,切换协程的函数就是第一条汇编语句的区别:popq%rsi这句话后面的注释也说了,其实还是在玩堆栈。这句话从栈中弹出这个汇编函数的原始返回地址,并使用之前重定向的地址——也就是主线程调用AMCCoroutineRun()之后的下一行代码。人很少,可能现在用C/C++写后台服务的人已经很少了,悲哀。。。本系列文章计划分为三部分,分别是:coroutines介绍汇编原理libevent结合coroutines(libco)这是同步服务开发的前两部分。最后一部分,目前代码已经完成,下一篇是原理文档,欢迎阅读~