# 简述

大约在半年以前,我曾经了解过协程的相关实现,也看过腾讯后台开源的协程库`libco`,对其中实现协程相关的汇编有很深的印象(`libco`适配的是 x86 平台)。接受了这样的思想,我在自己的毕业设计中,写出一套单片机汇编实现的协程。也自此之后,就有种念头在 iOS 端实现相关的代码。手机端使用的 CPU 跟单片机相差甚远,虽然我用的单片机也是 ARM 平台的,但和手机端 CPU 相比两者差距确很大,内核微架构差距太大,无从入手。后续突然想起可以使用`setjmp`和`longjmp`这两个函数间跳转函数,我为何不反汇编它,根据其汇编,来实现协程呢?

最后,我实现了这个想法,先把 Demo 放上来:https://github.com/suancaiAmour/CoroutineDemo。读者可以根据 Demo 和本篇文章一起了解相关内容。

# 协程的意义

协程早在 60 年代提出的一种概念,但在后续的发展中,这种理念得不到发展。原因在于 C 语言大行其道,在C 中很忌讳软件开发中,这种无限制的跳转。类似像`goto`语句,几乎编程教科书中都对`goto`语句进行了评判。但就我个人觉得,其实没有必要,像`goto`语句,有其强大之处,在 C 中,函数有很多地方都会走向结束并释放资源,而`goto`语句提供了一个统一地方去释放资源,这也能看到`goto`的作用,因此至今 linux 内核中还有几万个`goto`语句。协程也是如此,但协程更胆大妄为的跳转,早早的跟不上主流。而编程技术发展到今天,很多脚本语言都开始支持了协程,在 JS,lua 和 go 语言都有明显的使用,协程也在实际应用中进一步使用,例如腾讯微信后台协程库`libco`。

协程允许了一个函数可以跳转到另一个函数当中,执行另一个函数代码,前一个函数的栈是保存下来的,一定时机后也会从其他函数中跳转回来,恢复栈,继续跑下面的代码。这不是普通函数间的调用,协程是直接跳转到另一个栈的函数中去,让 CPU 跑那个函数,那个函数的栈和之前函数的栈根本无任何联系。如何实现,关键就在于栈的保存和当时函数将要跳转的时候,CPU 相关寄存器内容的保存,而`setjmp`和`longjmp`便实现了 CPU 相关寄存器内容的保存,我反汇编也得以实现这一想法(其实直接采用`setjmp`之类也可以实现协程,但后续扩展协程之间交互等,还是需要自己处理汇编来实现)。共享栈式表示在运行中,函数的栈只有一个,在跳转中,前一个函数栈的内容会被保存在堆中,然后要执行的函数栈的内容会被复制到栈中,保持运行。这其中就涉及到很多问题,我在其中也遇到了很多麻烦。这个 Demo 中并不像`libco`中使用`epoll`之类的函数实现 I/O 模型,我这里只是实现了协程间的跳转和相关上下文的切换。

协程先天性的优势在于处理高 I/O 任务的高效,比线程还轻量级的上下文切换,耗费极小的 CPU 性能,函数栈的保存致使它处理 I/O 任务像是在处理同步任务一样,不必考虑异步编程带来的回调地狱。而它的不足在于它无法处理高 CPU 任务,因为协程任务并不是并发执行的,没有像线程那样的时间片轮转机制。当一条线程执行高计算量的任务时,必然会影响到其他协程任务的执行时间。使用协程也会较占据内存空间,因为协程栈的内容是必须保存在内存中,当成千上万条协程执行时,内存会显的比较有压力,但实际上采用共享栈模式以后,协程的内存耗费量已经大规模下降,至少是可以接受的。`libco`也已经达到千万级别的协程支持了。

# ARM 相关寄存器保存的实现

具体内容在 Demo 的`Coroutine.s`中实现了。.text

.align 4

.globl _pushCoroutineEnv

.globl _popCoroutineEnv

.globl _getSP

.globl _getFP

_pushCoroutineEnv:

stp    x21, x30, [x0]

mov    x21, x0

