c++雲風coroutineライブラリ解析

28261 ワード

雲風coroutin eライブラリはC言語で実現される軽量レベルのコモンシップライブラリであり、ソースコードは簡潔で分かりやすく、(ru)解(keng)コモンシップ原理の最良のソースコードリソースと言える.私は前の文章の中で、テンセントのオープンソースのlibcoを借りて、C/C++の協力の実現について簡単に紹介して、ブログを参考にします.実はlibcoと雲風coroutineには似たような思想がたくさんありますが、実現の方法が異なるだけで、雲風庫は実現の構想を提供しただけで、hookを処理していません.libcoは工業級の協力庫で実現しています.両者のソースコードの読解分析を通じて、異なる実現方式の違いを比較することができて、多くソースコードライブラリの作者がどうしてこのように設計して、自分でそれに対して改善することができて、このように自分の向上に対してとても役に立ちます.
設計構想分析
クラウド風庫は主にucontextクラスタ関数を利用してコモンコンテキスト切替を行い、ucontextクラスタ関数の最大の点は簡単で使いやすいが、切替の性能はlibco設計のアセンブリロジックに及ばない(主な原因はucontext内部実装コンテキストが不要なレジスタを多く切替、libcoアセンブリ実装の切替はより簡潔で直接的である).主に4つの関数が含まれています.
//           ucp
getcontext(ucontext_t *ucp)
//   ucp           
setcontext(const ucontext_t *ucp)
//         
makecontext(ucontext_t *ucp, void (*func)(), int argc, ...) 
//         oucp,     ucp     
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp)

//ucontext   
typedef struct ucontext{
	//         ,  uc_link      
	//   NULL       
    struct ucontext* uc_link;
    //              
    sigset_t uc_sigmask;
    //        
    stack_t uc_stack;
    //           
    mcontext_t uc_mcontext;
    ...
}ucontext_t

APIの設計面では、雲風庫のAPIの設計は非常に簡潔で、主に以下の設計である.
//         
#define COROUTINE_DEAD 0
#define COROUTINE_READY 1
#define COROUTINE_RUNNING 2
#define COROUTINE_SUSPEND 3

//         
struct schedule * coroutine_open(void);
//       
void coroutine_close(struct schedule *);

//      
int coroutine_new(struct schedule *, coroutine_func, void *ud);
//      (     )
void coroutine_resume(struct schedule *, int id);
//         
int coroutine_status(struct schedule *, int id);
//         ID
int coroutine_running(struct schedule *);
//yeild       ,      
void coroutine_yield(struct schedule *);

コンポジットスタックの設計では、クラウド風ライブラリも共有スタックを選択する実装であり、すなわち、各コンポジットはスタック内のデータを自分で保持し、resumeが実行されるたびに自分のデータcopyを実行スタックに転送し、yieldのたびに実行スタックのデータ(まずスタックの底とスタックの頂上を見つける)を自分のコンポジット構造体に保存する.この方法の利点は、最初に大きなスタックメモリ(雲風ライブラリのデフォルトは1 M)を初期化するだけで、実行時のデータをその上に置くことができ、爆スタックの問題を考慮することなく、各コモンパスの1つの自分のスタックに比べてスタックメモリの利用率が高いことです.欠点は,コヒーレントスイッチングのたびにユーザ状態におけるcopyプロセスが用いられることである.次に、それがどのように実現されるかを見ることができます.
ソース分析
次に,その主な論理実装を見る.クラウドリポジトリには主に2つの構造体があります.一つはスケジューラで、一つは協程です.
//     
struct schedule {
	char stack[STACK_SIZE];	//    1MB,      ,           
	ucontext_t main;		//      
	int nco;				//            
	int cap;				//           。        。 nco >= cap     
	int running;			//       ID
	struct coroutine **co;	//    ,         
};

//  
struct coroutine {
	coroutine_func func;	//      
	void *ud;				//    
	ucontext_t ctx;			//     
	struct schedule * sch;	//        
	ptrdiff_t cap;			//            (stack   )
	ptrdiff_t size;			//            (stack   )
	int status;				//    
	char *stack;			//            ,         ,
	//                ,                     
};

