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
函數的具體實現。
正文
我們在上一篇文章中說過libco創建協程的接口爲了程序員可以更快的接收,採取了類似於線程的創建方法,我們看看co_create
的函數定義
int co_create( stCoRoutine_t **ppco,const stCoRoutineAttr_t *attr,pfn_co_routine_t pfn,void *arg )
ppco
是協程的主體結構,存儲着一個協程所有的信息。attr
其實和線程一樣,是我們希望創建的協程的一些屬性,不過libco中這個參數簡單一點,只是標記了棧的大小和是否使用共享棧。pfn
是我們希望協程執行的函數,當然實際執行的是一個封裝後的函數,後面我們會看到。arg
沒什麼說的,是傳入函數的參數。
搞清楚了各個參數的意義,接下來我們就來看看co_create的函數實現吧!
int co_create( stCoRoutine_t **ppco,const stCoRoutineAttr_t *attr,pfn_co_routine_t pfn,void *arg )
{
if( !co_get_curr_thread_env() ) // 是一個線程私有的變量
{
co_init_curr_thread_env();
}
stCoRoutine_t *co = co_create_env( co_get_curr_thread_env(), attr, pfn,arg );
*ppco = co;
return 0;
}
我們可以看到函數邏輯其實非常簡單,只有區區七行代碼,調用了三個不同的函數而已。
co_get_curr_thread_env
我們先來看看co_get_curr_thread_env
函數的實現;
static __thread stCoRoutineEnv_t* gCoEnvPerThread = NULL;
...........
stCoRoutineEnv_t *co_get_curr_thread_env()
{
return gCoEnvPerThread;
}
可以看到其實就是返回一個線程私有的變量,不懂__thread關鍵字的同學可以自行了解一下,這裏爲什麼不使用C++版的thread_local呢?我對這個問題的看法是這樣的,傳送門
這個函數其實也就是在每個線程的第一個協程被創建的時候去初始化gCoEnvPerThread
。那麼如何初始化呢,函數邏輯爲co_init_curr_thread_env
co_init_curr_thread_env
void co_init_curr_thread_env()
{
gCoEnvPerThread = (stCoRoutineEnv_t*)calloc( 1, sizeof(stCoRoutineEnv_t) );
stCoRoutineEnv_t *env = gCoEnvPerThread;
env->iCallStackSize = 0; // 修改"調用棧"頂指針
struct stCoRoutine_t *self = co_create_env( env, NULL, NULL,NULL );
self->cIsMain = 1; // 一個線程調用這個函數的肯定是主協程嘍
env->pending_co = NULL;
env->occupy_co = NULL;
coctx_init( &self->ctx ); // 能跑到這裏一定是main,所以清空上下文
env->pCallStack[ env->iCallStackSize++ ] = self; // 放入線程獨有環境中
stCoEpoll_t *ev = AllocEpoll();
SetEpoll( env,ev );
}
首先爲gCoEnvPerThread分配一份內存,這本沒什麼說的,但是這也顯示出libco其實整體是一個偏C的寫法。硬要說它是C++的話。可能就是用了一些STL吧,還有一點,就是爲什麼不使用RAII去管理內存,而是使用原始的手動管理內存?我的想法是爲了提升效率,庫本身並沒有什麼預期之外操作會出現,所以不存在運行到一半throw了(當然整個libco也沒有throw),手動管理內存完全是可以的,只不過比較麻煩罷了,不過確實省去了智能指針的開銷。
後面調用了co_create_env
創建了一個stCoRoutine_t
類型的結構,我們前面說過stCoRoutine_t其實是一個協程的實體,存儲着協程的所有信息,這裏創建的了一個協程是爲什麼呢?仔細一想再結合着後面的IsMain就非常明顯了,這個結構是主協程,因爲co_init_curr_thread_env在一個線程內只會被調用一次,那麼調用這個函數的線程理所當然就是主協程嘍。co_create_env
我們後面再說。
創建協程的下面四句其實都是一些內部成員的初始化,第五句其實是有些意思的,把self付給了pCallStack,並自增iCallStackSize,我們前面說過pCallStack其實是一個調用棧的結構,那麼這個調用棧的第一個肯定是主協程,第0個元素是self,然後iCallStackSize增爲1,等待主協程調用其他協程的時候放入調用棧。
然後就是對於env中epoll封裝結構的初始化,我們來看看AllocEpoll
和epoll的封裝結構:
struct stCoEpoll_t
{
int iEpollFd; // epollfd
static const int _EPOLL_SIZE = 1024 * 10; // 一次 epoll_wait 最多返回的就緒事件個數
struct stTimeout_t *pTimeout; // 單輪時間輪
struct stTimeoutItemLink_t *pstTimeoutList; // 鏈表用於臨時存放超時事件的item
struct stTimeoutItemLink_t *pstActiveList; // 該鏈表用於存放epoll_wait得到的就緒事件和定時器超時事件
// 對 epoll_wait() 第二個參數的封裝,即一次 epoll_wait 得到的結果集
co_epoll_res *result;
};
stCoEpoll_t *AllocEpoll()
{
stCoEpoll_t *ctx = (stCoEpoll_t*)calloc( 1,sizeof(stCoEpoll_t) );
ctx->iEpollFd = co_epoll_create( stCoEpoll_t::_EPOLL_SIZE );
ctx->pTimeout = AllocTimeout( 60 * 1000 );
ctx->pstActiveList = (stTimeoutItemLink_t*)calloc( 1,sizeof(stTimeoutItemLink_t) );
ctx->pstTimeoutList = (stTimeoutItemLink_t*)calloc( 1,sizeof(stTimeoutItemLink_t) );
return ctx;
}
這裏有一點值得一提,就是時間輪這個結構,我們先來看看它的結構:
/*
* 毫秒級的超時管理器
* 使用時間輪實現
* 但是是有限制的,最長超時時間不可以超過iItemSize毫秒
*/
struct stTimeout_t
{
/*
時間輪
超時事件數組,總長度爲iItemSize,每一項代表1毫秒,爲一個鏈表,代表這個時間所超時的事件。
這個數組在使用的過程中,會使用取模的方式,把它當做一個循環數組來使用,雖然並不是用循環鏈表來實現的
*/
stTimeoutItemLink_t *pItems;
int iItemSize; // 數組長度
unsigned long long ullStart; // 時間輪第一次使用的時間
long long llStartIdx; // 目前正在使用的下標
};
struct stTimeout_t
{
stTimeoutItemLink_t *pItems;
int iItemSize; // 數組長度
unsigned long long ullStart; // 時間輪第一次使用的時間
long long llStartIdx; // 目前正在使用的下標
};
極其疑惑,就一個鏈表,它什麼就叫時間輪了?註釋中已經很清楚了,在這裏的時候我想到了以前對時間輪的思考,這裏到底是單輪時間輪效率高,還是多輪時間輪效率高呢?我想這個問題沒有什麼意義,因爲對於時間輪的選擇取決於事件的超時時間。不給出場景討論效率就是耍流氓。一般來說單輪時間輪複雜度降低的時候超時時間大於時間輪長度的時候需要取餘放入,導致每次從時間輪取出的時候都會有一些無效的遍歷,libco在超時時間大於時間輪長度的時候就直接拒絕了。而多輪時間輪因爲其特性很難出現超時時間大於時間輪長度,所有就沒有了無效遍歷,但是需要一些拷貝。想要深入瞭解多輪時間輪的朋友可以繼續深入學習,但最好不要拿我那篇博客。。因爲當時寫的時候不知道寫文章的時候在想什麼,文字非常的簡潔,現在看來根本沒有講清楚問題,且代碼沒寫註釋,光顧自己寫的嗨了。不過那個封裝好的利用多輪時間輪去除不活躍連接的代碼倒是可以用,我在我的一個項目上就用了,沒有什麼問題,就是接口有點不太好用。
co_create_env
/**
* @env 環境變量
* @attr 協程信息
* @pfn 函數指針
* @arg 函數參數
*/
struct stCoRoutine_t *co_create_env( stCoRoutineEnv_t * env, const stCoRoutineAttr_t* attr,
pfn_co_routine_t pfn,void *arg )
{
stCoRoutineAttr_t at;
// 如果指定了attr的話就執行拷貝
if( attr )
{
memcpy( &at,attr,sizeof(at) );
}
// stack_size 有效區間爲[0, 1024 * 1024 * 8]
if( at.stack_size <= 0 )
{
at.stack_size = 128 * 1024;
}
else if( at.stack_size > 1024 * 1024 * 8 )
{
at.stack_size = 1024 * 1024 * 8;
}
// 4KB對齊,也就是說如果對stacksize取餘不爲零的時候對齊爲4KB
// 例如本來5KB,經過了這裏就變爲8KB了
if( at.stack_size & 0xFFF )
{
at.stack_size &= ~0xFFF;
at.stack_size += 0x1000;
}
// 爲協程分配空間
stCoRoutine_t *lp = (stCoRoutine_t*)malloc( sizeof(stCoRoutine_t) );
memset( lp,0,(long)(sizeof(stCoRoutine_t)));
lp->env = env;
lp->pfn = pfn;
lp->arg = arg;
stStackMem_t* stack_mem = NULL;
if( at.share_stack ) // 共享棧模式 棧需要自己指定
{ // 共享棧相關,下一篇文章會說
stack_mem = co_get_stackmem( at.share_stack);
at.stack_size = at.share_stack->stack_size;
}
else // 每個協程有一個私有的棧
{
stack_mem = co_alloc_stackmem(at.stack_size);
}
lp->stack_mem = stack_mem;
lp->ctx.ss_sp = stack_mem->stack_buffer; // 這個協程棧的基址
lp->ctx.ss_size = at.stack_size;// 未使用大小,與前者相加爲esp指針,見coctx_make解釋
lp->cStart = 0;
lp->cEnd = 0;
lp->cIsMain = 0;
lp->cEnableSysHook = 0;
lp->cIsShareStack = at.share_stack != NULL;
lp->save_size = 0;
lp->save_buffer = NULL;
return lp;
}
這裏有一點需要說,就是ss_size
其實是未使用的大小,爲什麼要記錄未使用大小呢?我們思考一個問題,這個棧其實是要把基址付給寄存器的,而系統棧中指針由高地址向低地址移動,而我們分配的堆內存實際上低地址是起始地址,這裏是把從線程分配的堆內存當做協程的棧,所以esp其實是指向這片堆地址的最末尾的,所以記錄未使用大小,使得基址加上未使用大小就是esp。簡單用簡筆畫描述一下:
|------------|
| esp |
|------------|
| ss_size |
|------------|
|stack_buffer|
|------------|
到了這裏,一個還沒有運行的協程實體就被創建好啦!