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實現
引言
我們總能在運行libco協程代碼的最後看到對於函數co_eventloop
的調用,它可以理解爲主協程執行的函數。我們舉一個簡單的例子來說明它的作用:
void* routinefun(void* args){
co_enable_hook_sys();
while(true){
poll(NULL, 0, 1000);
}
return 0;
}
int main(int argc,char *argv[])
{
vector<task_t> v;
for(int i=1;i<argc;i+=2)
{
task_t task = { 0 };
SetAddr( argv[i],atoi(argv[i+1]),task.addr );
v.push_back( task );
}
for(int i=0;i<2;i++)
{
stCoRoutine_t *co = 0;
co_create( &co,NULL,routinefun,v2 );
printf("routine i %d\n",i);
co_resume( co );
}
co_eventloop( co_get_epoll_ct(),0,0 );
return 0;
}
這段代碼非常簡單,主協程運行兩個協程,協程函數所做的事情就是使用poll切換執行權,並在一秒後切換回來(超時)。這裏線程的執行過程是這樣的,我們把主協程看做A,其他兩個協程看做BC。執行過程爲:
- B協程執行,使用poll把一個
stPoll_t
結構插入時間輪,切換執行權,回到A協程。 - C協程執行,使用poll把一個
stPoll_t
結構插入時間輪,切換執行權,回到A協程。 - 此時A協程執行Eventloop中,不停的循環,直到B協程註冊的事件超時,調用回調回到B協程。
- B協程繼續執行,再次使用poll,重複第一步,回到A協程。
- A協程繼續執行Eventloop,不停的循環,直到C協程註冊的事件超時,調用回調回到C協程。
- C協程繼續執行,再次使用poll,重複第二步,回到A協程。
- …
這樣我們就可以看清楚co_eventloop
到底做了什麼,其實就是不停的輪詢等待其他協程註冊的事件成立,僅此而已。
co_eventloop
/*
* libco的核心調度
* 在此處調度三種事件:
* 1. 被hook的io事件,該io事件是通過co_poll_inner註冊進來的
* 2. 超時事件
* 3. 用戶主動使用poll的事件
* 所以,如果用戶用到了三種事件,必須得配合使用co_eventloop
*
* @param ctx epoll管理器
* @param pfn 每輪事件循環的最後會調用該函數
* @param arg pfn的參數
*/
void co_eventloop( stCoEpoll_t *ctx,pfn_co_eventloop_t pfn,void *arg )
{
if( !ctx->result ) // 給結果集分配空間
{ // epoll結果集大小
ctx->result = co_epoll_res_alloc( stCoEpoll_t::_EPOLL_SIZE );
}
co_epoll_res *result = ctx->result;
for(;;)
{
// 最大超時時間設置爲 1 ms
// 所以最長1ms,epoll_wait就會被喚醒
int ret = co_epoll_wait( ctx->iEpollFd,result,stCoEpoll_t::_EPOLL_SIZE, 1 );
// 不使用局部變量的原因是epoll循環並不是元素的唯一來源.例如條件變量相關(co_routine.cpp stCoCondItem_t)
stTimeoutItemLink_t *active = (ctx->pstActiveList);
stTimeoutItemLink_t *timeout = (ctx->pstTimeoutList);
memset( timeout,0,sizeof(stTimeoutItemLink_t) );
// 獲取在co_poll_inner放入epoll_event中的stTimeoutItem_t結構體
for(int i=0;i<ret;i++)
{
stTimeoutItem_t *item = (stTimeoutItem_t*)result->events[i].data.ptr;
if( item->pfnPrepare ) // 如果用戶設置預處理回調的話就執行
{
// 若是hook後的poll的話,會把此事件加入到active隊列中,並更新一些狀態
item->pfnPrepare( item,result->events[i],active );
}
else
{
AddTail( active,item );
}
}
// 從時間輪上取出超時事件
unsigned long long now = GetTickMS();
// 以當前時間爲超時截止點
// 從時間輪中取出超時的時間放入到timeout中
TakeAllTimeout( ctx->pTimeout,now,timeout );
stTimeoutItem_t *lp = timeout->head;
while( lp ) // 遍歷超時鏈表,設置超時標誌,並加入active鏈表
{
//printf("raise timeout %p\n",lp);
lp->bTimeout = true;
lp = lp->pNext;
}
// 把timeout合併到active中
Join<stTimeoutItem_t,stTimeoutItemLink_t>( active,timeout );
lp = active->head;
// 開始遍歷active鏈表
while( lp )
{
// 在鏈表不爲空的時候刪除active的第一個元素 如果刪除成功,那個元素就是lp
PopHead<stTimeoutItem_t,stTimeoutItemLink_t>( active );
if (lp->bTimeout && now < lp->ullExpireTime)
{ // 一種排錯機制,在超時和所等待的時間內已經完成只有一個條件滿足纔是正確的
int ret = AddTimeout(ctx->pTimeout, lp, now);
if (!ret) //插入成功
{
lp->bTimeout = false;
lp = active->head;
continue;
}
}
// TODO 有問題,如果同一個協程有兩個事件在一次epoll循環中觸發,
// 那麼第一個事件切回去執行協程,第二個呢,已提交issue
if( lp->pfnProcess )
{ // 默認爲OnPollProcessEvent 會切換協程
lp->pfnProcess( lp );
}
lp = active->head;
}
// 每次事件循環結束以後執行該函數, 用於終止協程
if( pfn )
{
if( -1 == pfn( arg ) )
{
break;
}
}
}
}
首先我們可以看到active
和timeout
鏈表都在stCoEpoll_t中存儲,而這個結構是線程私有的。那麼爲什麼不把這個值設置成局部變量呢?答案不在co_eventloop中,而藏在其他函數,比如libco實現的條件變量中,條件變量會在signal後把值放入到active鏈表或者timeout鏈表,而這些只能放在stCoEpoll_t中。
還有這裏的timeout鏈表其實最終會合併到active中,先分開純粹是爲了處理方便一點。
然後就是把事件從epoll結果集中拿出來,去執行預處理回調。我們來看看預處理回調,我們曾在poll中提到過:
void OnPollPreparePfn( stTimeoutItem_t * ap,struct epoll_event &e,stTimeoutItemLink_t *active )
{
stPollItem_t *lp = (stPollItem_t *)ap;
// 把epoll此次觸發的事件轉換成poll中的事件
lp->pSelf->revents = EpollEvent2Poll( e.events );
stPoll_t *pPoll = lp->pPoll;
// 已經觸發的事件數加一
pPoll->iRaiseCnt++;
// 若此事件還未被觸發過
if( !pPoll->iAllEventDetach )
{
// 設置已經被觸發的標誌
pPoll->iAllEventDetach = 1;
// 將該事件從時間輪中移除
// 因爲事件已經觸發了,肯定不能再超時了
RemoveFromLink<stTimeoutItem_t,stTimeoutItemLink_t>( pPoll );
// 將該事件添加到active列表中
AddTail( active,pPoll );
}
}
我們可以看到其實所做的事情就是把epoll事件對應的stPoll_t
結構中的值執行一些修改,並把此項插入到active鏈表中。
然後就是從時間輪中取出根據目前時間來說已經超時的事件,並插入到timeout鏈表中:
inline void TakeAllTimeout( stTimeout_t *apTimeout,unsigned long long allNow,stTimeoutItemLink_t *apResult )
{
// 第一次調用是設置初始時間
if( apTimeout->ullStart == 0 )
{
apTimeout->ullStart = allNow;
apTimeout->llStartIdx = 0;
}
// 當前時間小於初始時間顯然是有問題的
if( allNow < apTimeout->ullStart )
{
return ;
}
// 求一個取出事件的有效區間
int cnt = allNow - apTimeout->ullStart + 1;
if( cnt > apTimeout->iItemSize )
{
cnt = apTimeout->iItemSize;
}
if( cnt < 0 )
{
return;
}
for( int i = 0;i<cnt;i++)
{ // 把上面求的有效區間過一遍,某一項存在數據的話插入到超時鏈表中
int idx = ( apTimeout->llStartIdx + i) % apTimeout->iItemSize;
// 鏈表操作,沒什麼可說的
Join<stTimeoutItem_t,stTimeoutItemLink_t>( apResult,apTimeout->pItems + idx );
}
// 更新時間輪屬性
apTimeout->ullStart = allNow;
apTimeout->llStartIdx += cnt - 1;
}
然後就是把超時鏈表處理以後加入到active鏈表啦,不得不說這些封裝的鏈表操作還是非常實用的。
然後就是遍歷active鏈表,一一執行每一個事件的回調啦,當然沒執行一次回調就意味着一次協程的切換,因爲我們在poll中註冊的回調執行co_resume。
這裏其實我個人認爲是有一些問題的。如果我們在poll中註冊了兩個fd的事件,這兩個時間在一次epoll_wait中被觸發,那麼第一個被執行了,第二個呢?如果再執行的話就會core dump,因爲這個上下文已經被用過了。這裏我們應該做一個哈希表,給每一個協程一個特定編號,在遍歷active時如果某一個協程已經被使用,我們在後面的遍歷過程不再調用回調,這樣就可以避免這個問題,已經提交issume。
我們注意到循環的最後調用了pfn,這是一個我們在調用co_eventloop時傳入的函數指針,它的作用是什麼呢?我的想法是跳出Eventloop循環,因爲不是所有的協程使用都想例子一樣把 co_eventloop放在函數最後,協程更多的是嵌到代碼中,我們需要在有些時候終止eventloop,傳入一個終止回調就是一個不錯的方法。