libco协程库上下文切换原理详解

微信 libco 协程库原理剖析 作者:alexzmzheng同 Go 语言一样,libco 也是提供了同步风格编程模式,同时还能保证系统的高并发能力,本文主要剖析 libco 中的协程原理。简介libco 是微信后台大规... 阅读详情

缘起

libco 协程库在单个线程中实现了多个协程的创建和切换。按照我们通常的编程思路,单个线程中的程序执行流程通常是顺序的,调用函数同样也是 “调用——返回”,每次都是从函数的入口处开始执行。而libco 中的协程却实现了函数执行到一半时,切出此协程,之后可以回到函数切出的位置继续执行,即函数的执行可以被“拦腰斩断”,这种在函数任意位置 “切出——恢复” 的功能是如何实现的呢? 本文从libco 代码层面对协程的切换进行了剖析,希望能让初次接触 libco 的同学能快速了解其背后的运行机理。

函数调用与协程切换的区别

下面的程序是我们常规调用函数的方法:

void A() {
   cout << 1 << " ";
   cout << 2 << " ";
   cout << 3 << " ";
}

void B() {
   cout << “x” << " ";
   cout << “y” << " ";
   cout << “z” << " ";
}

int main(void) {
  A();
  B();
}

在单线程中,上述函数的输出为:

1 2 3 x y z 
  • 1

如果我们用 libco 库将上面程序改造一下:

void A() {
   cout << 1 << " ";
   cout << 2 << " ";
   co_yield_ct();  // 切出到主协程
   cout << 3 << " ";
}

void B() {
   cout << “x” << " ";
   co_yield_ct();  // 切出到主协程
   cout << “y” << " ";
   cout << “z” << " ";
}

int main(void) {
  ...  // 主协程
  co_resume(A);  // 启动协程 A
  co_resume(B);  // 启动协程 B
  co_resume(A);  // 从协程 A 切出处继续执行
  co_resume(B);  // 从协程 B 切出处继续执行
}

同样在单线程中,改造后的程序输出如下:

1 2 x 3 y z
  •  

可以看出,切出操作是由 co_yield_ct() 函数实现的,而协程的启动和恢复是由 co_resume 实现的。函数 A() 和 B() 并不是一个执行完才执行另一个,而是产生了 “交叉执行“ 的效果,那么,在单个线程中,这种 ”交叉执行“,是如何实现的呢?

Read the f**king source code!

Talk is cheap, show me code.

下面我们就深入 libco 的代码来看一下,协程的切换是如何实现的。通过分析代码看到,无论是 co_yield_ct() 还是 co_resume,在协程切出和恢复时,都调用了同一个函数co_swap,在这个函数中调用了 coctx_swap 来实现协程的切换,这一函数的原型是:

void coctx_swap( coctx_t *,coctx_t* ) asm("coctx_swap");
  •  

两个参数都是 coctx_t *指针类型,其中第一个参数表示要切出的协程,第二个参数表示切出后要进入的协程。

在上篇文章 “x86-64 下函数调用及栈帧原理” 中已经指出,调用子函数时,父函数会把两个调用参数放入了寄存器中,并且把返回地址压入了栈中。即在进入 coctx_swap 时,第一个参数值已经放到了 %rdi 寄存器中,第二个参数值已经放到了 %rsi 寄存器中,并且栈指针 %rsp 指向的位置即栈顶中存储的是父函数的返回地址。进入 coctx_swap 后,堆栈的状态如下:
这里写图片描述

由于coctx_swap 是在 co_swap() 函数中调用的,下面所提及的协程的返回地址就是 co_swap() 中调用 coctx_swap() 之后下一条指令的地址:

void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co) {
     ....
    // 从本协程切出
    coctx_swap(&(curr->ctx),&(pending_co->ctx) );

    // 此处是返回地址,即协程恢复时开始执行的位置
    stCoRoutineEnv_t* curr_env = co_get_curr_thread_env();
    ....
    }

