arm64入栈出栈_使用 ARM64 汇编实现共享栈式协程
# 简述
大约在半年以前,我曾经了解过协程的相关实现,也看过腾讯后台开源的协程库`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 汇编实现共享栈式协程相关推荐
- 响应式 协程_为协程创建改造calladapter以将响应作为状态进行处理
响应式 协程 In the past, we used to use JakeWharton/retrofit2-kotlin-coroutines-adapter in order to use R ...
- 查看某个进程的线程在干什么_有了多线程,为什么还要有协程?
早期编程都是基于单进程来进行,随着计算机技术的发展,当下推出的通用操作系统都引入了线程,以便进一步提高系统的并发性,并把它视为现代操作系统的一个重要指标.既然线程可以解决高并发问题,为什么后来人们又搞 ...
- java 协程线程的区别_用大白话讲进程和线程、协程的区别
什么是进程和线程 有一定基础的小伙伴们肯定都知道进程和线程. 进程是什么呢? 直白地讲,进程就是应用程序的启动实例.比如我们运行一个游戏,打开一个软件,就是开启了一个进程. 进程拥有代码和打开的文件资 ...
- 串行和并行的区别_入门参考:从Go中的协程理解串行和并行
本文转自公众号语言随笔,欢迎关注 入门参考:从Go中的协程理解串行和并行mp.weixin.qq.com Go语言的设计亮点之一就是原生实现了协程,并优化了协程的使用方式.使得用Go来处理高并发问题 ...
- python是如何实现进程池和线程池的_进程、线程、线程池和协程如何理解?
1.进程.线程.线程池的概念 进程是一个动态的过程,是一个活动的实体.简单来说,一个应用程序的运行就可以被看做是一个进程,而线程,是运行中的实际的任务执行者.可以说,进程中包含了多个可以同时运行的线程 ...
- python实现栈的操作入站出站查找元素等_Python实现的栈(Stack)
前言 Python本身已有顺序表(List.Tupple)的实现,所以这里从栈开始. 什么是栈 想象一摞被堆起来的书,这就是栈.这堆书的特点是,最后被堆进去的书,永远在最上面.从这堆书里面取一本书出来 ...
- unity协程_[C#进阶]C#实现类似Unity的协程
使用过Unity的同学一定知道,Unity提供了一套协程机制,简直不要太好用.但是这个协程依赖于Unity引擎,离开Unity就无法使用.那有没有办法实现不依赖Unity的协程呢?答案是当然阔以. 所 ...
- 【数据结构】共享栈详解 判断共享栈满条件栈顶指针变化详解记忆方法例题
摘要:简单易懂,详细地介绍共享栈概念,指针,判断共享栈栈满条件以及记忆方法等 目录 共享栈概念 栈顶指针&变化详解 栈顶指针种类的记忆方法 判断栈满条件 判断栈满条件的记忆方法 例题 解题思路 ...
- java中栈和堆都存哪些东西_java中栈内存与堆内存(JVM内存模型)
java中栈内存与堆内存(JVM内存模型) Java中堆内存和栈内存详解1 和 Java中堆内存和栈内存详解2 都粗略讲解了栈内存和堆内存的区别,以及代码中哪些变量存储在堆中.哪些存储在栈中.内存中的 ...
最新文章
- R语言学习笔记:向量
- 2020-12-11 python查看pytorch版本
- ElasticSearch评分分析 explian 解释和一些查询理解
- 返回数组中的最大数 -freeCodeCamp
- html教图片程,html教的程大全.pdf
- MySQL的timeout那点事
- html获取视频时长,js获取本地视频时间长度
- 网易云音乐歌词下载 C#
- 关于原理图库和封装库设计(三)
- 美化复选框html,使用CSS3美化复选框checkbox
- 汇编程序编译连接过程
- 幂函数衰减系数公式推导(最小二乘法求解一元线性回归方程系数)
- “一线城市,年薪30万+,我却裸辞回老家”一个寒门贵子的10年职业思考
- 华为大数据研发第2轮面试
- 20210505 秀米导入已发布微信推送的所有内容
- 27 - Excel 的基本公式和重要函数(Excel入门下)
- 360搜索引擎数据抓取
- 如何快速通过软考中的高级项目管理师?
- 日语二级能力考试单词记忆的方法
- 【Java高级程序设计学习笔记】数据库编程