以前的文章讲到了bthread的相关机制,但主要是调度的规则等总体上的流程,关于bthread本身创建和切换相关的细节没有太多涉及,好久没看brpc代码了,这篇聊一下bthread是如何实现在pthread上进行创建和切换的。
我们常说的linux线程指的是light-weight process(轻量级进程,简成lwp), 在nptl(native posix thread library)的实现里,也就是pthread的实现里,是1:1的,也就是一个pthread对应一个lwp,而bthread则是m:n的,具体到实现上就是bthread运行在pthread上,并且可以在不同的pthread之间切换,一段时间内,同一个pthread可以运行不同的bthread,这很类似于协程,只不过我们常说的协程是n:1线程库,即所有的协程都运行于一个系统线程中,但除了这一点, bthead和协程很类似,它和协程一样,对于一个pthread来说,无论是bthread还是传统意义上的协程,核心都是如何在用户态完成协程或者bthread的切换,以减少上下文切换的开销。通俗地讲可以理解为如何进行各个子程序的切换。我们知道,程序在运行过程中的独有的状态主要有包含局部变量的栈和各个cpu寄存器,要切换本质上就是这些上下文的切换,目前开源生态里有不少组件提供了这些上下文的保存和切换,用于方便大家在用户态切换协程,比较有名的有boost::context,bhtread用到的则是libcontext,一个boost::context的轻量级版本。bthread的切换,具体到实现上,就是每个bthread有一块自己的栈存储空间,切换的时候就是切换栈顶指针和从栈里存取寄存器值。
为了更好的理解,我们需要先了解一下所用到的汇编的相关知识,这里简单介绍下相关的寄存器和指令。
2.1 x86-64cpu通用寄存器
通用寄存器分为寄存器分为被调用者保存寄存器,和调用者保存寄存器。假设有p调用q的过程,如果值放在了被调用者保存寄存器中,那么需要保证它们的值在q返回到p时与p刚调用q时是一样的。q如何保证这些值不变对于p来是透明的。
标号 | 作用 |
---|---|
%rax | 返回值 |
%rbx | 被调用者保存寄存器 |
%rcx | 第4个参数 |
%rdx | 第3个参数 |
%rsi | 第2个参数 |
%rdi | 第1个参数 |
%rbp | 被调用者保存寄存器 |
%rsp | 栈指针 |
%r8 | 第5个参数 |
%r9 | 第6个参数 |
%r10 | 调用者保存寄存器 |
%r11 | 调用者保存寄存器 |
%r12 | 被调用者保存寄存器 |
%r13 | 被调用者保存寄存器 |
%r14 | 被调用者保存寄存器 |
%r15 | 被调用者保存寄存器 |
2.2 切换用到的x86-64常用汇编指令
指令 | 作用 |
---|---|
pushq | 将寄存器的值入栈 |
popq | 值从栈pop到寄存器里 |
movq | 将一个寄存器的值保存到另一个寄存器 |
leaq | 将地址直接赋值给操作数 |
cmp | 比较两个操作数的大小,比较结果存入flag寄存器,eg:执行完zf=1说明相等,因为零标志为1说明结果为0 |
je | 根据zf标志以决定是否转移,zf=1则跳转 |
jmp | 无条件跳转 |
stmxcsr | 将mxcsr寄存器中的值保存到操作数中 |
ldmxcsr | 将操作数中的值加载到mxcsr寄存器中 |
fnstcw | 把控制寄存器的内容存储到由操作数指定的字存储单元 |
fldcw | 将由操作数指定的字存储单元内容存储到控制寄存器中 |
bthread栈结构如下,bthread_fcontext_t是void*的别名,bthread_fcontext_t context是栈顶指针:
创建bthread栈的工厂类如下:
其中最核心的代码分别是:
(1)分配存储空间:allocate_stack_storage(&storage, *stackclass::stack_size_flag,flags_guard_page_size)
(2)构造栈内部结构:context = bthread_make_fcontext(storage.bottom, storage.stacksize, entry)
其中(1)暂时不展开讨论,(2)的定义如下:
返回bthread_fcontext_t,也就是栈顶指针,函数有三个参数,第一个参数是bthread栈底指针,第二个是bthread栈大小,第三个是bthread的入口函数地址。
bthread_make_fcontext是直接用汇编写的,代码和解释如下:
可以看到,经过这些代码构造出的bthread栈结构如下:
中间这么多空值是为了和后面堆栈切换的结构相对应。
bthread通过调用jump_stack进行切换,如下:
jump_stack调用的是bthread_jump_fcontext,也是直接用汇编实现的,代码和解释如下:
其中需要注意的是popq r8,在bthread调用切换函数的时候会push rip,对于旧堆栈这个popq取到的就是这个原来push的rip的值。
切换整体上可以分为两大类,切换到一个新建的bthread和切换到一个运行过但未结束的bthread,以上处理过程都能覆盖,前面讲到的新建stack中间空置的部分就是为了对应这个处理流程,新栈和旧栈顶部的结构都是一样的,都是数据寄存器数据的区域,区别就在于下面是否有其他数据,新栈直接就到底部了,运行过的会有原来的其他数据。