coctx_swap 函数是用汇编实现的,我们这里只关注 x86-64 相关的部分,其代码如下:

coctx_swap:
   leaq 8(%rsp),%rax
   leaq 112(%rdi),%rsp
   pushq %rax
   pushq %rbx
   pushq %rcx
   pushq %rdx

   pushq -8(%rax) //ret func addr

   pushq %rsi
   pushq %rdi
   pushq %rbp
   pushq %r8
   pushq %r9
   pushq %r12
   pushq %r13
   pushq %r14
   pushq %r15

   movq %rsi, %rsp
   popq %r15
   popq %r14
   popq %r13
   popq %r12
   popq %r9
   popq %r8
   popq %rbp
   popq %rdi
   popq %rsi
   popq %rax //ret func addr
   popq %rdx
   popq %rcx
   popq %rbx
   popq %rsp
   pushq %rax

   xorl %eax, %eax
   ret

可以看出,coctx_swap 中并未像常规被调用函数一样创立新的栈帧。先看前两条语句:

   leaq 8(%rsp),%rax
   leaq 112(%rdi),%rsp

leaq 用于把其第一个参数的值赋值给第二个寄存器参数。第一条语句用来把 8(%rsp) 的本身的值存入到 %rax 中,注意这里使用的并不是 8(%rsp) 指向的值,而是把 8(%rsp) 表示的地址赋值给了 %rax。这一地址是父函数栈帧中除返回地址外栈帧顶的位置。

在第二条语句 leaq 112(%rdi), %rsp 中,%rdi 存放的是coctx_swap 第一个参数的值,这一参数是指向 coctx_t 类型的指针,表示当前要切出的协程,这一类型的定义如下:

struct coctx_t {
    void *regs[ 14 ]; 
    size_t ss_size;
    char *ss_sp;

};

因而 112(%rdi) 表示的就是第一个协程的 coctx_t 中 regs[14] 数组的下一个64位地址。而接下来的语句:

   pushq %rax   
   pushq %rbx
   pushq %rcx
   pushq %rdx
   pushq -8(%rax) //ret func addr
   pushq %rsi
   pushq %rdi
   pushq %rbp
   pushq %r8
   pushq %r9
   pushq %r12
   pushq %r13
   pushq %r14
   pushq %r15

第一条语句 pushq %rax 用于把 %rax 的值放入到 regs[13] 中,resg[13] 用来存储第一个协程的 %rsp 的值。这时 %rax 中的值是第一个协程 coctx_swap 父函数栈帧除返回地址外栈帧顶的地址。由于 regs[] 中有单独的元素存储返回地址,栈中再保存返回地址是无意义的,因而把父栈帧中除返回地址外的栈帧顶作为要保存的 %rsp 值是合理的。当协程恢复时,把保存的 regs[13] 的值赋值给 %rsp 即可恢复本协程 coctx_swap 父函数堆栈指针的位置。第一条语句之后的语句就是用pushq 把各CPU 寄存器的值依次从 regs 尾部向前压入。即通过调整%rsp 把 regs[14] 当作堆栈,然后利用 pushq 把寄存器的值和返回地址存储到 regs[14] 整个数组中。regs[14] 数组中各元素与其要存储的寄存器对应关系如下:

//-------------
// 64 bit
//low | regs[0]: r15 |
//    | regs[1]: r14 |
//    | regs[2]: r13 |
//    | regs[3]: r12 |
//    | regs[4]: r9  |
//    | regs[5]: r8  | 
//    | regs[6]: rbp |
//    | regs[7]: rdi |
//    | regs[8]: rsi |
//    | regs[9]: ret |  //ret func addr, 对应 rax
//    | regs[10]: rdx |
//    | regs[11]: rcx | 
//    | regs[12]: rbx |
//hig | regs[13]: rsp |

