Libco源码分析(二):底层协程切换的原理

涉及文件:
coctx_swap.S
coctx.cpp
coctx.h

其中核心文件为coctx_swap.S,利用汇编语言实现了协程的切换功能。coctx.{cpp,h}是在其基础上进行C语言的封装,很简单,所以我会重点分析coctx_swap.S中的代码。可能会有一些人没有学过汇编语言,我自己在接触Libco之前也没有学过,我觉得如果有C语言基础,把一些寄存器的作用记一下,基本的指令学习下,看懂coctx_swap.S应该不成问题。

底层协程切换的原理

协程切换是协程调度的关键,他将一个协程的状态保存并切换到另一个协程中继续执行

线程

一般而言,线程具有以下四个要素:

  • 执行代码
  • 寄存器

如果我们想要实现一个用户级的线程,即协程,每个协程就需要独立的以上四个属性。其中执行代码由寄存器eip保存。一些数据虽然是在堆中分配的,但他也是借助栈上分配的指针等间接保存的。而栈是由ebp和esp寄存器所保存,所以决定一个协程的要素被简化为寄存器。

寄存器(以x86 64位为例)

libco使用了14个寄存器,存储在reg数组中,分别是:

reg[0]: r15   低地址
reg[1]: r14
reg[2]: r13
reg[3]: r12
reg[4]: r9
reg[5]: r8
reg[6]: rbp   栈底
reg[7]: rdi   第一个参数地址
reg[8]: rsi   第二个参数地址
reg[9]: ret   返回函数
reg[10]: rdx
reg[11]: rcx
reg[12]: rbx
reg[13]: rsp  栈顶

其中rbp表示栈底,rsp表示栈顶,这两个寄存器表现出栈的环境。rdi和rsi分别表示传入函数的第一个和第二个参数的地址。ret表示函数返回后的地址。

协程切换(以x86 64位为例)

协程切换的本质实际就是一个偷梁换柱的把戏,将寄存器的环境改变来实现协程的切换

	leaq 8(%rsp),%rax//rax为返回函数的上一个地址
	leaq 112(%rdi),%rsp//rdi存放第一个参数的地址,rsp指向rdi+112的位置,结构体中的参数在栈中是倒着排的,推算下来rsp指向的是ss_size
	pushq %rax //rax存放之前栈的rsp指针+8的地址
	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//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

解析:
首先当前处在调用这个函数的协程所在的栈,有两个参数,第一个参数是当前栈的信息,第二个参数是要切换的目标协程所在栈的信息
rdi表示第一个参数的地址,rsi为第二个参数的地址,rsp为当前栈顶指针
第一行将rsp指向地址的上一个地址传给rax,这个值本身没什么用,rax只是作为临时存放rsp地址的变量,为什么是$rsp+8呢,后面分析就知道了
第二行rdi是第一个参数的地址,要了解112(%rdi)的具体含义,需要了解结构体在栈中的存储方式,首先第一个参数是一个指向结构体的指针,结构体在栈
中的存储为从后往前入栈,coctx_t的结构体为

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

那么先入栈的是ss_sp,然后是ss_size,最后数组regs也是倒着入栈的,所以最后入栈的是regs[0]
那么112(%rdi)的位置就可以计算出来是ss_size的位置,将这个位置的地址传给rsp

接下来就是一系列的push操作,push就可以把值存入rsp的下一个地址,然后自身也指向下一个地址
第一个pushq %rax 就是将之前栈的rsp指针+8的地址,这个值作为以后重新调用这个协程的rsp的值
接下来就是其他寄存器的push操作

-8(%rax) 就是调用coctx_swap函数的函数的地址,即返回地址

push完后,这个协程的状态就存进了参数一中了,接下来就可以进行协程的切换了

movq %rsi, %rsp rsi是第二个参数的地址,我们让rsp等于它,便可以对第二个参数进行操作了

接下来一系列的pop操作就是将第二个参数保存的寄存器状态全部恢复过来

还记得之前push的-8(%rax)吗,这个是函数的返回地址,我们先把他赋值给rax

我们让rsp等于之前存进去的rax,这个rax是存放替换的栈的rsp指针+8的地址

pushq %rax 就可以成功让我们当前的rsp指向当前协程的返回地址了,ret以后便可以继续这个协程的函数执行了!

Other

Libco在19年底的时候对coctx_swap.S进行了一次很大的修改,之前都是通过将rdi和rsi的指针给rsp通过pop和push操作保存或恢复寄存器。但是修改以后是通过mov指令来进行操作。对于改动的原因,我觉得可能是这个issues所提到的Sys V ABI约定的问题。所以对于上面代码的解析是针对之前push pop版本的,但其实对于mov版本原理是一样的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值