在 libgo 的上下文切換上,並無本身去實現建立和維護棧空間、保存和切換 CPU 寄存器執行狀態信息等的任務,而是直接使用了 Boost.Context。Boost.Context 做爲衆多協程底層支持庫,性能方面一直在被優化。編程
Boost.Context所作的工做,就是在傳統的線程環境中能夠保存當前執行的抽象狀態信息(棧空間、棧指針、CPU寄存器和狀態寄存器、IP指令指針),而後暫停當前的執行狀態,程序的執行流程跳轉到其餘位置繼續執行,這個基礎構建能夠用於開闢用戶態的線程,從而構建出更加高級的協程等操做接口。同時由於這個切換是在用戶空間的,因此資源損耗很小,同時保存了棧空間和執行狀態的全部信息,因此其中的函數能夠自由被嵌套使用。
引用自 https://yq.aliyun.com/ziliao/43404ide
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); };
當用戶去管理協程棧當時候,稍不注意,就會出現訪問棧越界當問題。只讀操做還好,可是若是進行了寫操做,整個程序就會直接奔潰,所以,棧保護工做仍是十分必要的。ui
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; } }
取消棧保護只有在釋放該協程空間的時候會調用。rest
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()); } }
#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.
libgo/context/context.hcode
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
該彙編實現的
雙斜槓後的中文註釋是本身新加的
彙編實現的函數,其實是
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整除的地址開始分配
X86-64 的全部寄存器都是 64 位,相對於 32 位系統來講,僅僅是標識符發生變化,如 %ebp->%rbp;
# X86-64 寄存器說明 %rax 做爲函數返回值使用 %rsp 棧指針寄存器,指向棧頂 %rdi,%rsi,%rdx,%rcx,%r8,%r9 用做函數參數,依次對應第1參數,第2參數。。。 %rbx,%rbp,%r12,%r13,%14,%15 用做數據存儲,遵循被調用者使用規則,簡單說就是隨便用,調用子函數以前要備份它,以防他被修改 %r10,%r11 用做數據存儲,遵循調用者使用規則,簡單說就是使用以前要先保存原值