brpc引入m:n的线程模型,固定的内核线程调度运行大量的bthread以避免内核线程上下文切换带来的开销。
bthread类似协程,即用户态线程,bthread的切换不会陷入内核,不会进行一系列内存同步等耗时操作,因此bthread的切换在100-200ns,相比内核线程的微秒级别有着数量级的提升。
为了实现协程需要协程栈,协程的初始化,以及协程间的切换,下面来逐一分析这几个过程。
首先看下协程栈的结构,如下,context指向协程栈顶,stacktype表示栈的类型(大小),storage为栈空间
栈分配时会通过mmap匿名映射一段空间,然后将高地址位赋值给bottom。
接着看下创建一个bthread的过程,函数入参分别为协程栈底,栈大小,以及这个bthread要执行的函数,返回值,即context为协程栈顶,具体看如下代码。
将rdi赋值给rax,rdi保存的是第一个参数,即stack的bottom
对rax进行 16字节对齐
将rax下移72字节
将rdx保存至rax + 56,rdx为第三个参数,即函数fn的地址
保存MXCSR寄存器(sse浮点数运算状态寄存器,32位)到rax所在位置
将 FPU 控制字的当前值存储到rax + 4
计算finish的绝对地址,保存到rcx中
将rcx保存到rax + 64
函数退出
rax保存的是栈顶,因此context现在指向协程栈的栈顶
这一过程如下图所示:上面为高地址,下面为低地址;左侧为调用此函数的bthread/pthread栈,右侧为执行完成后,新bthread栈空间结构,大框表示64位,小框表示32位,这里关于各个寄存器在栈中保存的位置是为了和协程切换函数里保持一致,具体下面会提到。
接下来看下协程的切换过程,首先看下代码,ofc为旧协程的栈顶,nfc为新协程的栈顶。
337-342行为将对应寄存器push到旧的协程栈中
将rsp下移8字节
比较rcx和0,因为rcx为0,所以zf为1
因为zf为1,所以跳转
将rsp保存至rdi中,rsp指向当前协程栈栈顶,rdi为第一个入参,即ofc
将rsi保存到rsp中,rsi为第二个参数,即nfc,此时栈顶指针rsp指向了新的协程栈
将rsp上移8字节
将协程栈中r12-rbp依次pop到对应寄存器
将rdx保存到rax,rdx为第三个参数,rax为返回值
将rdx保存到rdi,rdi为第一个入参,因此将作为新协程运行的入参
跳转到r8对应的寄存器运行。
在协程切换过程中有两种情况,第一种为新协程是通过bthread_make_fcontext函数刚刚创建的栈,另一种是已经运行过的栈,这两种过程分别如下图所示
上图表示切换到新协程的过程,因为pop到r8的是fn,所以接下来会运行创建协程时的函数fn,当协程运行结束后ret时会pop rip,此时便会执行finish。
上图表示切换到已经运行一段时间的协程的过程,因为右侧协程在上一次被切换的时候会将下一条指令地址push到栈中,然后在这次切换过程中被pop到r8,因此便会继续执行上次被切换后的代码。