libgo 源码剖析(3. libgo上下文切换实现)

在 libgo 的上下文切换上,并没有自己去实现创建和维护栈空间、保存和切换 CPU 寄存器执行状态信息等的任务,而是直接使用了 Boost.Context。Boost.Context 作为众多协程底层支持库,性能方面一直在被优化。

Boost.Context所做的工作,就是在传统的线程环境中可以保存当前执行的抽象状态信息(栈空间、栈指针、CPU寄存器和状态寄存器、IP指令指针),然后暂停当前的执行状态,程序的执行流程跳转到其他位置继续执行,这个基础构建可以用于开辟用户态的线程,从而构建出更加高级的协程等操作接口。同时因为这个切换是在用户空间的,所以资源损耗很小,同时保存了栈空间和执行状态的所有信息,所以其中的函数可以自由被嵌套使用。
引用自 https://yq.aliyun.com/ziliao/43404

1. fcontext_t

libgo/context/fcontext.h

Boost.Context 的底层实现是通过 fcontext_t 结构体来保存协程状态,使用 make_fcontext 创建协程,使用 jump_fcontext 实现协程切换。在 libgo 协程中,直接引用了这两个接口函数。boost 的内部实现这里不讨论,感兴趣的话可以在上面连接中查看。

// 所有内容和 Boost.Context 中的声明一致
extern "C"
{
    typedef void* fcontext_t;
    typedef void (*fn_t)(intptr_t);

    /*
    * 从 ofc 切换到 nfc 的上下文
    * */
    intptr_t jump_fcontext(fcontext_t * ofc, fcontext_t nfc,intptr_t vp, bool preserve_fpu = false);

    /*
    * 创建上下问对象
    * */
    fcontext_t make_fcontext(void* stack, std::size_t size, fn_t fn);
}

除此之外,还提供了一系列的栈函数

struct StackTraits
{
    static stack_malloc_fn_t& MallocFunc();

    static stack_free_fn_t& FreeFunc();

    // 获取当前栈顶设置的保护页的页数
    static int & GetProtectStackPageSize();

    // 对保护页的内容做保护
    static bool ProtectStack(void* stack, std::size_t size, int pageSize);

    // 取消对保护页的内存保护,析构是才会调用
    static void UnprotectStack(void* stack, int pageSize);
};

当用户去管理协程栈当时候,稍不注意,就会出现访问栈越界当问题。只读操作还好,但是如果进行了写操作,整个程序就会直接奔溃,因此,栈保护工作还是十分必要的。

栈保护

libgo 对栈对保护,使用了 mprotect 系统调用实现。我们在给该协程创建了大小为 N 字节对栈空间时,会对栈顶的一部分的空间进行保护,因此,分配的协程栈的大小,应该要大于要保护的内存页数加一。

为什么提到保护栈,总是以页为单位呢?因为 mprotect 是按照页来进行设置的,因此,对没有对其对地址,应该首先对其之后再去操作。

bool StackTraits::ProtectStack(void* stack, std::size_t size, int pageSize)
{
    // 协程栈的大小,应该大于(保护内存页数+1)
    if (!pageSize) return false;
    if ((int)size <= getpagesize() * (pageSize + 1))
        return false;

    // 使用 mprotect 保护的内存页应该是按页对其的
    // 栈从高地址向地地址生长,被保护的栈空间应该位于栈顶(低地址处)
    // protect_page_addr 是在当前协程栈内取最近的整数页边界的地址,如:0xf7234008 ---> 0xf7235000
    void *protect_page_addr = ((std::size_t)stack & 0xfff) ? (void*)(((std::size_t)stack & ~(std::size_t)0xfff) + 0x1000) : stack;

    // 使用 mprotect 系统调用实现栈保护,PROT_NONE 表明该内存空间不可访问
    if (-1 == mprotect(protect_page_addr, getpagesize() * pageSize, PROT_NONE)) {
        DebugPrint(dbg_task, "origin_addr:%p, align_addr:%p, page_size:%d, protect_page:%u, protect stack stack error: %s", stack, protect_page_addr, getpagesize(), pageSize, strerror(errno));
        return false;
    } else {
        DebugPrint(dbg_task, "origin_addr:%p, align_addr:%p, page_size:%d, protect_page:%u, protect stack success.",
        stack, protect_page_addr, pageSize, getpagesize());
        return true;
    }
}

