協程如今已經不是個新東西了,不少語言都提供了原生支持,也有不少開源的庫也提供了協程支持。php
最近爲了要給tbox增長協程,特意研究了下各大開源協程庫的實現,例如:libtask, libmill, boost, libco, libgo等等。linux
他們都屬於stackfull協程,每一個協程有完整的私有堆棧,裏面的核心就是上下文切換(context),而stackless的協程,比較出名的有protothreads,這個比較另類,有興趣的同窗能夠去看下源碼,這裏就很少說了。git
那麼現有協程庫,是怎麼去實現context切換的呢,目前主要有如下幾種方式:github
各個協程協程庫的切換效率的基準測試,能夠參考:切換效率基準測試報告macos
要研究ucontext,其實只要看下libtask的實現就好了,很是經典,這套接口其實效率並非很高,並且不少平臺已經標記爲廢棄接口了(像macosx),目前主要是在linux下使用windows
libtask裏面對不提供此接口的平臺,進行了彙編實現,已達到跨平臺的目的,架構
ucontext相關接口,主要有以下四個:app
下面給個簡單的例子:less
#include <stdio.h> #include <ucontext.h> static ucontext_t ctx[3]; static void func1(void) { // 切換到func2 swapcontext(&ctx[1], &ctx[2]); // 返回後,切換到ctx[1].uc_link,也就是main的swapcontext返回處 } static void func2(void) { // 切換到func1 swapcontext(&ctx[2], &ctx[1]); // 返回後,切換到ctx[2].uc_link,也就是func1的swapcontext返回處 } int main (void) { // 初始化context1,綁定函數func1和堆棧stack1 char stack1[8192]; getcontext(&ctx[1]); ctx[1].uc_stack.ss_sp = stack1; ctx[1].uc_stack.ss_size = sizeof(stack1); ctx[1].uc_link = &ctx[0]; makecontext(&ctx[1], func1, 0); // 初始化context2,綁定函數func2和堆棧stack2 char stack2[8192]; getcontext(&ctx[2]); ctx[2].uc_stack.ss_sp = stack2; ctx[2].uc_stack.ss_size = sizeof(stack1); ctx[2].uc_link = &ctx[1]; makecontext(&ctx[2], func2, 0); // 保存當前context,而後切換到context2上去,也就是func2 swapcontext(&ctx[0], &ctx[2]); return 0; }
那這套接口的實現原理是什麼呢,咱們能夠拿libtask的arm彙編實現,來看下,其餘平臺也相似。函數
/* get mcontext * * @param mcontext r0 * * @return r0 */ .globl getmcontext getmcontext: /* 保存全部當前寄存器,包括sp和lr */ str r1, [r0, #4] // mcontext.mc_r1 = r1 str r2, [r0, #8] // mcontext.mc_r2 = r2 str r3, [r0, #12] // mcontext.mc_r3 = r3 str r4, [r0, #16] // mcontext.mc_r4 = r4 str r5, [r0, #20] // mcontext.mc_r5 = r5 str r6, [r0, #24] // mcontext.mc_r6 = r6 str r7, [r0, #28] // mcontext.mc_r7 = r7 str r8, [r0, #32] // mcontext.mc_r8 = r8 str r9, [r0, #36] // mcontext.mc_r9 = r9 str r10, [r0, #40] // mcontext.mc_r10 = r10 str r11, [r0, #44] // mcontext.mc_fp = r11 str r12, [r0, #48] // mcontext.mc_ip = r12 str r13, [r0, #52] // mcontext.mc_sp = r13 str r14, [r0, #56] // mcontext.mc_lr = r14 // 設置從setcontext切換回getcontext後,從getcontext返回的值爲1 mov r1, #1 /* mcontext.mc_r0 = 1 * * if (getcontext(ctx) == 0) * setcontext(ctx); * * getcontext() will return 1 after calling setcontext() */ str r1, [r0] // 返回0 mov r0, #0 // return 0 mov pc, lr /* set mcontext * * @param mcontext r0 */ .globl setmcontext setmcontext: // 恢復指定context的全部寄存器,包括sp和lr ldr r1, [r0, #4] // r1 = mcontext.mc_r1 ldr r2, [r0, #8] // r2 = mcontext.mc_r2 ldr r3, [r0, #12] // r3 = mcontext.mc_r3 ldr r4, [r0, #16] // r4 = mcontext.mc_r4 ldr r5, [r0, #20] // r5 = mcontext.mc_r5 ldr r6, [r0, #24] // r6 = mcontext.mc_r6 ldr r7, [r0, #28] // r7 = mcontext.mc_r7 ldr r8, [r0, #32] // r8 = mcontext.mc_r8 ldr r9, [r0, #36] // r9 = mcontext.mc_r9 ldr r10, [r0, #40] // r10 = mcontext.mc_r10 ldr r11, [r0, #44] // r11 = mcontext.mc_fp ldr r12, [r0, #48] // r12 = mcontext.mc_ip ldr r13, [r0, #52] // r13 = mcontext.mc_sp ldr r14, [r0, #56] // r14 = mcontext.mc_lr // 設置getcontext的返回值 ldr r0, [r0] // r0 = mcontext.mc_r0 // 切換到getcontext的返回處,繼續執行 mov pc, lr // return
其實說白了,就是對寄存器進行保存和恢復的過程,切換原理很簡單
而後外面只須要用宏包裹下,就好了:
#define setcontext(u) setmcontext(&(u)->uc_mcontext) #define getcontext(u) getmcontext(&(u)->uc_mcontext)
而對於makecontext,主要的工做就是設置 函數指針 和 堆棧 到對應context保存的sp和pc寄存器中,這也就是爲何makecontext調用前,必需要先getcontext下的緣由。
void makecontext(ucontext_t *uc, void (*fn)(void), int argc, ...) { int i, *sp; va_list arg; // 將函數參數陸續設置到r0, r1,r2 .. 等參數寄存器中 sp = (int*)uc->uc_stack.ss_sp + uc->uc_stack.ss_size / 4; va_start(arg, argc); for(i=0; i<4 && i<argc; i++) uc->uc_mcontext.gregs[i] = va_arg(arg, uint); va_end(arg); // 設置堆棧指針到sp寄存器 uc->uc_mcontext.gregs[13] = (uint)sp; // 設置函數指針到lr寄存器,切換時會設置到pc寄存器中進行跳轉到fn uc->uc_mcontext.gregs[14] = (uint)fn; }
這套接口簡單有效,不支持的平臺還能夠經過彙編實現來支持,看上去已經很完美了,可是確有個問題,就是效率不高,由於每次切換保存和恢復的寄存器太多。
以後能夠看下boost.context的實現,就能夠對比出來了,下面先簡單講講setjmp的切換。。
libmill裏面的切換主要用的就是此套接口,其實應該是sigsetjmp/siglongjmp,不只保存了寄存器,還保存了signal mask。。
經過切換效率基準測試報告,能夠看到libmill在x86_64架構上,切換很是的快
實際上是由於針對這個平臺,libmill沒有使用原生sigsetjmp/siglongjmp接口,而是本身彙編實現了一套,作了些優化,而且去掉了signal mask的保存。
#if defined(__x86_64__) #if defined(__AVX__) #define MILL_CLOBBER \ , "ymm0", "ymm1", "ymm2", "ymm3", "ymm4", "ymm5", "ymm6", "ymm7",\ "ymm8", "ymm9", "ymm10", "ymm11", "ymm12", "ymm13", "ymm14", "ymm15" #else #define MILL_CLOBBER #endif #define mill_setjmp_(ctx) ({\ int ret;\ asm("lea LJMPRET%=(%%rip), %%rcx\n\t"\ "xor %%rax, %%rax\n\t"\ "mov %%rbx, (%%rdx)\n\t"\ "mov %%rbp, 8(%%rdx)\n\t"\ "mov %%r12, 16(%%rdx)\n\t"\ "mov %%rsp, 24(%%rdx)\n\t"\ "mov %%r13, 32(%%rdx)\n\t"\ "mov %%r14, 40(%%rdx)\n\t"\ "mov %%r15, 48(%%rdx)\n\t"\ "mov %%rcx, 56(%%rdx)\n\t"\ "mov %%rdi, 64(%%rdx)\n\t"\ "mov %%rsi, 72(%%rdx)\n\t"\ "LJMPRET%=:\n\t"\ : "=a" (ret)\ : "d" (ctx)\ : "memory", "rcx", "r8", "r9", "r10", "r11",\ "xmm0", "xmm1", "xmm2", "xmm3", "xmm4", "xmm5", "xmm6", "xmm7",\ "xmm8", "xmm9", "xmm10", "xmm11", "xmm12", "xmm13", "xmm14", "xmm15"\ MILL_CLOBBER\ );\ ret;\ }) #define mill_longjmp_(ctx) \ asm("movq (%%rax), %%rbx\n\t"\ "movq 8(%%rax), %%rbp\n\t"\ "movq 16(%%rax), %%r12\n\t"\ "movq 24(%%rax), %%rdx\n\t"\ "movq 32(%%rax), %%r13\n\t"\ "movq 40(%%rax), %%r14\n\t"\ "mov %%rdx, %%rsp\n\t"\ "movq 48(%%rax), %%r15\n\t"\ "movq 56(%%rax), %%rdx\n\t"\ "movq 64(%%rax), %%rdi\n\t"\ "movq 72(%%rax), %%rsi\n\t"\ "jmp *%%rdx\n\t"\ : : "a" (ctx) : "rdx" \ ) #else #define mill_setjmp_(ctx) \ sigsetjmp(*ctx, 0) #define mill_longjmp_(ctx) \ siglongjmp(*ctx, 1) #endif
通過測試分析,其實libc自帶的sigsetjmp/siglongjmp在不一樣平臺下,效率上表現差別很大,並且切換也比setjmp/longjmp的慢了很多
因此libmill除了優化過的x86_64平臺,在其餘arch上切換效果並非很理想,徹底依賴libc的實現效率。。
所以後來再封裝tbox的協程庫的時候,並無考慮此方案。
這套接口,我以前用來封裝setcontext/getcontext的時候,也實現並測試過,效果很是不理想,很是的慢,比用libtask那套純彙編的實現慢了10倍左右,直接放棄了
不過這套接口用起來仍是很方便,跟ucontext相似,徹底能夠用來模擬封裝成ucontext的使用方式,例如:
// getcontext GetThreadContext(GetCurrentThread(), mcontext); // setcontext SetThreadContext(GetCurrentThread(), mcontext);
而makecontext,我貼下以前寫的一些實現,不過如今已經廢棄了,僅供參考:
tb_bool_t makecontext(tb_context_ref_t context, tb_pointer_t stack, tb_size_t stacksize, tb_context_func_t func, tb_cpointer_t priv) { // check LPCONTEXT mcontext = (LPCONTEXT)context; tb_assert_and_check_return_val(mcontext && stack && stacksize && func, tb_false); // make stack address tb_long_t* sp = (tb_long_t*)stack + stacksize / sizeof(tb_long_t); // push arguments tb_uint64_t value = tb_p2u64(priv); *--sp = (tb_long_t)(tb_uint32_t)(value); *--sp = (tb_long_t)(tb_uint32_t)(value >> 32); // push return address(unused, only reverse the stack space) *--sp = 0; /* save function and stack address * * sp + 8: arg2 * sp + 4: arg1 * sp: return address(0) => esp */ mcontext->Eip = (tb_long_t)func; mcontext->Esp = (tb_long_t)sp; tb_assert_static(sizeof(tb_long_t) == 4); // save and restore the full machine context mcontext->ContextFlags = CONTEXT_FULL; // ok return tb_true; }
原理跟libtask的那個相似,就是修改esp和eip寄存器而已,具體實現能夠參考我以前的commit
這套接口,目前還沒測試過,不過看msdn介紹,使用仍是很方便的,不過部分xp系統上,並不提供此接口,須要較高版本的系統支持
所以爲了考慮跨平臺,tbox暫時沒去考慮使用,有興趣的同窗能夠研究下。
其實一開始tbox是參考libtask的ucontext彙編實現,封裝了一套context切換,當時其實已經封裝的差很少了,可是後來作benchbox的基準測試
把boost的切換一對比,直接就被秒殺了,哎。。而後去看boost的context實現源碼,雖然對boost自己並非太喜歡,可是底層的context是實現,確實很是精妙,不得不佩服。
它主要有兩個接口,一個make_fcontext()
,一個jump_fcontext()
,我在tbox的平臺庫裏面參考其實現,進行了封裝,使用方式跟boost相似,所以直接以tbox的使用爲例:
static tb_void_t func1(tb_context_from_t from) { // 獲取切換時傳入的contexts參數 tb_context_ref_t* contexts = (tb_context_ref_t*)from.priv; // 保存原始context contexts[0] = from.context; // 切換到func2 from = tb_context_jump(contexts[2], contexts); // 從func2返回後,切換回main tb_context_jump(contexts[0], tb_null); } static tb_void_t func2(tb_context_from_t from) { // 獲取切換時傳入的contexts參數 tb_context_ref_t* contexts = (tb_context_ref_t*)from.priv; // 切換到func1 from = tb_context_jump(from.context, contexts); // 從func1返回後,切換回main tb_context_jump(contexts[0], tb_null); } int main(int argc, char** argv) { // the stacks static tb_context_ref_t contexts[3]; static tb_byte_t stacks1[8192]; static tb_byte_t stacks2[8192]; // 經過stack1和func1生成context1 contexts[1] = tb_context_make(stacks1, sizeof(stacks1), func1); // 經過stack2和func2生成context2 contexts[2] = tb_context_make(stacks2, sizeof(stacks2), func2); // 切換到func1,而且傳入contexts做爲參數 tb_context_jump(contexts[1], contexts); }
其中tb_context_make
至關於boost的make_fcontext
, tb_context_jump
至關於boost的jump_fcontext
相比ucontext,boost的切換模式,少了單獨對context進行保存(getcontext)和切換(setcontext)過程,而是把二者合併到一塊兒,經過jump_fcontext接口實現直接切換。
這樣作有個好處,就是更加容易進行優化,使得整個切換過程更加的緊湊,咱們先來看下macosx平臺x86_64的實現,這個比較簡單易懂些。。
這裏我就直接貼tbox的代碼了,實現差很少的,只不過多了些註釋而已。
/* make context (refer to boost.context) * * ------------------------------------------------------------------------------- * stackdata: | | context ||||||| * -------------------------------------------------------------------------|----- * (16-align for macosx) * * * ------------------------------------------------------------------------------- * context: | r12 | r13 | r14 | r15 | rbx | rbp | rip | end | ... * ------------------------------------------------------------------------------- * 0 8 16 24 32 40 48 56 | * | 16-align for macosx * | * esp when jump to function * * @param stackdata the stack data (rdi) * @param stacksize the stack size (rsi) * @param func the entry function (rdx) * * @return the context pointer (rax) */ function(tb_context_make) // 保存棧頂指針到rax addq %rsi, %rdi movq %rdi, %rax /* 先對棧指針進行16字節對齊 * * * ------------------------------ * context: | retaddr | padding ... | * ------------------------------ * | | * | 此處16字節對齊 * | * esp到此處時,會進行ret * * 這麼作,主要是由於macosx下,對調用棧佈局進行了優化,在保存調用函數返回地址的堆棧處,須要進行16字節對齊,方便利用SIMD進行優化 */ movabs $-16, %r8 andq %r8, %rax // 保留context須要的一些空間,由於context和stack是在一塊兒的,stack底指針就是context leaq -64(%rax), %rax // 保存func函數地址到context.rip movq %rdx, 48(%rax) /* 保存__end地址到context.end,若是在在func返回時,沒有指定jump切換到有效context * 那麼會繼續會執行到此處,程序也就退出了 */ leaq __end(%rip), %rcx movq %rcx, 56(%rax) // 返回rax指向的棧底指針,做爲context返回 ret __end: // exit(0) xorq %rdi, %rdi #ifdef TB_ARCH_ELF call _exit@PLT #else call __exit #endif hlt endfunc /* jump context (refer to boost.context) * * @param context the to-context (rdi) * @param priv the passed user private data (rsi) * * @return the from-context (context: rax, priv: rdx) */ function(tb_context_jump) // 保存寄存器,而且按佈局構形成當前context,包括jump()自身的返回地址retaddr(rip) pushq %rbp pushq %rbx pushq %r15 pushq %r14 pushq %r13 pushq %r12 // 保存當前棧基址rsp,也就是contex,到rax中 movq %rsp, %rax // 切換到指定的新context上去,也就是切換堆棧 movq %rdi, %rsp // 而後按context上的棧佈局依次恢復寄存器 popq %r12 popq %r13 popq %r14 popq %r15 popq %rbx popq %rbp // 獲取context.rip,也就是make時候指定的func函數地址,或者是對方context中jump()調用的返回地址 popq %r8 // 設置返回值(from.context: rax, from.priv: rdx),也就是來自對方jump()的context和傳遞參數 movq %rsi, %rdx // 傳遞當前(context: rax, priv: rdx),做爲function(from)函數調用的入口參數 movq %rax, %rdi /* 跳轉切換到make時候指定的func函數地址,或者是對方context中jump()調用的返回地址 * * 切換過去後,此時的棧佈局以下: * * end是func的返回地址,也就是exit * * ------------------------------- * context: .. | end | args | padding ... | * ------------------------------- * 0 8 * | | * rsp 16-align for macosx */ jmp *%r8 endfunc
關於apple棧佈局16字節對齊優化問題,能夠參考:http://fabiensanglard.net/macosxassembly/index.php
借用下里面的圖哈,能夠看下:
boost的context和stack是一塊兒的,棧底指針就是context,設計很是巧妙,切換context就是切換stack,一箭雙鵰,可是這樣每次切換就必須更新context
由於每次切換context後,context地址都會變掉。
// 切換返回時,須要更新from.context的地址 from = tb_context_jump(from.context, contexts);
如今能夠和getcontext/setcontext對比下,就能夠看出,這種切換方式的一些優點:
1. 保存和恢復寄存器數據,在一個切換接口中,更加容易進行優化 2. 經過stack基棧做爲context,切換棧至關於切換了context,一箭雙鵰,指令數更少 3. 經過push/pop操做保存寄存器,比mov等方式指令字節數更少,更加精簡 4. 對參數、可變寄存器沒去保存,僅保存部分必須的寄存器,進一步減小指令數
爲了實現跨平臺,boost下各個架構的實現,我都研究了一遍,發現macosx i386的實現,是有問題的,運行會掛掉,裏面直接照搬了linux elf的i386實現版本。
估計macosx i386用的很少,因此沒去作測試,後來發現,原來macosx i386下jump()返回from(context, priv)的結構體並非基於棧的
而是使用eax, edx返回,所以tbox裏面針對這個架構,從新調整stack佈局,重寫了一套本身的實現。
其實在windows下,返回from(context, priv)的結構體,也是用的eax, edx,而不是像linux elf那樣基於棧的,所以實現上效率會高不少。
可是,boost裏面,卻像elf那個版本同樣,仍是採用了一個跳板,進行二次跳轉後,才切換到context上去,是沒有必要的。
在boost裏面的跳板代碼,相似像這樣(摘錄自tbox elf i386的實現):
__entry: /* pass arguments(context: eax, priv: edx) to the context function * * patch __end * | * | old-context * ----|------------------------------------ * context: .. | retval | context | priv | padding | * ----------------------------------------- * 0 4 arguments * | | * esp 16-align * (now) */ movl %eax, (%esp) movl %edx, 0x4(%esp) // retval = the address of label __end pushl %ebp /* jump to the context function entry * * @note need not adjust stack pointer(+4) using 'ret $4' when enter into function first */ jmp *%ebx
因爲elf i386下,返回from結構體是基於棧的,因此進入function入口的棧,和切換到對方jump()返回處的棧,並非徹底平衡的,所以須要一個跳板區分對待
stack佈局上也須要特殊處理,而windows i386的返回,只須要eax/edx就足夠,不必再去使用這個跳板。
所以,tbox裏面針對這個平臺,進行了優化,從新調整了棧佈局,省去跳板操做,直接進行跳轉,實測切換效率比boost的實現提高30%左右。