缘起

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 

如果我们用 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);  // 启动协程 Aco_resume(B);  // 启动协程 Bco_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),%raxleaq 112(%rdi),%rsppushq %raxpushq %rbxpushq %rcxpushq %rdxpushq -8(%rax) //ret func addrpushq %rsipushq %rdipushq %rbppushq %r8pushq %r9pushq %r12pushq %r13pushq %r14pushq %r15movq %rsi, %rsppopq %r15popq %r14popq %r13popq %r12popq %r9popq %r8popq %rbppopq %rdipopq %rsipopq %rax //ret func addrpopq %rdxpopq %rcxpopq %rbxpopq %rsppushq %raxxorl %eax, %eaxret

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

   leaq 8(%rsp),%raxleaq 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 %rbxpushq %rcxpushq %rdxpushq -8(%rax) //ret func addrpushq %rsipushq %rdipushq %rbppushq %r8pushq %r9pushq %r12pushq %r13pushq %r14pushq %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, %rsppopq %r15popq %r14popq %r13popq %r12popq %r9popq %r8popq %rbppopq %rdipopq %rsipopq %rax //ret func addrpopq %rdxpopq %rcxpopq %rbxpopq %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 %raxxorl %eax, %eaxret

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.

libco协程库上下文切换原理详解相关推荐

  1. Android 协程使用到原理详解

    协程是什么 协程是我们在 Android上进行异步编程的推荐解决方案之一,通过挂起和恢复让状态机状态流转实现把层层嵌套的回调代码变成像同步代码那样直观.简洁,协程的出现很好的避免了回调地狱的出现. 所 ...

  2. libco协程库源码解读

    2019独角兽企业重金招聘Python工程师标准>>> 协程,又被称为用户级线程,是在应用层被调度,可以减少因为调用系统调用而阻塞的线程切换的时间.目前有很多协程的实现,由于微信内部 ...

  3. 微信 libco 协程库原理剖析

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

  4. isql 测试mysql连接_[libco] 协程库学习,测试连接 mysql

    历史原因,一直使用 libev 作为服务底层:异步框架虽然性能比较高,但新人学习和使用门槛非常高,而且串行的逻辑被打散为状态机,这也会严重影响生产效率. 用同步方式实现异步功能,既保证了异步性能优势, ...

  5. python中图片绘制和输出相关库的原理详解

    Python在图片绘制和输出方面的发展历史可以追溯到20世纪90年代,当时的主要库是Python Imaging Library (PIL),用于处理图像文件和生成图像.PIL是Python中最早的图 ...

  6. linux c++11高性能协程库netco

    目录 一.开源协程库调研 1.golang语言自带协程 2.云风的coroutine协程库 3.腾讯的libco协程库 4.魅族的libgo协程库 二.netco协程库概述 三.netco的实现 1. ...

  7. 在C语言中实现协程库(一)----------协程切换原理详解

    从这篇文章开始,我将一点一点详细介绍如何在c语言中实现协程库.并对其中涉及到的技术进行详细的解释. 感兴趣的小伙伴欢迎一起参与 代码地址 协程切换原理 使用glibc中<ucontext.h&g ...

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

    1 导论 使用 C++ 来编写高性能的网络服务器程序,从来都不是件很容易的事情.在没有应用任何网络框架,从 epoll/kqueue 直接码起的时候尤其如此.即便使用 libevent, libev这 ...

  9. C/C++协程库libco:微信怎样漂亮地完成异步化改造

    如今,微信拥有月活跃用户8亿. 不可否认,当今的微信后台拥有着强大的并发能力. 不过, 正如罗马并非一日建成:微信的技术也曾经略显稚嫩. 微信诞生于2011年1月,当年用户规模为0.1亿左右:2013 ...

最新文章

  1. 服务器技术综述(二)
  2. 关于dbutils中QueryRunner看批量删除语句batch
  3. Leetcode 145. 二叉树的后序遍历 (每日一题 20210930)
  4. RabbitMQ——自动退出的解决方案
  5. Android菜鸟的成长笔记(28)——Google官方对Andoird 2.x提供的ActionBar支持
  6. C/C++的readdir和readdir_r函数(遍历目录)
  7. vue2.0中watch总结:普通监听和深度监听
  8. 检查gzip是否起效
  9. 自然数从1到n之间,有多少个数字含有1
  10. kafka偏移量保存到mysql里_用java代码手动控制kafkaconsumer偏移量
  11. 移动手机病毒的进化历程
  12. foobar2000播放dff格式音乐的解决办法
  13. wex5 新建mysql数据库_wex5新增数据库
  14. 美术生都要膜拜的AI,照片迅速被画成艺术画
  15. 公文国标字体(仿宋GB_2312和楷体GB_2312)
  16. 基于springboot+vue的幼儿园管理系统 elementui
  17. vue+vite+element-plus修改全局主题颜色
  18. 验证码ocrking接口
  19. 安卓滤镜君LR调色大师v2.2.1
  20. 八部委联合发文:规范供应链金融,支持使用电子签章

热门文章

  1. chrome html5 mp4,HTML5 Video Chrome - ffmpeg - mp4 working in all but Chrome
  2. 程序员再也不担心请不到假了!
  3. Nature拳头综述(IF=71)| 上海科技大学钟超等人系统介绍合成生物学及未来潜在应用...
  4. 免费直播 | 宏基因组云讲堂第二期由刘永鑫博士主持,特邀王金锋副研究员分享“用时序微生物组数据重现生物膜装配的动态过程”...
  5. 单细胞测序分析之小技巧之for循环批量处理数据和出图
  6. Blizzard Transitions for Mac - 动态风雪过渡效果FCPX转场
  7. 多功能mac代码编辑神器coderunner 4 比Xcode都强大
  8. 信息学奥赛一本通 提高篇 第一部分 基础算法 第2章 二分与三分
  9. 1168:大整数加法--2022.01.22 AC
  10. [9] ADB 查看设备信息