取消栈保护

取消栈保护只有在释放该协程空间的时候会调用。

void StackTraits::UnprotectStack(void *stack, int pageSize)
{
    if (!pageSize) return ;

    void *protect_page_addr = ((std::size_t)stack & 0xfff) ? (void*)(((std::size_t)stack & ~(std::size_t)0xfff) + 0x1000) : stack;

    // 允许该块内存可读可写
    if (-1 == mprotect(protect_page_addr, getpagesize() * pageSize, PROT_READ|PROT_WRITE)) {
        DebugPrint(dbg_task, "origin_addr:%p, align_addr:%p, page_size:%d, protect_page:%u, protect stack stack error: %s",stack, protect_page_addr, getpagesize(), pageSize, strerror(errno));
    } else {
        DebugPrint(dbg_task, "origin_addr:%p, align_addr:%p, page_size:%d, protect_page:%u, protect stack success.", stack, protect_page_addr, pageSize, getpagesize());
    }
}

mprotect 系统调用使用说明

#include <sys/mman.h>
int mprotect(void *addr, size_t len, int prot);
    addr:应该是按页对其的内存地址
    len:保护的内存页大小,因此保护的地址范围应该是[addr, addr+len-1]
    prot:保护类型
        PROT_NONE  The memory cannot be accessed at all.
        PROT_READ  The memory can be read.
        PROT_WRITE The memory can be modified.
        PROT_EXEC  The memory can be executed.

2. Context

libgo/context/context.h

Context 是 libgo 中封装的上下文对象,每个协程都会有一份独有的。

class Context
{
public:
    /*
    * 构造
    * */
    Context(fn_t fn, intptr_t vp, std::size_t stackSize);

    // 上下文切换接口
    ALWAYS_INLINE void SwapIn();
    ALWAYS_INLINE void SwapTo(Context & other);
    ALWAYS_INLINE void SwapOut();
    fcontext_t& GetTlsContext();

private:
    fcontext_t ctx_;
    fn_t fn_;       // 协程运行函数
    intptr_t vp_;   // 当前上下文属于的协程 Task 对象指针
    char* stack_ = nullptr;     // 栈空间
    uint32_t stackSize_ = 0;    // 栈大小
    int protectPage_ = 0;       // 保护页的数量
};

该类除了私有成员,其它的没有什么解释的。大多数的工作都是在构造函数中完成的,包括开辟栈空间、创建上下文、设置保护页等的操作。

默认配置

关于栈保护页的页数设置,还有默认的栈大小,都是在 CoroutineOptions 中配置的。在 coroutine.h 文件中

#define co_opt ::co::CoroutineOptions::getInstance()

因此,可以直接使用 co_opt 对象来修改默认配置。

可参照

test/gtest_unit/protect.cpp

3. 汇编实现上下文切换

该汇编实现的

双斜杠后的中文注释是自己新加的

汇编实现的函数,实际上是
intptr_t jump_fcontext(fcontext_t * ofc, fcontext_t nfc,intptr_t vp, bool preserve_fpu = false);

汇编代码如下:

.text
// 声明 jump_fcontext 为全局可见的符号
.globl jump_fcontext
.type jump_fcontext,@function
.align 16
jump_fcontext:
    // 保存当前协程的数据存储寄存器,压栈保存
    pushq  %rbp  /* save RBP */
    pushq  %rbx  /* save RBX */
    pushq  %r15  /* save R15 */
    pushq  %r14  /* save R14 */
    pushq  %r13  /* save R13 */
    pushq  %r12  /* save R12 */

    // rsp 栈顶寄存器下移 8 字节,为新协程 FPU 浮点运算预留
    /* prepare stack for FPU 浮点运算寄存器*/
    leaq  -0x8(%rsp), %rsp

    // %rcx 为函数的第四个参数,je 进行判断,等于则跳转到标识为1的地方,f(forword)
    // fpu 为浮点运算寄存器
    /* test for flag preserve_fpu */
    cmp  $0, %rcx
    je  1f

    // 保存MXCSR内容 rsp 寄存器
    /* save MMX control- and status-word */
    stmxcsr  (%rsp)
    // 保存当前FPU状态字到 rsp+4 的位置
    /* save x87 control-word */
    fnstcw   0x4(%rsp)