次にcoroutineを見てみましょうresumeのソースコードです.この関数は指定されたコモンシップをオンにします.コモンシップが初めて実行され、COROUTINE_からステータスが表示されます.READY -> COROUTINE_RUNNING、もう一つはコパス前に実行したがyield、再度実行し、状態はCOROUTINE_SUSPEND -> COROUTINE_RUNNING.
//mainfunc         ,              ,            
static void
mainfunc(uint32_t low32, uint32_t hi32) {
	uintptr_t ptr = (uintptr_t)low32 | ((uintptr_t)hi32 << 32);
	struct schedule *S = (struct schedule *)ptr;
	int id = S->running;
	struct coroutine *C = S->co[id];
	C->func(S,C->ud);	//      
	_co_delete(C);
	S->co[id] = NULL;
	--S->nco;
	S->running = -1;
}

//       (  )  
void 
coroutine_resume(struct schedule * S, int id) {
	//             ,  id    
	assert(S->running == -1);
	assert(id >=0 && id < S->cap);
	struct coroutine *C = S->co[id];
	if (C == NULL)
		return;
	int status = C->status;
	switch(status) {
	case COROUTINE_READY:
		//          ,     ,     ,getcontext              ,
		getcontext(&C->ctx);
		C->ctx.uc_stack.ss_sp = S->stack;	//     (              ,        ,        1M  )
		C->ctx.uc_stack.ss_size = STACK_SIZE;
		C->ctx.uc_link = &S->main;			//uc_link         ,           
		S->running = id;
		C->status = COROUTINE_RUNNING;
		//     S    ,  mainfunc  ,        uint32_t,    mainfunc   
		uintptr_t ptr = (uintptr_t)S;
		makecontext(&C->ctx, (void (*)(void)) mainfunc, 2, (uint32_t)ptr, (uint32_t)(ptr>>32));
		swapcontext(&S->main, &C->ctx);
		break;
	case COROUTINE_SUSPEND:
		//       yield ,memcpy          copy    
		//         yield resume        copy,                   copy
		memcpy(S->stack + STACK_SIZE - C->size, C->stack, C->size);
		S->running = id;
		C->status = COROUTINE_RUNNING;
		swapcontext(&S->main, &C->ctx);
		break;
	default:
		assert(0);
	}
}

次にcoroutineを見てみましょうyieldの実現.yieldは、コヒーレントスケジューラで現在実行されているコヒーレントを中断し、ユーザー状態で別のコヒーレント実行を切り替えます.中断の原因は、IOを待っているときや、システム呼び出しを待っているとき、ネットワークデータを待っているときなど、クラウドリポジトリでは実現されていません.具体的にはlibcoでhookしている関数を見てみましょう.ここで問題なのは、yieldの過程で、スタックを実行するデータcopyを出して、どのようにスタックを実行するデータを見つけますか?プロセスアドレス空間では、スタックは高アドレスから低アドレスに延びていることを知っています.つまり、スタックの底は高アドレスで、スタックの頂は低アドレスで、copyスタックの中のデータを見つけるには、スタックの頂とスタックの底のアドレスを見つけて、その中のデータmemcpyを出せばいいだけです.スタックの底は探しやすいです.S->stack+STACK_です.SIZE,スタックトップは1つのdummyオブジェクトを利用して,S->stackとdummyオブジェクトのアドレスを減算することができ,すなわちスタックの現在の長さである.
//          ,  coroutine       ,                。
static void
_save_stack(struct coroutine *C, char *top) {
	//  dump      (    )
	char dummy = 0;
	assert(top - &dummy <= STACK_SIZE);
	if (C->cap < top - &dummy) {
		free(C->stack);
		C->cap = top-&dummy;
		C->stack = malloc(C->cap);
	}
	C->size = top - &dummy;
	memcpy(C->stack, &dummy, C->size);
}

//            ,        ,       while(),           
void
coroutine_yield(struct schedule * S) {
	int id = S->running;
	assert(id >= 0);
	struct coroutine * C = S->co[id];
	assert((char *)&C > S->stack);
	_save_stack(C,S->stack + STACK_SIZE);//             ,S->stack + STACK_SIZE   (    )
	C->status = COROUTINE_SUSPEND;
	S->running = -1;
	swapcontext(&C->ctx , &S->main);
}

参照先:
  • 雲風のBLOG
  • libco協程概要