Boost库对于fibers协程源码的个人理解
第一章 环境说明与搭建
1、环境说明
为了能完美的复现我的源码以及实验,还是有必要说明一下我在看源码以及做实验的过程中所依赖的环境。(1)使用的是windows平台下的linux系统64位机(wsl2),上面的发行版是ubuntu-22.04(这个环境的搭建就不赘述,网上一大堆,之所以选linux系统,第一是因为我科研的环境就是这个,第二就是在后面看汇编指令的时候发现协程的上下文创建与跳转,linux环境下比windows下的汇编指令要简单一些,对于复杂的问题要从简单入手,这是基本,所以选择了linux系统)(2)boost我选择的时候1.85.0版本,相较于我写这篇文章的时间算是比较新的版本了(目前已经到了1.86.0)
2、环境搭建
2.1、boost环境
还是简单说一下boost的源码下载与安装,其实在搭建ubuntu系统的时候会自动安装boost库,但是为了不污染环境,以及后续实验可能会涉及到修改源码,所以最好就是源码安装,与系统自带的boost完全隔离开来。 step1:使用wget下载,wget下载的比较完整,虽然可以去github上下载或者clone(boost是纯开源的项目),但是boost是个大项目,其中有些部分组件是后续分开开发的,其中有些组件是一个指向其他仓库的链接,所以单纯的使用git clone去克隆boost库,不会克隆完整的,或者使用能连续拉取的命令(submodule),去把其他组件全部克隆
wget https://boostorg.jfrog.io/artifactory/main/release/1.85.0/source/boost_1_85_0.tar.gz
step2:在压缩包同级目录下解压缩
tar -xvzf boost_1_85_0.tar.gz
step3:进入到解压缩后的源码目录下
cd boost_1_85_0
step4:运行bash文件
./bootstrap.sh
step5:运行生成的b2
./b2 install --prefix=../local # 这条命令就是在下载boost的头文件以及它的静态库与动态库, 并放到local目录下面,
# --prefix参数是指定要安装的路径,这个地方我是在与boost_1_85_0同级的目录下创建了local,为的就是与源码隔开,自己
# 写的测试代码使用的头文件与库就全是local里面的
目录结构展示
2.2、vscode代码环境
step1:目录结构展示
step2:我使用的是vscode去连接wsl,在vscode中我使用的是clangd和clang-format,clangd是代码智能搜索用的,clang-format是代码格式用的,如果你要使用和我一样的环境,首先要确保ubuntu上有clangd和clang-format,使用
clangd --version
calng-format --version
# 这两个bash命令是获取各自的版本号,如果有则会显示版本号,如果没有它会提示怎么安装
安装好之后,需要在vscode的扩展里面搜索这两个,并下载插件,同时还要下载基本的c++插件以及cmake插件(要保证ubuntu系统中有cmake,检测与下载方法同上) step3:在工作空间根目录下创建隐藏文件.vscode,再在隐藏文件下创建两个文件,分别是 c_cpp_properties.json(要注意的地方也是比较重要的地方,亦是要修改的地方)
{
"configurations": [
{
"name": "Linux",
"includePath": [
"${workspaceFolder}/**"
],
"defines": [
"_DEBUG",
"UNICODE",
"_UNICODE"
],
"compilerPath": "/usr/bin/g++",
"cStandard": "c11",
"cppStandard": "c++17",
"intelliSenseMode": "${default}",
"compileCommands": "${workspaceFolder}/build/compile_commands.json",
"configurationProvider": "ms-vscode.cmake-tools"
}
],
"version": 4
}
settings.json(要注意的地方也是比较重要的地方,亦是要修改的地方)
{
"C_Cpp.intelliSenseEngine": "disabled", // 这个是关闭c++的插件自己的代码智能搜索引擎,因为我后续使用的是
// clangd,个人觉得clangd要好一些,但也有些瑕疵
"cmake.sourceDirectory": "/home/yzx/vscode/workspace", // 这是指向你项目的工作空间也就是项目根目录,需
// 要根据你的项目来修改
"[c]": {
"editor.defaultFormatter": "xaver.clang-format"
},
"[cpp]": {
"editor.defaultFormatter": "xaver.clang-format"
},
"editor.formatOnSave": true,
"workbench.colorCustomizations": {},
}
step4:在项目根目录下创建两个隐藏文件,分别是 .clangd
CompileFlags:
CompilationDatabase: ./build
.clang-format
BasedOnStyle: Google
step5:CMakeList文件的创建 顶层CMakeList
cmake_minimum_required(VERSION 3.0.0)
project(fiber)
set(CMAKE_PREFIX_PATH "${CMAKE_SOURCE_DIR}/boost_1_85_0/libs") # 指定优先搜索的路径,后面findpackage的
# 搜索路径
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_subdirectory(src)
src下的CMakeList
cmake_minimum_required(VERSION 3.0.0)
project(fiber)
set(Boost_INCLUDE_DIR "${CMAKE_SOURCE_DIR}/local/include")
set(Boost_LIBRARY_DIR "${CMAKE_SOURCE_DIR}/local/lib")
include_directories(${Boost_INCLUDE_DIR})
find_package(Boost 1.82.0 REQUIRED COMPONENTS context fiber system filesystem)
add_executable(main main.cpp)
target_link_libraries(main
${Boost_LIBRARY_DIR}/libboost_context.a
${Boost_LIBRARY_DIR}/libboost_fiber.a
${Boost_LIBRARY_DIR}/libboost_system.a
${Boost_LIBRARY_DIR}/libboost_filesystem.a)
step6:src下测试源码
#include <boost/fiber/all.hpp>
#include <iostream>
int main() {
boost::fibers::fiber f1([] {
std::cout << "fiber1 started" << std::endl;
boost::this_fiber::yield();
puts("fiber1 ended");
});
boost::fibers::fiber f2([] {
puts("fiber2 started");
boost::this_fiber::yield();
puts("fiber2 ended");
});
f1.join();
f2.join();
return 0;
}
step7:运行结果展示
上面展示结果证明环境搭建成功
第二章 源码解析
1、源码解析前的提示
我是使用ctrl+鼠标左键进行代码跳转,跟踪源码,所以需要一个比较好的代码搜索引擎,我使用clangd,但有时候clangd有小瑕疵(亦或是我配置的问题),它只会跳转到函数的声明不会跳转到其定义,这个时候需要自己找到源文件中的定义,第二次跳转的时候就能跳转到函数的定义了(小tips:函数的声明文件和其定义所在文件的名字大多时候是一样的只是一个以hpp结尾一个是cpp),在boost中其源码在libs下面。还有有时候跳转到某个函数的定义不一定准确,因为函数的实现可能会是多个,它找到的不一定准确,所以我还使用断言的方式去确定到底是哪一个实现
2、从main函数入口
step1:鼠标点击跳转,看boost::fibers::fiber这个对象是怎么构造的,跳转之后是在fiber.hpp文件中,此文件中有多个构造,并且有构造函数之间互相调用,先定位在下面这个构造函数中
step2:这个构造中有一个default_stack()很重要,最终是构造了一个basic_fixedsize_stack对象,并传递给fiber构造函数。下面是basic_fixedsize_stack构造方法,默认有一个栈的大小
step3:同时在basic_fixedsize_stack中还有一个比较重要的成员方法如下,在这个方法中真正调用了malloc在堆上开辟出了一个空间,并把模拟栈大小以及栈顶指针存入到stack_context中,在stack_context结构体中只有这两个属性,allocate创建完之后返回了一个stack_context对象,此时allocate方法还并没有调用
step4:继续跳转make_worker_context_with_properties函数,跳转之后在context.hpp文件中,在此方法中就开始在堆上构造模拟栈空间,如下。在此方法中才是真正在调用allocate方法,在堆上真正的开辟空间。在此方法中首先找到一个位置,就是storage指向的位置,这个位置和allocate开辟出来的栈顶之间的空间就是用来存放context_t对象的,也就是worker_context对象,至于0xff的存在是为了字节对齐,所以这就是第495行代码的含义,紧接着计算了一下栈底的位置,以及重新计算栈大小,并没有去修改stack_context对象中记录的栈大小,而是又重新构造了一个新的对象preallocated,这个对象中记录了三个属性,除了栈顶和栈大小,还用了一个属性(也就是指针),把preallocated对象和stack_context关联起来了
step5:接下来看return中的语句,关键的就是new ( storage) context_t,就是构造一个context_t对象也就是worker_context对象,括号中storage指的是对象从storage指向的位置开始填充,这就是上面为什么要修改栈顶指针指向的位置,就是给worker_context对象预留空间,接下来开worker_context的构造函数,如下,首先构造继承的context对象,context构造只是给几个属性赋值,没有太重要的信息,在worker_context中首先就是把你自己的函数调用对象以及参数封装到worker_context对象中,其次默认情况下if中的代码不会执行,接着就是构造最重要的对象c_也就是boost::context::fiber 对象,构造这对象的时候传递的不再是自己的函数调用对象以及参数,而是worker_context对象中的run_方法
step6:接着继续看boost::context::fiber 对象是怎么构造的,如下,通过参数个数可以确定调用的是create_fiber2而不是create_fiber1来构造fctx_也就是fcontext_t一个指向模拟栈的指针,就是通过这个真正才知道堆上栈的位置
step7:接着继续看是怎么create_fiber2构造的,如下,首先又是使用同样的方法构造了一个Record对象,这个对象构造很简单,只是给三个属性赋值,第一个是sctx_,第二个是allocate对象,第三个是worker_context中的run_函数调用,接着就是将栈顶下移64字节,计算栈底(每次重新计算是因为没有记录栈底,只是记录了栈顶和栈大小,可以算出来,可以记录也可以不记录,无非就是空间换时间,时间换空间),然后计算了一下当前栈还剩多少空间
3、上述代码小结示意图
4、汇编指令解析
在上面main函数入口最后的一个函数中,也就是create_fiber2函数中,在这个函数中最后就是还有几行代码,这几行代码涉及到汇编指令,所以放到这一节中,如下,make_fcontext就是在创建上下文,准确的说是往之前开辟好的堆栈地址空间中填入值,也就是寄存器的值以及其他需要记录的值,这个方法的第一个参数是当前栈顶指针,第二个参数是当前栈的大小,第三个参数是个函数入口,是后面jump_fcontext中要用到的地址。jump_fcontext是从一个上下文跳转到另一个上下文,第一个参数是要跳到的堆栈地址,第二个参数是当前堆栈空间的record。
4.1、函数调用回顾
在栈上,函数调用的步骤是,首先是参数传递的问题,如果参数单一(也就是基本类型)而且参数个数少,那么可以通过寄存器传值,如果参数复杂(也就是类对象等)或者参数个数多,那么一般就需要通过栈来传递,从右往左把参数一个一个的压入栈中,然后就是把下一条指令地址压入栈中,然后跳转到函数入口执行函数调用。明显可以看到这两个方法采用的是寄存器传递参数,在我的环境中,第一个参数放在rdi寄存器中,第二个参数放在rsi寄存器中,第三个参数放在rdx寄存器中。
4.2、make_fcontext汇编指令分析
汇编指令在源码的libs/context/src/asm/make_x86_64_sysv_elf_gas.S中,如下
# ifdef __i386__
# include "make_i386_sysv_elf_gas.S"
# else
# if defined __CET__
# include <cet.h>
# define SHSTK_ENABLED (__CET__ & 0x2)
# define BOOST_CONTEXT_SHADOW_STACK (SHSTK_ENABLED && SHADOW_STACK_SYSCALL)
# else
# define _CET_ENDBR
# endif
.file "make_x86_64_sysv_elf_gas.S"
.text
.globl make_fcontext
.type make_fcontext,@function
.align 16
make_fcontext:
_CET_ENDBR
#if BOOST_CONTEXT_SHADOW_STACK
/* the new shadow stack pointer (SSP) */
movq -0x8(%rdi), %r9
#endif
/* first arg of make_fcontext() == top of context-stack */
movq %rdi, %rax
/* shift address in RAX to lower 16 byte boundary */
andq $-16, %rax
/* reserve space for context-data on context-stack */
/* on context-function entry: (RSP -0x8) % 16 == 0 */
leaq -0x48(%rax), %rax
/* third arg of make_fcontext() == address of context-function */
/* stored in RBX */
movq %rdx, 0x30(%rax)
/* save MMX control- and status-word */
stmxcsr (%rax)
/* save x87 control-word */
fnstcw 0x4(%rax)
#if defined(BOOST_CONTEXT_TLS_STACK_PROTECTOR)
/* save stack guard */
movq %fs:0x28, %rcx /* read stack guard from TLS record */
movq %rcx, 0x8(%rsp) /* save stack guard */
#endif
/* compute abs address of label trampoline */
leaq trampoline(%rip), %rcx
/* save address of trampoline as return-address for context-function */
/* will be entered after calling jump_fcontext() first time */
movq %rcx, 0x40(%rax)
/* compute abs address of label finish */
leaq finish(%rip), %rcx
/* save address of finish as return-address for context-function */
/* will be entered after context-function returns */
movq %rcx, 0x38(%rax)
#if BOOST_CONTEXT_SHADOW_STACK
/* Populate the shadow stack and normal stack */
/* get original SSP */
rdsspq %r8
/* restore new shadow stack */
rstorssp -0x8(%r9)
/* save the restore token on the original shadow stack */
saveprevssp
/* push the address of "jmp trampoline" to the new shadow stack */
/* as well as the stack */
call 1f
jmp trampoline
1:
/* save address of "jmp trampoline" as return-address */
/* for context-function */
pop 0x38(%rax)
/* Get the new SSP. */
rdsspq %r9
/* restore original shadow stack */
rstorssp -0x8(%r8)
/* save the restore token on the new shadow stack. */
saveprevssp
/* reserve space for the new SSP */
leaq -0x8(%rax), %rax
/* save the new SSP to this fcontext */
movq %r9, (%rax)
#endif
#if BOOST_CONTEXT_SHADOW_STACK
/* Populate the shadow stack */
/* get original SSP */
rdsspq %r8
/* restore new shadow stack */
rstorssp -0x8(%r9)
/* save the restore token on the original shadow stack */
saveprevssp
/* push the address of "jmp trampoline" to the new shadow stack */
/* as well as the stack */
call 1f
jmp trampoline
1:
/* save address of "jmp trampoline" as return-address */
/* for context-function */
pop 0x38(%rax)
/* Get the new SSP. */
rdsspq %r9
/* restore original shadow stack */
rstorssp -0x8(%r8)
/* save the restore token on the new shadow stack. */
saveprevssp
/* now the new shadow stack looks like:
base-> +------------------------------+
| address of "jmp trampoline" |
SSP-> +------------------------------+
| restore token |
+------------------------------+
*/
/* reserve space for the new SSP */
leaq -0x8(%rax), %rax
/* save the new SSP to this fcontext */
movq %r9, (%rax)
#endif
ret /* return pointer to context-data */
trampoline:
/* store return address on stack */
/* fix stack alignment */
_CET_ENDBR
#if BOOST_CONTEXT_SHADOW_STACK
/* save address of "jmp *%rbp" as return-address */
/* on stack and shadow stack */
call 2f
jmp *%rbp
2:
#else
push %rbp
#endif
/* jump to context-function */
jmp *%rbx
finish:
_CET_ENDBR
/* exit code is zero */
xorq %rdi, %rdi
/* exit application */
call _exit@PLT
hlt
.size make_fcontext,.-make_fcontext
/* Mark that we don't need executable stack. */
.section .note.GNU-stack,"",%progbits
# endif
在上述代码中有很多宏定义的代码,可以通过objdump把可执行文件中所有的宏全部输出重定向到文件,然后搜索看看是不是定义了这些宏,在本台电脑上这些宏都没有定义
/* first arg of make_fcontext() == top of context-stack */
movq %rdi, %rax // movq后面加q是指64位,前面说过rdi寄存器存的是第一个参数,这条指令就是把当前栈顶指针存入rax寄存器中
/* shift address in RAX to lower 16 byte boundary */
andq $-16, %rax // 这个指令并不是把栈顶指针向下移动16个字节,而是进行16字节对齐,当前正好本身就是16字节对齐的,所以这条
// 指令不做任何操作
/* reserve space for context-data on context-stack */
/* on context-function entry: (RSP -0x8) % 16 == 0 */
leaq -0x48(%rax), %rax // 这条指令就是把栈顶指针向下移动72字节
/* third arg of make_fcontext() == address of context-function */
/* stored in RBX */
movq %rdx, 0x30(%rax) // 这条指令就是把第三个参数放到指定的位置
/* save MMX control- and status-word */
stmxcsr (%rax)
/* save x87 control-word */
fnstcw 0x4(%rax) // 这两条指令也是存值,存的是关于浮点数计算的寄存器值(可以不用太关心)
/* compute abs address of label trampoline */
leaq trampoline(%rip), %rcx
/* save address of trampoline as return-address for context-function */
/* will be entered after calling jump_fcontext() first time */
movq %rcx, 0x40(%rax) // 这两条指令的作用是,trampoline是个个标签,在下面,计算当前指令到这个标签的相对位置,并记录到
// 指定为止,这个标签是为了后面jump_fcontext使用的,执行一次之后就会被覆盖
/* compute abs address of label finish */
leaq finish(%rip), %rcx
/* save address of finish as return-address for context-function */
/* will be entered after context-function returns */
movq %rcx, 0x38(%rax) // 这两条指令和上面一样,标签是finish,最终协程执行完切换之后会跳转到这个地方,这个执行完之后协程
// 函数算是真正的执行完
ret /* return pointer to context-data */ // 构建协程上下文算是完成了(准确说是堆栈填值完成),调用ret返回,调用ret
// 返回,是把rax寄存器中的值赋值给接收变量
4.3、堆栈示意图
4.4、jump_fcontext汇编指令分析
汇编指令在源码的libs/context/src/asm/jump_x86_64_sysv_elf_gas.S中,如下
# ifdef __i386__
# include "jump_i386_sysv_elf_gas.S"
# else
# if defined __CET__
# include <cet.h>
# define SHSTK_ENABLED (__CET__ & 0x2)
# define BOOST_CONTEXT_SHADOW_STACK (SHSTK_ENABLED && SHADOW_STACK_SYSCALL)
# else
# define _CET_ENDBR
# endif
.file "jump_x86_64_sysv_elf_gas.S"
.text
.globl jump_fcontext
.type jump_fcontext,@function
.align 16
jump_fcontext:
_CET_ENDBR
leaq -0x40(%rsp), %rsp /* prepare stack */
#if !defined(BOOST_USE_TSX)
stmxcsr (%rsp) /* save MMX control- and status-word */
fnstcw 0x4(%rsp) /* save x87 control-word */
#endif
#if defined(BOOST_CONTEXT_TLS_STACK_PROTECTOR)
movq %fs:0x28, %rcx /* read stack guard from TLS record */
movq %rcx, 0x8(%rsp) /* save stack guard */
#endif
movq %r12, 0x10(%rsp) /* save R12 */
movq %r13, 0x18(%rsp) /* save R13 */
movq %r14, 0x20(%rsp) /* save R14 */
movq %r15, 0x28(%rsp) /* save R15 */
movq %rbx, 0x30(%rsp) /* save RBX */
movq %rbp, 0x38(%rsp) /* save RBP */
#if BOOST_CONTEXT_SHADOW_STACK
/* grow the stack to reserve space for shadow stack pointer(SSP) */
leaq -0x8(%rsp), %rsp
/* read the current SSP and store it */
rdsspq %rcx
movq %rcx, (%rsp)
#endif
#if BOOST_CONTEXT_SHADOW_STACK
/* grow the stack to reserve space for shadow stack pointer(SSP) */
leaq -0x8(%rsp), %rsp
/* read the current SSP and store it */
rdsspq %rcx
movq %rcx, (%rsp)
# endif
/* store RSP (pointing to context-data) in RAX */
movq %rsp, %rax
/* restore RSP (pointing to context-data) from RDI */
movq %rdi, %rsp
#if BOOST_CONTEXT_SHADOW_STACK
/* first 8 bytes are SSP */
movq (%rsp), %rcx
leaq 0x8(%rsp), %rsp
/* Restore target(new) shadow stack */
rstorssp -8(%rcx)
/* restore token for previous shadow stack is pushed */
/* on previous shadow stack after saveprevssp */
saveprevssp
/* when return, jump_fcontext jump to restored return address */
/* (r8) instead of RET. This miss of RET implies us to unwind */
/* shadow stack accordingly. Otherwise mismatch occur */
movq $1, %rcx
incsspq %rcx
#endif
movq 0x40(%rsp), %r8 /* restore return-address */
#if !defined(BOOST_USE_TSX)
ldmxcsr (%rsp) /* restore MMX control- and status-word */
fldcw 0x4(%rsp) /* restore x87 control-word */
#endif
#if defined(BOOST_CONTEXT_TLS_STACK_PROTECTOR)
movq 0x8(%rsp), %rdx /* load stack guard */
movq %rdx, %fs:0x28 /* restore stack guard to TLS record */
#endif
movq 0x10(%rsp), %r12 /* restore R12 */
movq 0x18(%rsp), %r13 /* restore R13 */
movq 0x20(%rsp), %r14 /* restore R14 */
movq 0x28(%rsp), %r15 /* restore R15 */
movq 0x30(%rsp), %rbx /* restore RBX */
movq 0x38(%rsp), %rbp /* restore RBP */
leaq 0x48(%rsp), %rsp /* prepare stack */
/* return transfer_t from jump */
#if !defined(_ILP32)
/* RAX == fctx, RDX == data */
movq %rsi, %rdx
#else
/* RAX == data:fctx */
salq $32, %rsi
orq %rsi, %rax
#endif
/* pass transfer_t as first arg in context function */
#if !defined(_ILP32)
/* RDI == fctx, RSI == data */
#else
/* RDI == data:fctx */
#endif
movq %rax, %rdi
/* indirect jump to context */
jmp *%r8
.size jump_fcontext,.-jump_fcontext
/* Mark that we don't need executable stack. */
.section .note.GNU-stack,"",%progbits
# endif
leaq -0x40(%rsp), %rsp /* prepare stack */ // 将主线程的栈指针向下移动64字节,用于保存当前主线程的上下文
movq %r12, 0x10(%rsp) /* save R12 */
movq %r13, 0x18(%rsp) /* save R13 */
movq %r14, 0x20(%rsp) /* save R14 */
movq %r15, 0x28(%rsp) /* save R15 */
movq %rbx, 0x30(%rsp) /* save RBX */
movq %rbp, 0x38(%rsp) /* save RBP */
// 将各个寄存器的值保存到当前线程的普通栈中,rbp寄存器中存入的是上一个栈帧的位置,再往上有一个0x40的位置,在这里没有显示的存
// 储但是这个位置很重要,存储的是函数调用执行完之后下一条指令的位置
/* store RSP (pointing to context-data) in RAX */
movq %rsp, %rax // 将rsp寄存器的值存到rax寄存器中,rax寄存器指向了rsp寄存器指向的位置,即rax寄存器指向了主线程栈顶的位置
/* restore RSP (pointing to context-data) from RDI */
movq %rdi, %rsp // 将第一个参数存入rsp寄存器中,即rsp寄存器指向前面堆上填充好的上下文,也就是指向4.3小节堆栈示意图中rax
// 寄存器指向的位置
movq 0x40(%rsp), %r8 /* restore return-address */
// 将rsp为基址上面64字节位置的值存入r8寄存器中,也就是r8寄存器中存的是trampoline标签的相对地址
movq 0x10(%rsp), %r12 /* restore R12 */
movq 0x18(%rsp), %r13 /* restore R13 */
movq 0x20(%rsp), %r14 /* restore R14 */
movq 0x28(%rsp), %r15 /* restore R15 */
movq 0x30(%rsp), %rbx /* restore RBX */
movq 0x38(%rsp), %rbp /* restore RBP */
// 将所有的值回填到寄存器中,此时R12,R13,R14,R15这些寄存器中存的值无用,rbx寄存器跟4.3小节堆栈示意图中rdx寄存器存的值一
// 样,rbx寄存器指向的是第三个参数函数调用地址。rbp寄存器存的是finish标签的相对地址
leaq 0x48(%rsp), %rsp /* prepare stack */ // rsp寄存器指针上移72字节(指的是寄存器中的值的改变,个人叫法),此时和上
// 面make_fcontext中下移72字节匹配上了
/* RAX == fctx, RDX == data */
movq %rsi, %rdx // 把第二个参数存入rdx寄存器中
movq %rax, %rdi // 把rax寄存器的值存入rdi寄存器中,rdi寄存器指向普通栈的栈顶位置,之所以这么做,是因为函数调用的参数传递
/* indirect jump to context */
jmp *%r8 // 无条件跳转r8寄存器指向的位置,也就是trampoline标签的位置,接下来要跳转回到make_fcontext中,找到这个标签下面
// 的汇编指令代码
push %rbp // 把rbp寄存器中的值压栈,此时有个问题压栈到哪儿去,并不是压到普通栈上,因为此时rsp寄存器指向是worker——context
// 堆栈上的位置,具体位置上面提到过,压栈操作还是遵循向低地址扩展的规则,所以rbp寄存器中的值存入了堆栈上存trampoline标签相
// 对地址的上面的8个字节,此时trampoline标签相对地址这个地方上下都是一样的,存的都是finish标签的相对地址,第二个问题是为什
// 么要存,是因为trampoline标签的相对地址上面的所有堆栈上的信息是在整个协程调用切换过程中要一直存在,而它下面(包括他自己)
// 的所有堆栈空间用于协程内部自己的栈空间,所以在协程运行过程中会不断覆写的,所以要把finish标签的相对地址换个位置,这就充分
// 利用了堆栈空间
/* jump to context-function */
jmp *%rbx // 跳转到rbx寄存器指向的位置,也就是make_fcontext函数的第三个参数函数调用的入口地址,这不是正常的函数调用,是直
// 接无条件跳转所以参数的传递要自己构造,这也就是为什么上面要把rax寄存器的值存入到rdi寄存器中
4.5、堆栈示意图
虚线框中%rax/%rdi寄存器此时不指向这个位置,是指向右边中的位置,画在这儿是后面需要用到这个地方来说明
5、回到源码
5.1、继续构造boost::fibers::fiber对象
step1:跳转到make_fcontext的第三个参数的函数入口地址,transfer_t是一个结构体,中有两个指针元素,所以通过寄存器传值,其中 fctx是第一个元素,所以通过rdi寄存器传值,通过上面堆栈示意图可以看出,rax寄存器指向主线程普通栈的栈顶位置,data是第二个元素,所以通过rsi寄存器传值,rsi中存的是jump_fcontext函数的第二个参数,也就是record,所以可以使用static_cast安全转换,接下来再次调用jump_fcontext再次从worker_context的堆栈上跳回主线程的普通栈上,首先要把当前worker_context的上下文也就是各种寄存器的值存回堆栈中,然后rax寄存器的值和rsp寄存器的值互换,rsp栈顶指针指回到普通栈上,然后恢复普通栈的寄存器的值,最后跳转到r8寄存器中的值,也就上面提到过的下一条指令的地址,此时就重新回到create_fiber2函数的最后的return中,jump_fcontext返回值是一个结构体,结构体中两个指针,return返回的是第一个指针,所以通过寄存器传出返回值,只需关注rdi寄存器,此时rdi寄存器指向4.5小节堆栈示意图中左边虚线框中,所以return返回的是这个地址,此时对象的构造完成,然后一层一层的返回回去,在make_worker_context_with_properties函数中return语句构造了一个intrusive_ptr对象,其实就是自定义的结构用于封装worker_context,可以封装各种context,不止此处的worker_context还有后面的其他的context
step2:执行start_函数,如下,
context * ctx = context::active();第一行代码很重要,在构造协程调度器以及协程的分发上下文对象
step3:接着进入第一行代码中,如下,初始化一个静态的context_initializer对象,thread_local作用是每一个线程有一个自己的这个对象,不受其他线程影响,静态的只会一次初始化,下次在进入active之中,就不会执行这行代码,直接返回当前活跃的上下文,继续跳转
如下图,再次保证只进入一次,继续跳转就在下面
如下图,首先new default_scheduler(),看着是在new一个默认的协程调度器对象,实际上new出来的并不是协程调度器对象,而是协程调度策略对象,这里是默认的调度策略algo::round_robin(轮询),接着make_stack_allocator_wrapper是在new一个polymorphic_stack_allocator_impl对象,这个对象中保存着allocate句柄,就是前面用来真正在堆上开辟worker_context空间的对象,但此时不是原来的那个对象,是新new了一个出来
// main fiber context of this thread
context * main_ctx = new main_context{}; // 这是构造了一个main_context对象,这个对象不同于worer_context对象,没有worker_context构造的那么复杂
// scheduler of this thread
auto sched = new scheduler(algo); // 这才是真正的在构造协程调度器对象,并把协程调度策略对象传入进去
// attach main context to scheduler
sched->attach_main_context( main_ctx); // 这行代码是要让调度器知道当前活跃的上下文是上面new出来的main_context对象,同时也要让这个对象知道是哪个调度器在调度自己
step4:接下来这行代码很重要,是在构造一个dispatcher_context,make_dispatcher_context构造dispatcher_context的方法同woker_context构造一模一样,接着看attach_dispatcher_context的具体实现,如下,首先就是要让协程调度器知道自己的分发上下文对象是谁,就是给dispatcher_ctx_赋值make_dispatcher_context构造出来的对象,其次要让分发上下文对象知道调度自己的调度器是谁
step5:接着看awakened函数是怎么实现的,如下,是调用dispatcher_context对象的ready_link方法,并把rqueue_传进去,rqueue_是协程调度器对象的协程调度策略对象所拥有的
接着就是ready_link的实现,把当前对象加入到rqueue_末尾;综上,awakened函数的作用就是把dispatcher_context对象加入到协程调度策略对象的就绪队列中
// make main context to active context
active_ = main_ctx; // 初始化最后的一行代码,就是设定当前线程活跃的上下文是main_context对象
5.2、回到start_()函数中attach()函数的实现
当前是main_context在调用attach()方法,首先获取调度器,因为前面设置过,所以main_context知道是哪个调度器在调度自己,接着就是调度器调用attach_worker_context方法,并把ctx当参数传入,此时ctx是前面构造出来的worker_context对象
attach_worker_context函数的实现,如下,接着在调用worker_context对象的方法worker_link,并把worker_queue_参数传入,worker_queue_是调度器所拥有的属性,接下来是让worker_context对象知道那个调度器在调度自己
继续看worker_link的实现,如下,同样是把worker_context对象加入到调度器的worker就绪队列中
5.3、回到start_()中继续执行
get_policy是获取worker_context的policy_属性,默认情况下是launch::post,所以走第一个case分支,首先通过main_context对象获取调度器对象,然后调用它的schedule方法,在此方法中调用algo_->awakened(ctx)方法,把worker_context对象加入到协程调度策略对象的就绪队列中,至此boost::fibers::fiber对象构造完成
5.4、调用join执行协程
step1:以自己写的demo中的join为入口,开始调用协程,如下,接着是worker_context对象调用自己的join方法
step2:获取当前活跃的上下文,因为在前面已经初始化过调度器以及dispatcher_context对象,所以这里不会再初始化,只会返回当前活跃的上下文对象,此时活跃的上下文对象是main_context
step3:接着调用worker_context对象的属性wait_queue_的suspend_and_wait方法,如下,此时的active_ctx是main_context对象,用其构造了一个waker_with_hook对象,然后加入到worker_context对象的等待队列的末尾
step4:接着调用mian_context对象的suspend方法,如下
step5:然后再次执行调度器对象的suspend方法,如下,调用协程调度器策略对象的pick_next方法获取下一个等待执行的上下文对象,此时得到的应该是dispatcher_context上下文对象
step6:接着调用其resume方法唤醒自己,执行完swap之后,prev中是main_context对象,active_中是dispatcher_context对象,接着dispatcher_context对象调用自己属性的方法唤醒自己执行,
step7:resume_with的实现如下,调用ontop_fcontext,ontop_fcontext是通过汇编指令实现的,功能和jump_fcontext大致相似,保存当前的上下文恢复dispatcher_context的上下文,唯一不一样的就是跳转到fiber_ontop函数入口地址,传入的参数就是函数调用对象p,同时使用exchange把dispatcher_context对象的fctx_返回之后赋值为nullptr
step8:fiber_ontop的实现,如下,data存的就是resume_with传入的lambda表达式,fctx存的是跳转前的上下文的地址,此时是要从main_context上下文切换到dispatcher_context上下文,在p函数中把main_context上下文的入口地址保存在了main_context对象中,执行完之后就该执行return,(断言输出fctx_是空的,不知道返回回去有什么意思,我猜测一下是为了调用析构,将本函数的局部变量弹出栈),执行return之前要把dispatcher_context对象堆栈的局部变量弹栈出去,然后此时rsp寄存器栈顶存的就是下一条要执行的指令,下一条指令就是在构造dispatcher_context对象上下文的时候,调用make_fcontext的第三个参数是一个函数的入口地址,在此函数中执行第二次jump_fcontext,从dispatcher_context上下文跳回到普通栈上,这个fiber_entry函数是没有执行完的所以局部变量等信息还是存在dispatcher_context堆栈上的,所以下一条指令就是此函数中jump_fcontext的下面一行代码t.fctx = rec->run(t.fctx),fctx指的是main_context上下文的入口
step9:接着要执行record对象的run函数,在fiber_record类中,如下,invoke函数最终会调用dispatcher_context对象中的run_函数,invoke中的细节就不再说了,感兴趣的自己去看
stepp10:获取dispatcher_context对象绑定的协程调度器,然后调用dispatch函数
dispatch中有如下一行关键代码,是从协程调度策略对象的就绪队列中找到下一个就绪的上下文,此时得到的下一个上下文是worker_context
// get next ready context
context * ctx = algo_->pick_next();
还有一行重要的代码,ctx是下一个要唤醒的上下文,也就是worker_context,传入的参数是当前活跃的上下文
ctx->resume( dispatcher_ctx_.get() );
step11:所做的操作和上面从main_context上下文切换到dispatcher_context上下文大致一样,我要说的是这个传入的lambda表达式中多了一行代码context::active()->schedule(ready_ctx);这行代码的作用是把dispatcher_context对象再次添加到协程调度策略对象的就绪队列最后
step12:跳转到worker_context上下文进行继续执行,如下,worker_context也是在jump_fcontext中跳转出去的,因此恢复回来之后就该执行下一条指令,t.fctx = rec->run(t.fctx);也是要执行fiber_record类中的run函数,在此函数中又要调用worker_context对象的run_函数
step13:如下,在此方法中fn才是用户自己写的函数调用对象,arg是用户自己写的函数的参数,apply函数中才是真的调用用户自己写的协程函数
step14:用户自己写的协程函数,如下,此方法中boost::this_fiber::yield();是当前活跃的协程主动让出,让调度器调度下一个协程,一步一步调试进去最终还是执行algo_->pick_next()->resume( ctx);这行代码,接下来协程切换操作和上面dispatcher_context上下文切换到worker_context上下文一模一样
5.5、协程结束操作
step1:还是从用户写的代码入手,如下,执行完puts,这个协程就算是执行完成了,需要回到worker_context对象的run_函数中,执行terminate函数
step2:terminate函数的实现,如下,通知所有等待在worker_context对象的wait_queue_队列上的上下文,然后就是return中调度器调用terminate方法,并把锁和worker_context对象传入。wait_queue_.notify_all();这行代码会把worker_context对象的wait_queue_中存的main_context上下文对象再次放到协程调度策略对象的就绪队列中
step3:调度器的terminate实现,如下,最重要的就是最后一行代码,调度策略对象获取下一个就绪的协程上下文对象,并调用该对象的suspend_with_cc方法
step4:suspend_with_cc的具体实现,如下,此时是从worker_context上下文切换到main_context上下文,然后结束整个join的调度
step5:接着就是main函数的return,在整个程序结束之前,因为context_initializer是构造的类中static成员,在程序结束时会调用它的析构函数,如下,在析构函数中会把协程的调度器对象析构,以及main_context对象析构
step6:接下来就是调度器的析构具体实现,将变量shutdown_修改为true,context::active()->suspend();代码调用下一个上下文,此时只剩一个上下文也就是dispatcher_context上下文,跳转到这儿执行
接下来因为shutdown_前面修改为true,执行break跳出for死循环,最后跳转到finish标签执行,最后就是main函数的return,至此整个程序执行完成
6、个人小结
对于多个boost::fibers::fiber对象,也就是多个协程,会在调用第一个join结束之后,执行完所有的协程。以上是我个人对boost库中fibers的理解,如有问题欢迎各位大佬指正,下一篇文章是协程调度策略的分析:Boost库协程策略分析
#c++协程解析#