libco源碼解析(3) 協程執行,co_resume

libco源碼解析(1) 協程運行與基本結構
libco源碼解析(2) 創建協程,co_create
libco源碼解析(3) 協程執行,co_resume
libco源碼解析(4) 協程切換,coctx_make與coctx_swap
libco源碼解析(5) poll
libco源碼解析(6) co_eventloop
libco源碼解析(7) read,write與條件變量
libco源碼解析(8) hook機制探究
libco源碼解析(9) closure實現

引言

我們知道協程在由co_create創建以後其實是沒有運行的,需要我們顯式的執行co_resume纔可以,這裏顯然是一個比較麻煩的過程,因爲這裏涉及到了協程的切換,也就意味着我們需要操作寄存器,這裏就需要使用到一些彙編代碼。

正文

我們先來看看co_resume的函數邏輯吧!

void co_resume( stCoRoutine_t *co )
{
	// stCoRoutine_t結構需要我們在我們的代碼中自行調用co_release或者co_free
	stCoRoutineEnv_t *env = co->env;

	// 獲取當前正在進行的協程主體 
	stCoRoutine_t *lpCurrRoutine = env->pCallStack[ env->iCallStackSize - 1 ];
	if( !co->cStart ) // 
	{
		coctx_make( &co->ctx,(coctx_pfn_t)CoRoutineFunc,co,0 ); //保存上一個協程的上下文
		co->cStart = 1;
	}
	// 把此次執行的協程控制塊放入調用棧中
	env->pCallStack[ env->iCallStackSize++ ] = co;
	// co_swap() 內部已經切換了 CPU 執行上下文
	co_swap( lpCurrRoutine, co );
	
}

我們可以看到首先從線程唯一的env中的調用棧中拿到了調用此協程的協程實體,也就是現在正在運行的這個協程的實體。我們首先要明確一個問題,就是co_resume並不是只調用一次,伴隨着協程主動讓出執行權,它總要被再次執行,靠的就是這個co_resume函數,無非第一次調用的時候需要初始化寄存器信息,後面不用罷了。

co->cStart標記了這個協程是否是第一次執行co_resume,關於coctx_make,我打算用一篇單獨的文章講解,因爲確實比較麻煩。我們先來看看其他的邏輯,現在暫且知道coctx_make所做的事情就是爲coctx_swap函數的正確執行去初始化co->ctx中寄存器信息。

然後就是把此次執行的協程放入調用棧中並自增iCallStackSize,iCallStackSize自增後是調用棧中目前有的協程數量。

stStackMem_t && stShareStack_t

接下來就是核心函數co_swap,它執行了協程的切換,並做了一些其他的工作。不過在co_swap之前我希望先說說libco的兩種協程棧的策略,一種是一個協程分配一個棧,這也是默認的配置,不過缺點十分明顯,因爲默認大小爲128KB,如果1024個協程就是128MB,1024*1024個協程就是128GB,好像和協程“千萬連接”相差甚遠。且這些空間中顯然有很多的空隙,可能很多協程只用了1KB不到,這顯然是一種極大的浪費。所以還有另一種策略,即共享棧。看似高大上,實則沒什麼意思,還記得我此一次看到這個名詞的時候非常疑惑,想了很久如何才能高效的實現一個共享棧,思考無果後查閱libco源碼,出乎意料,libco的實現並不高效,但是能跑,且避免了默認配置的情況。答案就是在進行協程切換的時候把已經使用的內存進行拷貝。這樣一個線程所有的協程在運行時使用的確實是同一個棧,也就是我們所說的共享棧了

我們先來看看棧和共享棧的結構

struct stStackMem_t
{
	stCoRoutine_t* occupy_co; // 執行時佔用的那個協程實體,也就是這個棧現在是那個協程在用
	int stack_size;			//當前棧上未使用的空間
	char* stack_bp; 		//stack_buffer + stack_size
	char* stack_buffer;		//棧的起始地址,當然對於主協程來說這是堆上的空間

};

// 共享棧中多棧可以使得我們在進程切換的時候減少拷貝次數
struct stShareStack_t
{
	unsigned int alloc_idx;		// stack_array中我們在下一次調用中應該使用的那個共享棧的index
	int stack_size;				// 共享棧的大小,這裏的大小指的是一個stStackMem_t*的大小
	int count;					// 共享棧的個數,共享棧可以爲多個,所以以下爲共享棧的數組
	stStackMem_t** stack_array;	// 棧的內容,這裏是個數組,元素是stStackMem_t*
};

基本註釋都說清楚了,stStackMem_t沒什麼說的。我們來看看stShareStack_t,其中有一個參數count,是共享棧的個數。共享棧爲什麼還能有多個?這是一個對於共享棧的優化,可以減少內容的拷貝數。我們知道共享棧在切換協程的時候會執行拷貝,把要切換出去的協程的棧內容進行拷貝,但是如果要當前協程和要切換的協程所使用的棧不同,拷貝這一步當然就可以省略了。