接下来的汇编语句:

   movq %rsi, %rsp
   popq %r15
   popq %r14
   popq %r13
   popq %r12
   popq %r9
   popq %r8
   popq %rbp
   popq %rdi
   popq %rsi
   popq %rax //ret func addr
   popq %rdx
   popq %rcx
   popq %rbx
   popq %rsp   

这里用的方法还是通过改变%rsp 的值,把某块内存当作栈来使用。第一句 movq %rsi, %rsp 就是让%rsp 指向 coctx_swap 第二个参数,这一参数表示要进入的协程。而第二个参数也是coctx_t 类型的指针,即执行完 movq 语句后,%rsp 指向了第二个参数 coctx_t 中 regs[0],而之后的pop 语句就是用 regs[0-13] 中的值填充cpu 的寄存器,这里需要注意的是popq 会使得 %rsp 的值增加而不是减少,这一点保证了会从 regs[0] 到regs[13] 依次弹出到 cpu 寄存器中。在执行完最后一句 popq %rsp 后,%rsp 已经指向了新协程要恢复的栈指针(即新协程之前调用 coctx_swap 时父函数的栈帧顶指针),由于每个协程都有一个自己的栈空间,可以认为这一语句使得%rsp 指向了要进入协程的栈空间。

coctx_swap 中最后三条语句如下:

   pushq %rax
   xorl %eax, %eax
   ret

pushq %rax 用来把 %rax 的值压入到新协程的栈中,这时 %rax 是要进入的目标协程的返回地址,即要恢复的执行点。然后用 xorl 把 %rax 低32位清0以实现地址对齐。最后ret 语句用来弹出栈的内容,并跳转到弹出的内容表示的地址处,而弹出的内容正好是上面 pushq %rax 时压入的 %rax 的值,即之前保存的此协程的返回地址。即最后这三条语句实现了转移到新协程返回地址处执行,从而完成了两个协程的切换。可以看出,这里通过调整%rsp 的值来恢复新协程的栈,并利用了 ret 语句来实现修改指令寄存器 %rip 的目的,通过修改 %rip 来实现程序运行逻辑跳转。注意%rip 的值不能直接修改,只能通过 call 或 ret 之类的指令来间接修改。

整体上看来,协程的切换其实就是cpu 寄存器内容特别是%rip 和 %rsp 的写入和恢复,因为cpu 的寄存器决定了程序从哪里执行(%rip) 和使用哪个地址作为堆栈 (%rsp)。寄存器的写入和恢复如下图所示:

这里写图片描述

执行完上图的流程,就将之前 cpu 寄存器的值保存到了协程A 的 regs[14] 中,而将协程B regs[14] 的内容写入到了寄存器中,从而使执行逻辑跳转到了 B 协程 regs[14] 中保存的返回地址处开始执行,即实现了协程的切换(从A 协程切换到了B协程执行)。

结语

为实现单线程中协程的切换,libco 使用汇编直接读写了 cpu 的寄存器。由于通常我们在高级语言层面很少接触上下文切换的情形,因而会觉得在单线程中切换上下文的方法会十分复杂,但当我们对代码抽丝剥茧后,发现其实现机理也是很容易理解的。从libco 上下文切换中可以看出,用汇编与 cpu 硬件寄存器配合竟然可以设计出如此神奇的功能,不得不惊叹于 cpu 硬件设计的精妙。

libco 库的说明中提及这种上下文切换的方法取自 glibc,看来基础库中隐藏了不少 “屠龙之技”。

看来,想要提高编程技能,无他,Read the f**king source code !

The End.

避坑指南:Docker部署PaddleOCR-VL服务时,关于flash-attn安装、端口映射和模型加载的那些坑 本文详细记录了Docker部署PaddleOCR-VL服务时遇到的常见问题及解决方案,包括flash-attn安装、GPU兼容性检查、容器生命周期管理、模型加载优化等关键环节。特别针对不同GPU架构提供了flash-attn的安装策略,并分享了端口映射、配置文件处理等实用技巧,帮助开发者高效避坑。 阅读详情

相关推荐