bl     openSVC

mov    x0, x21

ldp    x21, x30, [x0]

mov    x1, sp

stp    x19, x20, [x0]

stp    x21, x22, [x0, #0x10]

stp    x23, x24, [x0, #0x20]

stp    x25, x26, [x0, #0x30]

stp    x27, x28, [x0, #0x40]

stp    x29, x30, [x0, #0x50]

stp    x29, x1, [x0, #0x60]

stp    d8, d9, [x0, #0x70]

stp    d12, d13, [x0, #0x90]

stp    d14, d15, [x0, #0xa0]

mov    x0, #0x0

ret

_popCoroutineEnv:

sub    sp, sp, #0x10

mov    x21, x0

ldr    x0, [x21, #0xb0]

str    x0, [sp, #0x8]

add    x1, sp, #0x8

orr    w0, wzr, #0x3

mov    x2, #0x0

bl     openSVC

mov    x0, x21

add    sp, sp, #0x10

ldp    x19, x20, [x0]

ldp    x21, x22, [x0, #0x10]

ldp    x23, x24, [x0, #0x20]

ldp    x25, x26, [x0, #0x30]

ldp    x27, x28, [x0, #0x40]

ldp    x29, x30, [x0, #0x50]

ldp    x29, x2, [x0, #0x60]

ldp    d8, d9, [x0, #0x70]

ldp    d10, d11, [x0, #0x80]

ldp    d12, d13, [x0, #0x90]

ldp    d14, d15, [x0, #0xa0]

mov    sp, x2

ret

_getSP:

mov   x0, sp

ret

_getFP:

mov   x0, x29

ret

openSVC:

mov    x16, #0x30

svc    #0x80

stp    x29, x30, [sp, #-0x10]!

mov    x29, sp

mov    sp, x29

ldp    x29, x30, [sp], #0x10

ret

在`Coroutine.s`实现的内容大体像上面那样(后续版本可能会有迭代,不一定跟上面相似),简单介绍其几个函数的作用:

_pushCoroutineEnv: 保存调用此函数时,为了后续执行,把 ARM 相关寄存器保存到内存中。

_popCoroutineEnv: 从内存保存过的 ARM 相关寄存器的内容从新赋值到对应的寄存器内,要注意的是,此时`LR`(即`x30`寄存器)寄存器已经改变,所以当执行到`ret`语句时,函数的执行地址会跳转到新的`LR`所保存的地址上去,也其实就是`_pushCoroutineEnv`的下一语句中。`_pushCoroutineEnv`和`_popCoroutineEnv`是两两相对的。

_getSP: 获取到栈底寄存器的内容,为后续栈内容的拷贝使用。

_getFP: 获取到栈帧寄存器内容,主要是为了创建新的协程任务,让新的协程任务的栈可以依靠到触发函数的栈中。

openSVC: 开启 ARM 芯片的 SVC 模式,也就是超级用户模式, ARM 芯片有五种模式,在不同模式有不同的作用。只有开启了 SVC 模式,我们的代码才能访问到一些特定的寄存器,不在此模式访问了那些寄存器,会出现硬件错误。这是 ARM 芯片硬件实现的权限管理,避免非内核访问到不该访问的内容。所以每次保存寄存器内容和恢复寄存器内容必须要开启 SVC 模式。

后续如果要增加协程同步等功能的时候,还会修改这些相关的汇编代码,0.1 版本的协程 Demo 只实现了最基础的功能,连 I/O 模型都没有,所以代码量也并不会很多。

# Demo 中相关 API 的介绍

关键函数有 4 个:typedef void (*coroutineTask)(void);

void coroutine_switch(void);

void coroutine_release(void);

void coroutine_start(coroutineTask entryTask);

void coroutine_create(coroutineTask task);

coroutine_start: 开启协程,并启动一个入口`entryTask`。注意当执行到`coroutine_start`函数后面下一语句时,这时协程已经结束了,协程环境也被释放了。

coroutine_create: 创建一个协程,注意,在未使用`coroutine_start`前是无法创建协程的,相关环境并未创建好,因此,`coroutine_create`会在`entryTask`或者其他协程里面使用,只有协程里面才能创建另一个协程。

coroutine_release: 当一个协程要结束时,必须调用`coroutine_release`函数,来释放此条协程的环境,不然,会跳到此条协程第一条代码语句继续执行。

coroutine_switch: 协程切换,当这条协程需要等待 I/O 的时候,可以切换到另一条协程中,让 CPU 继续执行另一条协程的代码,具体的跳转机制是链表实现的,开发者不必考虑具体会切换到哪一条协程,都是照链表的顺序执行下去的。

# 相关 API 的解析

这里就不贴代码,具体可看文件`Coroutine.c`。

## coroutine_start

1.  初始化一下`pthread`相关的东西,确保每条线程之间的协程环境不会杂在一起,这里就体现出面对对象的重要性了,如果使用面对对象根本不会有这种问题,但这里我一开始并没有这样的打算,因为 C API 显的更简洁。

2.  获取到栈顶和栈底寄存器,必须在这个函数获取,因为这是所有协程的开始点。

3.  创建空白协程和入口,空白协程用来检测所有协程是否结束任务,如果结束,释放相关资源,跳回线程中继续执行线程代码。

4.  开启空白协程,执行协程代码。

## coroutine_create

1. 获取到协程起始点的栈顶寄存器和栈帧寄存器。

2. 将栈顶寄存器,栈帧寄存器和`LR`寄存器(task 的地址)相关内容放在链表中。

## coroutine_release

在链表中把这个协程的释放标志位打开。

## coroutine_switch

这个函数是关键。

1. 获取到当前执行的协程,将它的栈和相关寄存器的内容更新到链表中。

2. 从链表中获取到下一协程,如果此协程是要被释放,则释放此协程,再去找寻下一协程,直到找到可执行协程,然后,将可执行协程的栈的内容和相关寄存器内容赋值到栈和寄存器中。

3. 如此便会执行下一可执行协程代码。

## 空白协程

检测链表中是否只有自己一个协程,如果是,释放协程环境,否,则切换到下一协程。

# 总结

为了实现相关逻辑,实际上也遇到了一些问题,但也让我加深了对 ARM 芯片和栈等的了解。

比如说,要将堆上的内容复制到栈上去,使用`memcpy`函数是会出问题的,因为`memcpy`也会使用到栈,这样在复制的时候,会把`memcpy`的栈干掉,致使出现问题。后续的解决方案是自己从新实现了一个`memcpy`类似的函数,将要使用的变量放在静态区域,因为栈和堆肯定不会在同一内存区域,不会内存冲突问题,这个函数也好写。但带来的问题是,必须对静态区域加互斥锁,不然在不同线程肯定会出问题,这就造成了性能损耗,当然最好的方法是用汇编实现`memcpy`函数,将相关变量放在寄存器内。

这个 Demo 我用 Xcode9 编译,在 iPhone6 实现运行。按理来说, Demo 中的代码适合所有 64 位的 ARM 芯片,但不同的编译环境肯定是有所区别的。

有想法就要实现,看起来还是很完美的。

arm64入栈出栈_使用 ARM64 汇编实现共享栈式协程相关推荐

  1. 响应式 协程_为协程创建改造calladapter以将响应作为状态进行处理

    响应式 协程 In the past, we used to use JakeWharton/retrofit2-kotlin-coroutines-adapter in order to use R ...

  2. 查看某个进程的线程在干什么_有了多线程,为什么还要有协程?

    早期编程都是基于单进程来进行,随着计算机技术的发展,当下推出的通用操作系统都引入了线程,以便进一步提高系统的并发性,并把它视为现代操作系统的一个重要指标.既然线程可以解决高并发问题,为什么后来人们又搞 ...

  3. java 协程线程的区别_用大白话讲进程和线程、协程的区别

    什么是进程和线程 有一定基础的小伙伴们肯定都知道进程和线程. 进程是什么呢? 直白地讲,进程就是应用程序的启动实例.比如我们运行一个游戏,打开一个软件,就是开启了一个进程. 进程拥有代码和打开的文件资 ...

  4. 串行和并行的区别_入门参考:从Go中的协程理解串行和并行

    本文转自公众号语言随笔,欢迎关注 入门参考:从Go中的协程理解串行和并行​mp.weixin.qq.com Go语言的设计亮点之一就是原生实现了协程,并优化了协程的使用方式.使得用Go来处理高并发问题 ...

  5. python是如何实现进程池和线程池的_进程、线程、线程池和协程如何理解?

    1.进程.线程.线程池的概念 进程是一个动态的过程,是一个活动的实体.简单来说,一个应用程序的运行就可以被看做是一个进程,而线程,是运行中的实际的任务执行者.可以说,进程中包含了多个可以同时运行的线程 ...

  6. python实现栈的操作入站出站查找元素等_Python实现的栈(Stack)

    前言 Python本身已有顺序表(List.Tupple)的实现,所以这里从栈开始. 什么是栈 想象一摞被堆起来的书,这就是栈.这堆书的特点是,最后被堆进去的书,永远在最上面.从这堆书里面取一本书出来 ...

  7. unity协程_[C#进阶]C#实现类似Unity的协程

    使用过Unity的同学一定知道,Unity提供了一套协程机制,简直不要太好用.但是这个协程依赖于Unity引擎,离开Unity就无法使用.那有没有办法实现不依赖Unity的协程呢?答案是当然阔以. 所 ...

  8. 【数据结构】共享栈详解 判断共享栈满条件栈顶指针变化详解记忆方法例题

    摘要:简单易懂,详细地介绍共享栈概念,指针,判断共享栈栈满条件以及记忆方法等 目录 共享栈概念 栈顶指针&变化详解 栈顶指针种类的记忆方法 判断栈满条件 判断栈满条件的记忆方法 例题 解题思路 ...

  9. java中栈和堆都存哪些东西_java中栈内存与堆内存(JVM内存模型)

    java中栈内存与堆内存(JVM内存模型) Java中堆内存和栈内存详解1 和 Java中堆内存和栈内存详解2 都粗略讲解了栈内存和堆内存的区别,以及代码中哪些变量存储在堆中.哪些存储在栈中.内存中的 ...

最新文章

  1. R语言学习笔记:向量
  2. 2020-12-11 python查看pytorch版本
  3. ElasticSearch评分分析 explian 解释和一些查询理解
  4. 返回数组中的最大数 -freeCodeCamp
  5. html教图片程,html教的程大全.pdf
  6. MySQL的timeout那点事
  7. html获取视频时长,js获取本地视频时间长度
  8. 网易云音乐歌词下载 C#
  9. 关于原理图库和封装库设计(三)
  10. 美化复选框html,使用CSS3美化复选框checkbox
  11. 汇编程序编译连接过程
  12. 幂函数衰减系数公式推导(最小二乘法求解一元线性回归方程系数)
  13. “一线城市,年薪30万+,我却裸辞回老家”一个寒门贵子的10年职业思考
  14. 华为大数据研发第2轮面试
  15. 20210505 秀米导入已发布微信推送的所有内容
  16. 27 - Excel 的基本公式和重要函数(Excel入门下)
  17. 360搜索引擎数据抓取
  18. 如何快速通过软考中的高级项目管理师?
  19. 日语二级能力考试单词记忆的方法
  20. 【Java高级程序设计学习笔记】数据库编程

热门文章

  1. IOC概述及其实现原理
  2. python 删除MYSQL数据库
  3. 图像对比算法有哪些,图像对比算法是什么
  4. vivo是安卓手机吗_安卓手机运存越来越大 高运存能让手机变快,是真的吗
  5. 基于MATLAB二值化图像的形态学处理
  6. 基于SpringBoot+vue的校园招聘系统
  7. 流量主开通以及添加广告步骤
  8. 微信小程序-申请小程序
  9. web渗透测试之代码审计
  10. openGL实现中点画线算法、DDA画线算法,Bresenham画线算法,并进行鼠标键盘的交互