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

相关文章

深度解析Python中递归下降解析器的原理与实现

《深度解析Python中递归下降解析器的原理与实现》在编译器设计、配置文件处理和数据转换领域,递归下降解析器是最常用且最直观的解析技术,本文将详细介绍递归下降解析器的原理与实现,感兴趣的小伙伴可以跟随... 目录引言:解析器的核心价值一、递归下降解析器基础1.1 核心概念解析1.2 基本架构二、简单算术表达

深度解析Java @Serial 注解及常见错误案例

《深度解析Java@Serial注解及常见错误案例》Java14引入@Serial注解,用于编译时校验序列化成员,替代传统方式解决运行时错误,适用于Serializable类的方法/字段,需注意签... 目录Java @Serial 注解深度解析1. 注解本质2. 核心作用(1) 主要用途(2) 适用位置3

Java MCP 的鉴权深度解析

《JavaMCP的鉴权深度解析》文章介绍JavaMCP鉴权的实现方式,指出客户端可通过queryString、header或env传递鉴权信息,服务器端支持工具单独鉴权、过滤器集中鉴权及启动时鉴权... 目录一、MCP Client 侧(负责传递,比较简单)(1)常见的 mcpServers json 配置

从原理到实战解析Java Stream 的并行流性能优化

《从原理到实战解析JavaStream的并行流性能优化》本文给大家介绍JavaStream的并行流性能优化:从原理到实战的全攻略,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的... 目录一、并行流的核心原理与适用场景二、性能优化的核心策略1. 合理设置并行度:打破默认阈值2. 避免装箱

Maven中生命周期深度解析与实战指南

《Maven中生命周期深度解析与实战指南》这篇文章主要为大家详细介绍了Maven生命周期实战指南,包含核心概念、阶段详解、SpringBoot特化场景及企业级实践建议,希望对大家有一定的帮助... 目录一、Maven 生命周期哲学二、default生命周期核心阶段详解(高频使用)三、clean生命周期核心阶

Android协程高级用法大全

《Android协程高级用法大全》这篇文章给大家介绍Android协程高级用法大全,本文结合实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友跟随小编一起学习吧... 目录1️⃣ 协程作用域(CoroutineScope)与生命周期绑定Activity/Fragment 中手

深入解析C++ 中std::map内存管理

《深入解析C++中std::map内存管理》文章详解C++std::map内存管理,指出clear()仅删除元素可能不释放底层内存,建议用swap()与空map交换以彻底释放,针对指针类型需手动de... 目录1️、基本清空std::map2️、使用 swap 彻底释放内存3️、map 中存储指针类型的对象

Java Scanner类解析与实战教程

《JavaScanner类解析与实战教程》JavaScanner类(java.util包)是文本输入解析工具,支持基本类型和字符串读取,基于Readable接口与正则分隔符实现,适用于控制台、文件输... 目录一、核心设计与工作原理1.底层依赖2.解析机制A.核心逻辑基于分隔符(delimiter)和模式匹

Java+AI驱动实现PDF文件数据提取与解析

《Java+AI驱动实现PDF文件数据提取与解析》本文将和大家分享一套基于AI的体检报告智能评估方案,详细介绍从PDF上传、内容提取到AI分析、数据存储的全流程自动化实现方法,感兴趣的可以了解下... 目录一、核心流程:从上传到评估的完整链路二、第一步:解析 PDF,提取体检报告内容1. 引入依赖2. 封装

深度解析Python yfinance的核心功能和高级用法

《深度解析Pythonyfinance的核心功能和高级用法》yfinance是一个功能强大且易于使用的Python库,用于从YahooFinance获取金融数据,本教程将深入探讨yfinance的核... 目录yfinance 深度解析教程 (python)1. 简介与安装1.1 什么是 yfinance?