您好,登錄后才能下訂單哦!
在 libgo 的上下文切換上,并沒有自己去實現(xiàn)創(chuàng)建和維護(hù)??臻g、保存和切換 CPU 寄存器執(zhí)行狀態(tài)信息等的任務(wù),而是直接使用了 Boost.Context。Boost.Context 作為眾多協(xié)程底層支持庫,性能方面一直在被優(yōu)化。
Boost.Context所做的工作,就是在傳統(tǒng)的線程環(huán)境中可以保存當(dāng)前執(zhí)行的抽象狀態(tài)信息(??臻g、棧指針、CPU寄存器和狀態(tài)寄存器、IP指令指針),然后暫停當(dāng)前的執(zhí)行狀態(tài),程序的執(zhí)行流程跳轉(zhuǎn)到其他位置繼續(xù)執(zhí)行,這個基礎(chǔ)構(gòu)建可以用于開辟用戶態(tài)的線程,從而構(gòu)建出更加高級的協(xié)程等操作接口。同時因為這個切換是在用戶空間的,所以資源損耗很小,同時保存了??臻g和執(zhí)行狀態(tài)的所有信息,所以其中的函數(shù)可以自由被嵌套使用。
引用自 https://yq.aliyun.com/ziliao/43404
libgo/context/fcontext.h
Boost.Context 的底層實現(xiàn)是通過 fcontext_t 結(jié)構(gòu)體來保存協(xié)程狀態(tài),使用 make_fcontext 創(chuàng)建協(xié)程,使用 jump_fcontext 實現(xiàn)協(xié)程切換。在 libgo 協(xié)程中,直接引用了這兩個接口函數(shù)。boost 的內(nèi)部實現(xiàn)這里不討論,感興趣的話可以在上面連接中查看。
// 所有內(nèi)容和 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);
/*
* 創(chuàng)建上下問對象
* */
fcontext_t make_fcontext(void* stack, std::size_t size, fn_t fn);
}
除此之外,還提供了一系列的棧函數(shù)
struct StackTraits
{
static stack_malloc_fn_t& MallocFunc();
static stack_free_fn_t& FreeFunc();
// 獲取當(dāng)前棧頂設(shè)置的保護(hù)頁的頁數(shù)
static int & GetProtectStackPageSize();
// 對保護(hù)頁的內(nèi)容做保護(hù)
static bool ProtectStack(void* stack, std::size_t size, int pageSize);
// 取消對保護(hù)頁的內(nèi)存保護(hù),析構(gòu)是才會調(diào)用
static void UnprotectStack(void* stack, int pageSize);
};
當(dāng)用戶去管理協(xié)程棧當(dāng)時候,稍不注意,就會出現(xiàn)訪問棧越界當(dāng)問題。只讀操作還好,但是如果進(jìn)行了寫操作,整個程序就會直接奔潰,因此,棧保護(hù)工作還是十分必要的。
libgo 對棧對保護(hù),使用了 mprotect 系統(tǒng)調(diào)用實現(xiàn)。我們在給該協(xié)程創(chuàng)建了大小為 N 字節(jié)對棧空間時,會對棧頂?shù)囊徊糠值目臻g進(jìn)行保護(hù),因此,分配的協(xié)程棧的大小,應(yīng)該要大于要保護(hù)的內(nèi)存頁數(shù)加一。
為什么提到保護(hù)棧,總是以頁為單位呢?因為 mprotect 是按照頁來進(jìn)行設(shè)置的,因此,對沒有對其對地址,應(yīng)該首先對其之后再去操作。
bool StackTraits::ProtectStack(void* stack, std::size_t size, int pageSize)
{
// 協(xié)程棧的大小,應(yīng)該大于(保護(hù)內(nèi)存頁數(shù)+1)
if (!pageSize) return false;
if ((int)size <= getpagesize() * (pageSize + 1))
return false;
// 使用 mprotect 保護(hù)的內(nèi)存頁應(yīng)該是按頁對其的
// 棧從高地址向地地址生長,被保護(hù)的棧空間應(yīng)該位于棧頂(低地址處)
// protect_page_addr 是在當(dāng)前協(xié)程棧內(nèi)取最近的整數(shù)頁邊界的地址,如:0xf7234008 ---> 0xf7235000
void *protect_page_addr = ((std::size_t)stack & 0xfff) ? (void*)(((std::size_t)stack & ~(std::size_t)0xfff) + 0x1000) : stack;
// 使用 mprotect 系統(tǒng)調(diào)用實現(xiàn)棧保護(hù),PROT_NONE 表明該內(nèi)存空間不可訪問
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;
}
}
取消棧保護(hù)只有在釋放該協(xié)程空間的時候會調(diào)用。
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;
// 允許該塊內(nèi)存可讀可寫
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:應(yīng)該是按頁對其的內(nèi)存地址
len:保護(hù)的內(nèi)存頁大小,因此保護(hù)的地址范圍應(yīng)該是[addr, addr+len-1]
prot:保護(hù)類型
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.h
Context 是 libgo 中封裝的上下文對象,每個協(xié)程都會有一份獨有的。
class Context
{
public:
/*
* 構(gòu)造
* */
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_; // 協(xié)程運行函數(shù)
intptr_t vp_; // 當(dāng)前上下文屬于的協(xié)程 Task 對象指針
char* stack_ = nullptr; // ??臻g
uint32_t stackSize_ = 0; // 棧大小
int protectPage_ = 0; // 保護(hù)頁的數(shù)量
};
該類除了私有成員,其它的沒有什么解釋的。大多數(shù)的工作都是在構(gòu)造函數(shù)中完成的,包括開辟??臻g、創(chuàng)建上下文、設(shè)置保護(hù)頁等的操作。
關(guān)于棧保護(hù)頁的頁數(shù)設(shè)置,還有默認(rèn)的棧大小,都是在 CoroutineOptions 中配置的。在 coroutine.h 文件中
#define co_opt ::co::CoroutineOptions::getInstance()
因此,可以直接使用 co_opt 對象來修改默認(rèn)配置。
可參照
test/gtest_unit/protect.cpp
該匯編實現(xiàn)的
雙斜杠后的中文注釋是自己新加的
匯編實現(xiàn)的函數(shù),實際上是
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:
// 保存當(dāng)前協(xié)程的數(shù)據(jù)存儲寄存器,壓棧保存
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 字節(jié),為新協(xié)程 FPU 浮點運算預(yù)留
/* prepare stack for FPU 浮點運算寄存器*/
leaq -0x8(%rsp), %rsp
// %rcx 為函數(shù)的第四個參數(shù),je 進(jìn)行判斷,等于則跳轉(zhuǎn)到標(biāo)識為1的地方,f(forword)
// fpu 為浮點運算寄存器
/* test for flag preserve_fpu */
cmp $0, %rcx
je 1f
// 保存MXCSR內(nèi)容 rsp 寄存器
/* save MMX control- and status-word */
stmxcsr (%rsp)
// 保存當(dāng)前FPU狀態(tài)字到 rsp+4 的位置
/* save x87 control-word */
fnstcw 0x4(%rsp)
1:
// 保存當(dāng)前棧頂位置到 rdi
/* store RSP (pointing to context-data) in RDI */
movq %rsp, (%rdi)
// 修改棧頂?shù)刂?,為新協(xié)程的地址
/* 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 字節(jié),恢復(fù)為 FPU 浮點運算預(yù)留空間
/* prepare stack for FPU */
leaq 0x8(%rsp), %rsp
// 將當(dāng)前新協(xié)程的寄存器恢復(fù)
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
// 原協(xié)程所屬的 task 作為函數(shù)返回值存入 rax 寄存器
/* use third arg as return-value after jump */
movq %rdx, %rax
// 將當(dāng)前協(xié)程的 task 地址放到第一個參數(shù)的位置(即替換當(dāng)前協(xié)程的上下文地址)
/* use third arg as first arg in context function */
movq %rdx, %rdi
// 跳轉(zhuǎn)到返回地址處
/* indirect jump to context */
jmp *%r8
.size jump_fcontext,.-jump_fcontext
以從協(xié)程 A 切換到協(xié)程 B 為例:
intptr_t jump_fcontext(fcontext_t * ofc, fcontext_t nfc, intptr_t vp, bool preserve_fpu = false);
# 偽指令
text:
指定了后續(xù)編譯出來的內(nèi)容放在代碼段【可執(zhí)行】;
global:
告訴編譯器后續(xù)跟的是一個全局可見的名字【可能是變量,也可以是函數(shù)名】;
align num:
對齊偽指令,num 必須是2的整數(shù)冪
告訴匯編程序,本偽指令下面的內(nèi)存變量必須從下一個能被Num整除的地址開始分配
X86-64 的所有寄存器都是 64 位,相對于 32 位系統(tǒng)來說,僅僅是標(biāo)識符發(fā)生變化,如 %ebp->%rbp;
# X86-64 寄存器說明
%rax 作為函數(shù)返回值使用
%rsp 棧指針寄存器,指向棧頂
%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函數(shù)參數(shù),依次對應(yīng)第1參數(shù),第2參數(shù)。。。
%rbx,%rbp,%r12,%r13,%14,%15 用作數(shù)據(jù)存儲,遵循被調(diào)用者使用規(guī)則,簡單說就是隨便用,調(diào)用子函數(shù)之前要備份它,以防他被修改
%r10,%r11 用作數(shù)據(jù)存儲,遵循調(diào)用者使用規(guī)則,簡單說就是使用之前要先保存原值
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權(quán)內(nèi)容。