1:
    // 保存当前栈顶位置到 rdi
    /* store RSP (pointing to context-data) in RDI */
    movq  %rsp, (%rdi)

    // 修改栈顶地址,为新协程的地址
    /* restore RSP (pointing to context-data) from RSI */
    movq  %rsi, %rsp

    /* test for flag preserve_fpu */
    cmp  $0, %rcx
    je  2f

    /* restore MMX control- and status-word */
    ldmxcsr  (%rsp)
    /* restore x87 control-word */
    fldcw  0x4(%rsp)

2:
    // rsp 栈顶寄存器上移 8 字节,恢复为 FPU 浮点运算预留空间
    /* prepare stack for FPU */
    leaq  0x8(%rsp), %rsp

    // 将当前新协程的寄存器恢复
    popq  %r12  /* restrore R12 */
    popq  %r13  /* restrore R13 */
    popq  %r14  /* restrore R14 */
    popq  %r15  /* restrore R15 */
    popq  %rbx  /* restrore RBX */
    popq  %rbp  /* restrore RBP */

    // 将返回地址放到 r8 寄存器中
    /* restore return-address */
    popq  %r8

    // 原协程所属的 task 作为函数返回值存入 rax 寄存器
    /* use third arg as return-value after jump */
    movq  %rdx, %rax

    // 将当前协程的 task 地址放到第一个参数的位置(即替换当前协程的上下文地址)
    /* use third arg as first arg in context function */
    movq  %rdx, %rdi

    // 跳转到返回地址处
    /* indirect jump to context */
    jmp  *%r8
.size jump_fcontext,.-jump_fcontext

切换流程

以从协程 A 切换到协程 B 为例:

intptr_t jump_fcontext(fcontext_t * ofc, fcontext_t nfc, intptr_t vp, bool preserve_fpu = false);

指令说明

# 伪指令
text:
    指定了后续编译出来的内容放在代码段【可执行】;
global:
    告诉编译器后续跟的是一个全局可见的名字【可能是变量,也可以是函数名】;
align num:
    对齐伪指令,num 必须是2的整数幂
    告诉汇编程序,本伪指令下面的内存变量必须从下一个能被Num整除的地址开始分配

寄存器说明

  1. X86-64 的所有寄存器都是 64 位,相对于 32 位系统来说,仅仅是标识符发生变化,如 %ebp->%rbp;
  2. X86-64 新增 %r8~%r15 8个寄存器;
# X86-64 寄存器说明

%rax 作为函数返回值使用

%rsp 栈指针寄存器,指向栈顶

%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。。。

%rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改

%r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值

原文地址:http://blog.51cto.com/muhuizz/2330678

时间: 2024-08-01 17:58:05

libgo 源码剖析(3. libgo上下文切换实现)的相关文章

libgo 源码剖析(1. libgo简介与调度浅谈)

闲谈 协程是一个很早的概念了,早些年的游戏行业中已经大规模地在使用,像lua.go这些语言中的协程原语已经相对比较完善了,一般来说直接使用就好,但是在系统后台开发上,出现的时间并不长.我是做C++方向的后台开发,目前国内一些公司也开源了一些C++协程库,但目前来说,还是在逐步完善的阶段.最早接触的C++协程库是腾讯微信的 libco,可以说是相当轻量级的协程,网上关于libco的实现的文章也是相对较多,这里的话不会去过多地叙述. 在网上查找关于 libgo 的资料,关于代码实现的文章并没有多少,

libgo 源码剖析(2. libgo调度策略源码实现)

