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

相关文章

MySQL中FIND_IN_SET函数与INSTR函数用法解析

《MySQL中FIND_IN_SET函数与INSTR函数用法解析》:本文主要介绍MySQL中FIND_IN_SET函数与INSTR函数用法解析,本文通过实例代码给大家介绍的非常详细,感兴趣的朋友一... 目录一、功能定义与语法1、FIND_IN_SET函数2、INSTR函数二、本质区别对比三、实际场景案例分

Pytest多环境切换的常见方法介绍

《Pytest多环境切换的常见方法介绍》Pytest作为自动化测试的主力框架,如何实现本地、测试、预发、生产环境的灵活切换,本文总结了通过pytest框架实现自由环境切换的几种方法,大家可以根据需要进... 目录1.pytest-base-url2.hooks函数3.yml和fixture结论你是否也遇到过

Spring Boot项目中结合MyBatis实现MySQL的自动主从切换功能

《SpringBoot项目中结合MyBatis实现MySQL的自动主从切换功能》:本文主要介绍SpringBoot项目中结合MyBatis实现MySQL的自动主从切换功能,本文分步骤给大家介绍的... 目录原理解析1. mysql主从复制(Master-Slave Replication)2. 读写分离3.

Java图片压缩三种高效压缩方案详细解析

《Java图片压缩三种高效压缩方案详细解析》图片压缩通常涉及减少图片的尺寸缩放、调整图片的质量(针对JPEG、PNG等)、使用特定的算法来减少图片的数据量等,:本文主要介绍Java图片压缩三种高效... 目录一、基于OpenCV的智能尺寸压缩技术亮点:适用场景:二、JPEG质量参数压缩关键技术:压缩效果对比

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++

关于WebSocket协议状态码解析

《关于WebSocket协议状态码解析》:本文主要介绍关于WebSocket协议状态码的使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录WebSocket协议状态码解析1. 引言2. WebSocket协议状态码概述3. WebSocket协议状态码详解3

CSS Padding 和 Margin 区别全解析

《CSSPadding和Margin区别全解析》CSS中的padding和margin是两个非常基础且重要的属性,它们用于控制元素周围的空白区域,本文将详细介绍padding和... 目录css Padding 和 Margin 全解析1. Padding: 内边距2. Margin: 外边距3. Padd

Oracle数据库常见字段类型大全以及超详细解析

《Oracle数据库常见字段类型大全以及超详细解析》在Oracle数据库中查询特定表的字段个数通常需要使用SQL语句来完成,:本文主要介绍Oracle数据库常见字段类型大全以及超详细解析,文中通过... 目录前言一、字符类型(Character)1、CHAR:定长字符数据类型2、VARCHAR2:变长字符数

使用Jackson进行JSON生成与解析的新手指南

《使用Jackson进行JSON生成与解析的新手指南》这篇文章主要为大家详细介绍了如何使用Jackson进行JSON生成与解析处理,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1. 核心依赖2. 基础用法2.1 对象转 jsON(序列化)2.2 JSON 转对象(反序列化)3.

Springboot @Autowired和@Resource的区别解析

《Springboot@Autowired和@Resource的区别解析》@Resource是JDK提供的注解,只是Spring在实现上提供了这个注解的功能支持,本文给大家介绍Springboot@... 目录【一】定义【1】@Autowired【2】@Resource【二】区别【1】包含的属性不同【2】@