RT-Thread代码启动过程——以及 $Sub$ $main 与 $Super$ $main

我们找到系统复位的地方,可以往下单步跟踪。 ①从系统初始化开始执行,将函数地址赋给R0寄存器,跳转到R0地址执行并返回此处(BLX是带链接的跳转,即带返回的跳转)。 ②将main函数地址给R0,将函数地址赋给R0,跳转到R0地址执行,不返回(BX是跳转,不返回)。 ③跳转到了$Sub$$main。 【注:在 __CC_ARM 编译器环境下,使用了$Sub$$ 与$Super$ $ 的...

yang1111111112的博客 1万+

协程和线程的上下文切换详解

什么是上下文切换?上下文切换的时机?CPU通过分配时间片来执行任务,当一个任务的时间片用完,就会切换到另一个任务。在切换之前会保存上一个任务的状态,当下次再切换到该任务,就会加载这个状态。——任务从保存到再加载的过程就是一次上下文切换。切出线程的时间片用完有一个比切出线程优先级更高的线程需要被运行虚拟机的垃圾回收动作。...

qq_48826531的博客 1605

欧姆龙PLC与fanuc发那科机器人进行Ethrenet ip通信测试

近期在做发那科FANUC的通讯,会吧CC-LINK、Profibus-DP、Profinet I/O、DeviceNet、EtherNet/IP这些常用通讯做成文档发上来,大家喜欢的话可以关注 注:做对应的通讯,在机器人控制柜都需要安装对应的板卡以及软件功能 截图中的程序设置和机器人EDS插件在下面链接 https://download.youkuaiyun.com/download/gojava/13770799 PLC为主站 机器人为从站 一、硬件部分 欧姆龙PLC ...

七兔自习室 1万+

libco协程切换原理详解

一 备用知识 之前不止一次的把libco的源码下下来看,但是每次看到协程切换汇编部分都放弃了,大学那点仅有的汇编基础也忘的一干二净,更不堪的是百度搜到的libco相关文章也看不懂,最近决定静下心来看一些相关的东西,终于像啃骨头一样把这块东西搞明白了。在网下阅读之前请先务必看懂下面的内容。 第一部分 汇编基础 1 CSAPP阅读笔记-汇编语言初探(数据传送类指令):CSAPP阅读笔记-汇编语言初探(数据传送类指令) 2 CSAPP阅读笔记-汇编语言初探(算术和逻辑操作类指令)...

D_Guco的专栏 2425

ADS软件操作(10)——DAC控件使用

该控件可以用于为元件定义随频率变换的参数,如放大器的最佳负载阻抗或者源阻抗,需要双频匹配时的优化等,下图是ADS中该控件的模型。

Zhuanzhuyan的博客 4402

win11本地部署openclaw实操第1集

OpenClaw 是一款开源、本地优先、可自托管的 AI 智能体(AI Agent)网关 / 自动化框架,核心是让 AI 从 “只聊天、给建议” 变成 “能在你电脑上真做事” 的数字助手。

weixin_44626085的博客 7128

汇编基础知识

这是1.4中探讨variable的scope和lifetime的代码,这里我将用它来做基础的汇编知识学习global fun声明fun函数为全局符号,意味着它在其他文件中也可以被引用。type fun, @function: 指定fun的类型为函数。fun:: 函数fun的开始。LFBO:: 标签,通常用于调试信息。cfi_startproc: 调用帧信息的开始。pushq %rbp: 将基指针rbp压入栈。

qq_62893047的博客 1384

第1.2章 机器人SLAM计算机基础总结(二):VSCode教程及 AI等效率插件推荐

如何高效编写代码?怎样快速调试程序?**别担心,今天就给大家带来一款超强大的编程神器 ——VSCode,以及能让编程之路 “一马平川” 的实用插件推荐,无论你是新手小白还是资深大神,都能从中受益!

行知SLAM 1862

协程5 --- 栈切换