本文将从源码实现上对 libgo 的调度策略进行分析,主要涉及到上一篇文章中的三个结构体的定义: 调度器 Scheduler(简称 S) 执行器 Processer(简称 P) 协程 Task(简称 T) 三者的关系如下图所示: 本文会列出类内的主要成员和主要函数做以分析. 1. 协程调度器:class Scheduler libgo/scheduler/scheduler.h class Scheduler{ public: /* * 创建一个调度器,初始化 libgo * 创建主线程的执行器

下载-深入浅出Netty源码剖析、Netty实战高性能分布式RPC、NIO+Netty5各种RPC架构实战演练三部曲视频教程

下载-深入浅出Netty源码剖析.Netty实战高性能分布式RPC.NIO+Netty5各种RPC架构实战演练三部曲视频教程 第一部分:入浅出Netty源码剖析 第二部分:Netty实战高性能分布式RPC 第三部分:NIO+Netty5各种RPC架构实战演练

Phaser实现源码剖析

在这里首先说明一下,由于Phaser在4.3代码里是存在,但并没有被开放出来供使用,但已经被本人大致研究了,因此也一并进行剖析. Phaser是一个可以重复利用的同步栅栏,功能上与CyclicBarrier和CountDownLatch相似,不过提供更加灵活的用法.也就是说,Phaser的同步模型与它们差不多.一般运用的场景是一组线程希望同时到达某个执行点后(先到达的会被阻塞),执行一个指定任务,然后这些线程才被唤醒继续执行其它任务. Phaser一般是定义一个parties数(parties一

【Java集合源码剖析】HashMap源码剖析

转载请注明出处:http://blog.csdn.net/ns_code/article/details/36034955 HashMap简介 HashMap是基于哈希表实现的,每一个元素是一个key-value对,其内部通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长. HashMap是非线程安全的,只是用于单线程环境下,多线程环境下可以采用concurrent并发包下的concurrentHashMap. HashMap 实现了Serializable接口,因此它支持序列化,

转:【Java集合源码剖析】Vector源码剖析

转载请注明出处:http://blog.csdn.net/ns_code/article/details/35793865   Vector简介 Vector也是基于数组实现的,是一个动态数组,其容量能自动增长. Vector是JDK1.0引入了,它的很多实现方法都加入了同步语句,因此是线程安全的(其实也只是相对安全,有些时候还是要加入同步语句来保证线程的安全),可以用于多线程环境. Vector没有丝线Serializable接口,因此它不支持序列化,实现了Cloneable接口,能被克隆,实

下载BootStrap企业级应用培训课程(零基础、源码剖析,内部教材,项目实训)

全套500多课,附赠JS OOP编程,转一播放码.下载地址:http://pan.baidu.com/s/1kVLdZmf 第一季:基础篇,侧重于BootStrap 相关 API 详解.主要包含以下内容:Brackets前端开发工具详解.BootStrap框架三大核心-CSS.BootStrap框架三大核心-布局组件.BootStrap框架三大核心-JavaScript插件.附-BootStrap编码规范第二季:高级篇,侧重于BootStap源码解析与第三方扩展.主要包含以下内容:BootStr

菜鸟nginx源码剖析 框架篇(一) 从main函数看nginx启动流程(转)

俗话说的好,牵牛要牵牛鼻子 驾车顶牛,处理复杂的东西,只要抓住重点,才能理清脉络,不至于深陷其中,不能自拔.对复杂的nginx而言,main函数就是“牛之鼻”,只要能理清main函数,就一定能理解其中的奥秘,下面我们就一起来研究一下nginx的main函数. 1.nginx的main函数解读 nginx启动显然是由main函数驱动的,main函数在在core/nginx.c文件中,其源代码解析如下,涉及到的数据结构在本节仅指出其作用,将在第二节中详细解释. nginx main函数的流程图如下:

HashMap(2) 源码剖析(推荐)

今天看代码,想到去年发生的HashMap发生的CPU使用率100%的事件,转载下当时看的三个比较不错的博客(非常推荐) 参考:http://coolshell.cn/articles/9606.html   http://github.thinkingbar.com/hashmap-analysis/ http://developer.51cto.com/art/201102/246431.htm 在 Java 集合类中,使用最多的容器类恐怕就是 HashMap 和 ArrayList 了,所以