菜鸟笔记
提升您的技术认知

brpc 笔记

bthread(一) 前言bthread(二) 线程模型及bthreadbthread(三) bthread数据结构bthread(四) bthread用户接口和代码执行路径bthread(五) 无锁队列rq的代码实现bthread(六) 小结brpc的精华bthread源码剖析brpc介绍、编译与使用brpc源码解析(一)—— rpc服务添加以及服务器启动主要过程brpc源码解析(二)—— brpc收到请求的处理过程brpc源码解析(三)—— 请求其他服务器以及往socket写数据的机制brpc源码解析(四)—— Bthread机制brpc源码解析(五)—— 基础类resource pool详解brpc源码解析(六)—— 基础类socket详解brpc源码解析(七)—— worker基于ParkingLot的bthread调度brpc源码解析(八)—— 基础类EventDispatcher详解brpc源码解析(九)—— 基础类WorkStealingQueue详解brpc源码解析(十)—— 核心组件bvar详解(1)简介和整体架构brpc源码解析(十一)—— Reducer类和Adder类解析brpc源码解析(十二)—— 核心组件bvar详解 AgentGroup类详解brpc源码解析(十三)—— 核心组件bvar详解(4)combiner详解brpc源码解析(十四)—— 核心组件bvar详解 sampler详解brpc源码解析(十五)—— bthread栈创建和切换详解brpc源码解析(十六)—— 作为client的连接建立和处理详解brpc源码解析(十七)—— bthread上的类futex同步组件butex详解brpc源码解析(十八)—— MPSC队列ExecutionQueue详解brpc源码解析(十九)—— 双buffer数据结构DoublyBufferedData详解brpc源码解析(二十)—— 用于访问下游的Channel类详解

brpc源码解析(十五)—— bthread栈创建和切换详解

阅读 : 825

以前的文章讲到了bthread的相关机制,但主要是调度的规则等总体上的流程,关于bthread本身创建和切换相关的细节没有太多涉及,好久没看brpc代码了,这篇聊一下bthread是如何实现在pthread上进行创建和切换的。

一.bthread的本质

我们常说的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栈结构如下,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堆栈切换

bthread通过调用jump_stack进行切换,如下:


jump_stack调用的是bthread_jump_fcontext,也是直接用汇编实现的,代码和解释如下:

其中需要注意的是popq r8,在bthread调用切换函数的时候会push rip,对于旧堆栈这个popq取到的就是这个原来push的rip的值。

切换整体上可以分为两大类,切换到一个新建的bthread和切换到一个运行过但未结束的bthread,以上处理过程都能覆盖,前面讲到的新建stack中间空置的部分就是为了对应这个处理流程,新栈和旧栈顶部的结构都是一样的,都是数据寄存器数据的区域,区别就在于下面是否有其他数据,新栈直接就到底部了,运行过的会有原来的其他数据。