数组名 得到的是数组指针,数组指针的步长为整个数组的长度。看实现之前可以先看一下另一个博客特殊的栈溢出例子。用堆模拟栈的时候,堆栈的生长方向是相反的。为什么要设置成只有一个长度的数组?sizeof(数组名) 为数组长度。作为函数参数退化为指针。

c++小白的博客 531

[QMT量化交易小白入门]-七十一、十年回测ETF动量策略,超额年化收益率为10.6%

本专栏主要是介绍QMT的基础用法,常见函数,写策略的方法,也会分享一些量化交易的思路,大概会写100篇左右。QMT的相关资料较少,在使用过程中不断的摸索,遇到了一些问题,记录下来和大家一起沟通,共同进步。

木头大左的博客 1857

毕设帮扶:从0到1搭建基于YOLOv5的打架斗殴检测系统——助你搞定深度学习毕设

做完这个毕设,你已经掌握了从数据准备到模型部署、界面开发的全流程。把模型部署到边缘设备(如安防摄像头的嵌入式系统)上,实现本地化实时检测与报警;加入行为严重程度分析(如轻微推搡 vs 严重斗殴)、涉事人员数量统计,让系统从“检测”升级为“智能安防决策”;尝试融合声音、红外等多模态数据,打造全方位的暴力事件监测系统。按照这个教程一步步做,你的毕设肯定能做得很扎实。要是过程中遇到啥问题,别担心,慢慢梳理或者咱们一起聊聊,一定能把这个毕设做成你满意的作品。

LXY_LOVE36的博客 122

st(state-threads) coroutine和stack分析

st(state-threads)https://github.com/winlinvip/state-threads 以及基于st的RTMP/HLS服务器:https://github.com/winlinvip/simple-rtmp-server st是实现了coroutine的一套机制,即用户态线程,或者叫做协程。将epoll(async,nonblocking socket

OSSRS 8397

复制幻灯片到另一个PPT编程

请确保将源PPT文件和目标PPT文件的文件名修改为你实际使用的文件名。运行上述代码后,目标PPT将包含源PPT中的所有幻灯片和形状。这里假设源PPT文件名为"source.pptx",目标PPT文件名为"target.pptx"。方法将每个幻灯片的布局添加到目标PPT中。是一个强大的库,可以用于创建、修改和操作PPT文件。要将一个PPT中的幻灯片复制到另一个PPT,可以使用Python的。这段代码遍历源PPT的每个幻灯片,并使用。步骤 3: 打开源PPT和目标PPT。这将保存修改后的目标PPT文件。

ByteSparkX的博客 1322

C++ 开源协程库 libco——原理及应用

1 导论 使用 C++ 来编写高性能的网络服务器程序,从来都不是件很容易的事情。在没有 应用任何网络框架,从 epoll/kqueue 直接码起的时候尤其如此。即便使用 libevent, libev 这样事件驱动的网络框架去构建你的服务,程序结构依然不会很简单。为何会这样?因 为这类框架提供的都是非阻塞式的、异步的编程接口,异步的编程方式,这需要思维方 式的转变。为什么 golang 近几年能够...

cool grass's blog 5333

LinePhone3.2.7(android端最新版本) sip客户端

SIP客户端For android:LinePhone3.2.7(最新版本),官网地址:http://www.linphone.org/

Camera2Demo

在Camera2 的基础上进行封装,使得Camera2一堆初始化逻辑与Activity分离,降低耦合度,完成预览拍照并保存的操作

协程实现原理——以libco为例

协程是个很重要的概念,对于向我一样的在校生而言,或者对进程和线程比较熟悉,对协程一般不太理解,协程也确实不好理解,本篇文章希望以通俗易懂的方式讲述协程,并且以著名的libco为例,看看核心的协程实现代码。协程的概念非常不好理解,虽然网上文章众多,但大多数都给人一种云里雾里的感觉,对协程的理解也有一些不同的观点,我个人认为协程就是一种可以“暂停和恢复执行”的特殊函数。

新人博主,多多关照 1678