我們來看看在co_create中的co_get_stackmem是如何在共享棧結構中分配棧的:

static stStackMem_t* co_get_stackmem(stShareStack_t* share_stack)
{
	if (!share_stack)
	{
		return NULL;
	}
	int idx = share_stack->alloc_idx % share_stack->count;
	share_stack->alloc_idx++;

	return share_stack->stack_array[idx];
}

我們可以看到邏輯非常簡單,就是一個輪詢。

好了,準備知識說完了,可以開始co_swap的解析了。

co_swap

// 當前準備讓出 CPU 的協程叫做 current 協程,把即將調入執行的叫做 pending 協程
void co_swap(stCoRoutine_t* curr, stCoRoutine_t* pending_co)
{
 	stCoRoutineEnv_t* env = co_get_curr_thread_env();

	// get curr stack sp
	// 獲取esp指針, 
	char c;
	curr->stack_sp= &c; 

	if (!pending_co->cIsShareStack)
	{ 
		env->pending_co = NULL;
		env->occupy_co = NULL;
	}
	else // 如果採用了共享棧
	{
		env->pending_co = pending_co;
		// get last occupy co on the same stack mem
		// 獲取pending使用的棧空間的執行協程
		stCoRoutine_t* occupy_co = pending_co->stack_mem->occupy_co;
		// 也就是當前正在執行的進程
		// set pending co to occupy thest stack mem
		// 將該共享棧的佔用者改爲pending_co
		pending_co->stack_mem->occupy_co = pending_co;

		env->occupy_co = occupy_co;
		if (occupy_co && occupy_co != pending_co)
		{
			// 如果上一個使用協程不爲空,則需要把它的棧內容保存起來
			save_stack_buffer(occupy_co); 
		}
	}

	//swap context 這個函數執行完, 就切入下一個協程了
	coctx_swap(&(curr->ctx),&(pending_co->ctx) );

	//stack buffer may be overwrite, so get again;
	stCoRoutineEnv_t* curr_env = co_get_curr_thread_env();
	stCoRoutine_t* update_occupy_co =  curr_env->occupy_co; 
	stCoRoutine_t* update_pending_co = curr_env->pending_co;
	
	if (update_occupy_co && update_pending_co && update_occupy_co != update_pending_co)
	{
		//resume stack buffer
		if (update_pending_co->save_buffer && update_pending_co->save_size > 0)
		{
			// 如果是一個協程執行到一半然後被切換出去然後又切換回來,這個時候需要恢復棧空間
			memcpy(update_pending_co->stack_sp, update_pending_co->save_buffer, update_pending_co->save_size);
		}
	}
}

首先我們可以看到令人疑惑的一句代碼:

	char c;
	curr->stack_sp= &c; 

說實話,在第一次看到的時候,實在是疑惑至極,這玩意在幹嘛,但仔細想,我們上面說了當協程選擇共享棧的時候需要在協程切換是拷貝棧上已使用的內存。問題來了,拷貝哪一部分呢?

|------------|
|	 used    |
|------------|
|     esp    |
|------------|
|   ss_size  |
|------------|
|stack_buffer|
|------------|

答案就是usd部分,首先棧底我們可以很容易的得到。就是棧基址加上棧大小,但是esp怎麼獲取呢?再寫一段彙編代碼?可以,但沒必要。libco採用了一個極其巧妙的方法,個人認爲這是libco最精妙的幾段代碼之一。就是直接用一個char類型的指針放在函數頭,獲取esp。這樣我們就得到了需要保存的數據範圍了。

然後就是一段更新env中pengdingoccupy_co的代碼,最後執行共享棧中棧的保存。

coctx_swap是libco的核心,即一段彙編代碼,功能爲把傳入的兩個把當前寄存器的值存入curr中,把pending中的值存入寄存器中,也就是我們所說的上下文切換。這段代碼和coctx_make一樣放在下一篇文章中,現在知其然就可以啦。

我們腦子要清楚一件事情。就是coctx_swap執行完以後,CPU就跑去執行pendding中的代碼了,也就是說執行完執行coctx_swap的這條語句後,下一條要執行的語句不是stCoRoutineEnv_t* curr_env = co_get_curr_thread_env();,而是pedding中的語句。這一點要尤其注意。

那麼時候執行coctx_swap這條語句之後的語句呢?就是在協程被其他地方執行co_resume了以後纔會繼續執行這裏。

後面就簡單啦,切換出去的時候要把棧內容拷貝下來,切換回來當然又要拷貝到棧裏面啦。

這就是協程的執行過程。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章