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);

libgo 源碼剖析(3. libgo上下文切換實現)

指令說明

# 僞指令
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 用作數據存儲,遵循調用者使用規則,簡單說就是使用之前要先保存原值
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章