温度PID控制PLC程序

温度PID稳定控制程序,适用于欧姆龙PLC,对其他PLC也具有很大参考价值

libco协程库原理解析与应用

       最近在准备一个libco协程库原理简析与应用的分享,顺便就整理下写个博客,一方面加深下自己对协程库的理解,另一方面也希望能对想了解协程的学者有所帮助。废话不多说了,言归正传吧。        想去剖析libco协程库的实现原理,首先我们要了解下什么是协程。维基百科上给协程的定义是协程(coroutine)又称微线程,是一个无优先级的子程序(函数)调度组件,允许子程序在特定的地方挂起和...

hhyjiayou的博客 3632

ET2012新版免狗免安装绿色软件

ET2012新版免狗免安祖昂绿色软件。,新界面,新视觉,

微信libco协程库底层实现原理深度解析

libco通过非对称协程模型、用户态调度、Hook机制等精妙设计,实现了高性能的协程库。化繁为简:将复杂的异步编程转化为简单的同步模式以小博大:用较小的内存和CPU开销支撑海量并发渐进增强:在不修改现有代码的基础上提升性能协程不是银弹,但在合适的场景下,它确实能够带来质的飞跃。随着云原生和微服务架构的普及,协程技术必将在高并发编程领域发挥更大的作用!🔗相关链接libco GitHub仓库微信技术博客📝本文作者:专注于高性能服务器开发,欢迎交流讨论!

苦哈哈的 C++ 程序员,写这个技术博客不是为了装逼,也不是为了立人设,是为了收割流量 905

C++ libco 介绍与应用

文章目录导论准备知识协程的背景libco 使用简介libco 协程关键数据结构及其关系libco 协程的生命周期事件驱动与协程调度libco 总结 导论   使用 C++ 来编写高性能的网络服务器程序,从来都不是一件很容易的事。   在没有应用任何网络框架的情况下,从 epoll/kqueue 直接码起的时候更是如此。即便使用 libevent、libev 这样事件驱动的网络框架去构建你的服务,程序结构依然不会简单。   为何会这样?因为这类框架提供的都是非阻塞式的、异步的编程方式,这需要思维方式的转变。

qq_41033011的博客 6560

Tecent libco C++协程库初探

libco生产者消费者模型demo实现和一点对协程的思考。

ZYunfei的博客 1858

Libco协程切换的原理

Libco协程切换 声明:下面的都是参考Libco 协程栈的切换理解 函数调用的原理 linux 程序内存布局 传统linux程序(32bit)拥有4G的虚拟内存区域,高1G的区域供内核使用,剩余的3G内存供程序使用。按段划分,主要分程序段(text segement)、数据段、BSS段。BSS段用于未初始化的静态变量的初始化(0值初始化)。栈从高到低地址增长。堆从低到高增长。栈和堆的这两种不同的...

Tattoo的博客 1624

libco 协程的理解

学习libco,做了一些代码解读,记录在此 1 线程上下文 线程上下文实际是一个协程栈,维护协程之间的执行序列 同一个线程中的所有协程,共用一个线程上下文 线程上下文在 第一次被使用到的时候进行创建 // 协程公共的 线程上下文 struct stCoRoutineEnv_t { stCoRoutine_t *pCallStack[ 128 ]; // 协程栈,保存同

LIncoLN的博客 1523

libco源码解析(1) 协程运行与基本结构

引言 这是计划的一系列文章的第一篇, 作为第一篇,我们要先来看看libco的协程如何使用,我们直接拿libco中的example_cond.cpp来对协程的运行过程做一个简单的解释。 #include <unistd.h> #include <stdio.h> #include <stdlib.h> #include <queue> #include "co_routine.h" using namespace std; struct stTask_t {

李兆龙的博客 3856
上一篇: x86-64 下函数调用及栈帧原理
下一篇: Linux 进程热升级 共享库的动态替换
废言Pro
博客等级 码龄16年 1093粉丝 142原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值