libco源码解析(4) 协程切换,coctx_make与coctx_swap

2024-03-13 12:10

本文主要是介绍libco源码解析(4) 协程切换,coctx_make与coctx_swap,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

libco源码解析(1) 协程运行与基本结构
libco源码解析(2) 创建协程,co_create
libco源码解析(3) 协程执行,co_resume
libco源码解析(4) 协程切换,coctx_make与coctx_swap
libco源码解析(5) poll
libco源码解析(6) co_eventloop
libco源码解析(7) read,write与条件变量
libco源码解析(8) hook机制探究
libco源码解析(9) closure实现

文章目录

  • 引言
    • 基础知识
    • 正文
    • coctx_make
    • 16L的哲学
    • coctx_swap

引言

题目说的很清楚,这篇文章旨在把协程最为神秘的部分,也即是协程的切换讲的清楚明白,这部分也是令很多人望而生畏的地方,因为在切换协程时用到了一部分汇编代码。所以想要真正理解这部分,还是得先花一点时间把丢掉的汇编先拿回来。

基础知识

首先我们来看下栈帧的定义:

In C and modern CPU design conventions, the stack frame is a chunk of memory, allocated from the stack, at run-time, each time a function is called, to store its automatic variables. Hence nested or recursive calls to the same function, each successively obtain their own separate frames.
Physically, a function’s stack frame is the area between the addresses contained in esp, the stack pointer, and ebp, the frame pointer (base pointer in Intel terminology). Thus, if a function pushes more values onto the stack, it is effectively growing its frame.
.

在C语言和现代CPU的设计规范中,栈帧是一块由栈分配的内存块,在运行时,每当调用一次函数时,都要存储其自动变量。因此对于同一函数的递归调用在每一次都会连续的获得自己独立的栈帧。
从物理上将,函数的栈帧是指esp和ebp之间的一块地址。因此如果一个函数把更多的值压入堆栈,实际上是在扩展它本身的栈帧。

这里算是讲的的非常清楚了,栈帧就是esp和ebp之间的一块内存。

我们来看一下一个栈帧的实际布局;
在这里插入图片描述

在这幅图中我们应该关注的重点就是红框中EBP上面的值,即EIP和采用__cdecl调用约定的参数。这里出现了一个新的名词__cdecl,这其实是函数调用的一种调用约定,下面罗列出来:

  1. __stdcall :函数采用从右到左的压栈方式,自己在退出时清空堆栈。
  2. __cdecl:即C调用约定(The C default calling convention),按从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数vararg的函数(如printf)只能使用该调用约定)。
  3. __fastcall: __fastcall调用的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍旧自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈)。

我们回到上面那幅图,采用__cdecl调用约定的调用者会将参数从右到左的入栈,最后将返回地址入栈。这个返回地址是指,函数调用结束后的下一行执行的代码地址。获取参数和返回地址的话我们只需要通过EBP加偏移就可以了。当然图上的偏移量是32为系统的。

正文

上面简单的过了一下基础知识,接下来我们通过对libco中coctx_makecoctx_swap的解析,搞清楚协程切换的本质,因为学汇编的时候学习的都是32位的,我们以32位为例子进行讲解。64位只是多了一些寄存器和一些调用规则的上的不同罢了,基本的逻辑都是一样的,所以我们选择32位系统进行分析。

我们先来看看与协程切换相关的数据结构:

// 用于分配coctx_swap两个参数内存区域的结构体,仅32位下使用,64位下两个参数直接由寄存器传递
struct coctx_param_t
{const void *s1;const void *s2;
};
struct coctx_t
{
#if defined(__i386__)	// 上下文void *regs[ 8 ];
#elsevoid *regs[ 14 ]; 
#endif size_t ss_size;// 栈的大小char *ss_sp; // 栈顶指针esp};  

coctx_t结构可以说是libco中最为重要的结构了,它直接存储了协程的上下文。

coctx_make

调用coctx_swap之前的准备工作由coctx_make设置完成,我们来看看其实现:

int coctx_make(coctx_t* ctx, coctx_pfn_t pfn, const void* s, const void* s1) {// make room for coctx_param// 此时sp其实就是esp指向的地方 其中ss_size感觉像是这个栈上目前剩余的空间,char* sp = ctx->ss_sp + ctx->ss_size - sizeof(coctx_param_t);//------- ss_sp + ss_size//|     |//|     |//------- ss_sp//ctx->ss_sp 对应的空间是在堆上分配的,地址是从低到高的增长,而堆栈是往低地址方向增长的,//所以要使用这一块人为改变的栈帧区域,首先地址要调到最高位,即ss_sp + ss_size的位置sp = (char*)((unsigned long)sp & -16L);// 字节对齐,16L是一个magic number,下文会做解释// param用来给我们预留下来的参数区设置值coctx_param_t* param = (coctx_param_t*)sp;void** ret_addr = (void**)(sp - sizeof(void*) * 2); // 函数返回值// (sp - sizeof(void*) * 2) 这个指针存放着指向ret_addr的指针*ret_addr = (void*)pfn; // 新协程要执行的指令函数,也即执行完这个函数要cotx_swap要返回的值param->s1 = s; //即将切换到的协程 param->s2 = s1; // 切换出的线程//------- ss_sp + ss_size//|pading| 这里是对齐区域//|s2    |//|s1    |//|原esp |//| 返回地址  |//|esp实际空间|//-------  <- sp(原esp - sizeof(void*) * 2)//|      |//------- ss_sp// 对照着上面那个栈帧的图去看memset(ctx->regs, 0, sizeof(ctx->regs));// ESP指针sp向下偏移2,因为除了ebp还有一个返回地址  // 进入函数以后就会push ebp了ctx->regs[kESP] = (char*)(sp) - sizeof(void*) * 2; //sp初始指向第一个参数的起始地址//函数调用,压入参数之后,还有一个返回地址要压入,所以还需要将sp往下移动8个字节,//32位汇编获取参数是通过EBP+8, EBP+12来分别获取第一个参数,第二个参数的,//这里减去4个字节是为了对齐这种约定,这里可以看到对齐以及参数还有4个字节的虚拟返回地址已经//占用了一定的栈空间,所以实际上供协程使用的栈空间是小于分配的空间。另外协程且走调用co_swap参数入栈也会占用空间,// KESP(7)在swap中是赋给esp的return 0;
}

其实就是一个函数调用过程的模拟,功能就是给coctx_swap做一些准备工作,关键是要理解那个(sp - sizeof(void*) * 2),在理解的时候搭配着那张栈帧的图可以更有效率。

16L的哲学

然后我们来说一说那个16L的魔法数字到底有什么用,我们在代码中提到了这个magic number其实是为了字节对齐。16这个数字非常奇怪,一般来说我们的认知都是32位下字节对齐应该是4,64位系统下当然就是8了,这个16是什么情况?答案就是GCC默认的堆对齐设置的就是16字节。具体可查看这篇文章:《Why does System V / AMD64 ABI mandate a 16 byte stack alignment?》

coctx_swap

接下来我们来看看coctx_swap执行协程切换的过程:

    movl 4(%esp), %eax 这里ESP获取到的是对应图中old %EIP的地址,加4对应第一个参数的地址,把这个值赋给eax,当然也隐藏着eax[0]的赋值| *ss_sp  || ss_size || regs[7] || regs[6] || regs[5] || regs[4] || regs[3] || regs[2] || regs[1] || regs[0] |--------------   <---EAXmovl %esp,  28(%eax)  movl %ebp, 24(%eax)movl %esi, 20(%eax)movl %edi, 16(%eax)movl %edx, 12(%eax)movl %ecx, 8(%eax)movl %ebx, 4(%eax)// 想想看,这里eax加偏移不就是对应了regs中的值吗?这样就把所有寄存器中的值保存在了参数中// ESP偏移八位就是第二个参数的偏移了,这样我们就可以把第二个参数regs中的上下文切换到寄存器中了movl 8(%esp), %eax movl 4(%eax), %ebxmovl 8(%eax), %ecxmovl 12(%eax), %edx  movl 16(%eax), %edimovl 20(%eax), %esimovl 24(%eax), %ebpmovl 28(%eax), %espret// 这样我们就完成了一次协程的切换

这里面对于协程切换来说最重要的就是regs[0]和regs[7]了,regs[0] 存放下一个指令执行地址,也即返回地址。regs[7] 存放切换到新协程后,ESP指针调整的新地址,也就是栈上的偏移。这样程序的数据和代码都被改变,当然也就做到了一个线程可以跑多份代码了。

参考:

  • 博文《__stdcall,__cdecl和__fastcall的作用和区别》
  • 博文《Why does System V / AMD64 ABI mandate a 16 byte stack alignment?》
  • 博文《Libco 协程栈的切换理解》

这篇关于libco源码解析(4) 协程切换,coctx_make与coctx_swap的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/804830

相关文章

nginx -t、nginx -s stop 和 nginx -s reload 命令的详细解析(结合应用场景)

《nginx-t、nginx-sstop和nginx-sreload命令的详细解析(结合应用场景)》本文解析Nginx的-t、-sstop、-sreload命令,分别用于配置语法检... 以下是关于 nginx -t、nginx -s stop 和 nginx -s reload 命令的详细解析,结合实际应

MyBatis中$与#的区别解析

《MyBatis中$与#的区别解析》文章浏览阅读314次,点赞4次,收藏6次。MyBatis使用#{}作为参数占位符时,会创建预处理语句(PreparedStatement),并将参数值作为预处理语句... 目录一、介绍二、sql注入风险实例一、介绍#(井号):MyBATis使用#{}作为参数占位符时,会

PostgreSQL的扩展dict_int应用案例解析

《PostgreSQL的扩展dict_int应用案例解析》dict_int扩展为PostgreSQL提供了专业的整数文本处理能力,特别适合需要精确处理数字内容的搜索场景,本文给大家介绍PostgreS... 目录PostgreSQL的扩展dict_int一、扩展概述二、核心功能三、安装与启用四、字典配置方法

Go语言中make和new的区别及说明

《Go语言中make和new的区别及说明》:本文主要介绍Go语言中make和new的区别及说明,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1 概述2 new 函数2.1 功能2.2 语法2.3 初始化案例3 make 函数3.1 功能3.2 语法3.3 初始化

IDEA中新建/切换Git分支的实现步骤

《IDEA中新建/切换Git分支的实现步骤》本文主要介绍了IDEA中新建/切换Git分支的实现步骤,通过菜单创建新分支并选择是否切换,创建后在Git详情或右键Checkout中切换分支,感兴趣的可以了... 前提:项目已被Git托管1、点击上方栏Git->NewBrancjsh...2、输入新的分支的

深度解析Java DTO(最新推荐)

《深度解析JavaDTO(最新推荐)》DTO(DataTransferObject)是一种用于在不同层(如Controller层、Service层)之间传输数据的对象设计模式,其核心目的是封装数据,... 目录一、什么是DTO?DTO的核心特点:二、为什么需要DTO?(对比Entity)三、实际应用场景解析

深度解析Java项目中包和包之间的联系

《深度解析Java项目中包和包之间的联系》文章浏览阅读850次,点赞13次,收藏8次。本文详细介绍了Java分层架构中的几个关键包:DTO、Controller、Service和Mapper。_jav... 目录前言一、各大包1.DTO1.1、DTO的核心用途1.2. DTO与实体类(Entity)的区别1

Java中的雪花算法Snowflake解析与实践技巧

《Java中的雪花算法Snowflake解析与实践技巧》本文解析了雪花算法的原理、Java实现及生产实践,涵盖ID结构、位运算技巧、时钟回拨处理、WorkerId分配等关键点,并探讨了百度UidGen... 目录一、雪花算法核心原理1.1 算法起源1.2 ID结构详解1.3 核心特性二、Java实现解析2.

使用Python绘制3D堆叠条形图全解析

《使用Python绘制3D堆叠条形图全解析》在数据可视化的工具箱里,3D图表总能带来眼前一亮的效果,本文就来和大家聊聊如何使用Python实现绘制3D堆叠条形图,感兴趣的小伙伴可以了解下... 目录为什么选择 3D 堆叠条形图代码实现:从数据到 3D 世界的搭建核心代码逐行解析细节优化应用场景:3D 堆叠图

深度解析Python装饰器常见用法与进阶技巧

《深度解析Python装饰器常见用法与进阶技巧》Python装饰器(Decorator)是提升代码可读性与复用性的强大工具,本文将深入解析Python装饰器的原理,常见用法,进阶技巧与最佳实践,希望可... 目录装饰器的基本原理函数装饰器的常见用法带参数的装饰器类装饰器与方法装饰器装饰器的嵌套与组合进阶技巧