一、协程的优点
在传统网络编程中,同步IO由于需要线程等待返回,无法最大使用CPU。而异步IO解决了性能问题,但是管理回调函数,IO返回时该事件的状态,使编程更加复杂,代码也不易阅读。协程的存在,让我们可以以同步方式编程,且最大发挥CPU的性能
二、libco协程实现
协程切换:libco自己实现了切换函数上下文的函数(coctx_swap,通过修改存在CPU寄存器的栈指针以及函数,函数参数指针)
Hook系统函数:通过hook系统函数,应用层依然可以同步调用send,recv, poll等系统函数,但是底层通过加入epoll,主动让出当前CPU,实现了异步IO的方式。
epoll+时间轮:管理网络事件以及超时事件
共享栈模式:节约系统内存,从而支持更多的连接,共享栈切换时需要保存栈内容
三、协程切换部分代码
struct stCoRoutineEnv_t/*每个进程一个协程管理类*/
{
stCoRoutine_t *pCallStack[ 128 ];/*数组实现的栈,先进后出,运行协程压入,协程主动放弃pop出*/
int iCallStackSize;
stCoEpoll_t *pEpoll;/*每个进程申请一个epoll实例*/
//使用共享栈模式使用,后续
stCoRoutine_t* pending_co;
stCoRoutine_t* occupy_co;
};
static stCoRoutineEnv_t* g_arrCoEnvPerThread[ 204800 ] = { 0 };/*存着所有进程的协程管理类,可通过Pid下标访问*/
协程类
struct stCoRoutine_t
{
stCoRoutineEnv_t *env;
pfn_co_routine_t pfn;
void *arg;
coctx_t ctx;
char cStart;
char cEnd;
char cIsMain;
char cEnableSysHook;
char cIsShareStack;
void *pvEnv;
//char sRunStack[ 1024 * 128 ];
stStackMem_t* stack_mem;
//save satck buffer while confilct on same stack_buffer;
char* stack_sp;
unsigned int save_size;
char* save_buffer;
stCoSpec_t aSpec[1024];
};
协程提供了三个函数
co_create、创建协程
co_resume、主动切换某个协程
co_yield_ct、协程主动让出CPU
int co_create( stCoRoutine_t **ppco,const stCoRoutineAttr_t *attr,pfn_co_routine_t pfn,void *arg )
{
if( !co_get_curr_thread_env() )
{ /*如果该进程第一次调用,初始化该进程的协程管理类,包括初始化epoll实例,并把当前主协程类压入pCallStack*/
co_init_curr_thread_env();
}
stCoRoutine_t *co = co_create_env( co_get_curr_thread_env(), attr, pfn,arg );
*ppco = co;
return 0;
}
void co_resume( stCoRoutine_t *co )
{ac
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( lpCurrRoutine, co );
}
void co_yield_env( stCoRoutineEnv_t *env )
{
stCoRoutine_t *last = env->pCallStack[ env->iCallStackSize - 2 ];
stCoRoutine_t *curr = env->pCallStack[ env->iCallStackSize - 1 ];
env->iCallStackSize--;
co_swap( curr, last);
}
四、系统函数hook以及事件管理
以example_echosvr.cpp来看看怎么处理网络IO
for(int k=0;k<proccnt;k++)
{
/*开启proccnt个进程*/
pid_t pid = fork();
if( pid > 0 )
{
continue;
}
else if( pid < 0 )
{
break;
}
for(int i=0;i<cnt;i++)/*每个进程开cnt个协程处理io事件*/
{
task_t * task = (task_t*)calloc( 1,sizeof(task_t) );
task->fd = -1;
co_create( &(task->co),NULL,readwrite_routine,task );
co_resume( task->co );
}
stCoRoutine_t *accept_co = NULL;
co_create( &accept_co,NULL,accept_routine,0 );
co_resume( accept_co );
/*主协程将执行事件循环,主要处理epoll返回的事件以及时间超时*/
co_eventloop( co_get_epoll_ct(),0,0 );
exit(0);
}
accept_routine 函数调用co_accept后,调用co_poll函数,在空闲协程队列中取出协程,并将这个套接字的读写交给这个协程处理,切换至这个协程
readwrite_routine函数调用co_poll后,再调用read,write函数,处理完再放入空闲协程队列
co_poll最后调用的是co_poll_inner函数
int co_poll_inner( stCoEpoll_t *ctx,struct pollfd fds[], nfds_t nfds, int timeout, poll_pfn_t pollfunc)
{
if( timeout > stTimeoutItem_t::eMaxTimeout )
{
timeout = stTimeoutItem_t::eMaxTimeout;
}
int epfd = ctx->iEpollFd;
stCoRoutine_t* self = co_self();
//1.struct change
stPoll_t& arg = *((stPoll_t*)malloc(sizeof(stPoll_t)));
memset( &arg,0,sizeof(arg) );
arg.iEpollFd = epfd;
arg.fds = (pollfd*)calloc(nfds, sizeof(pollfd));
arg.nfds = nfds;
stPollItem_t arr[2];
if( nfds < sizeof(arr) / sizeof(arr[0]) && !self->cIsShareStack)
{
arg.pPollItems = arr;
}
else
{
arg.pPollItems = (stPollItem_t*)malloc( nfds * sizeof( stPollItem_t ) );
}
memset( arg.pPollItems,0,nfds * sizeof(stPollItem_t) );
arg.pfnProcess = OnPollProcessEvent;/*在epoll_wait返回调用的函数,实际上是切换到属于该套接字的协程*/
arg.pArg = GetCurrCo( co_get_curr_thread_env() );
//2. add epoll
for(nfds_t i=0;i<nfds;i++)
{
arg.pPollItems[i].pSelf = arg.fds + i;
arg.pPollItems[i].pPoll = &arg;
arg.pPollItems[i].pfnPrepare = OnPollPreparePfn;/*epollwait返回预处理函数,从时间轮里取出该事件*/
struct epoll_event &ev = arg.pPollItems[i].stEvent;
if( fds[i].fd > -1 )
{
ev.data.ptr = arg.pPollItems + i;
ev.events = PollEvent2Epoll( fds[i].events );
int ret = co_epoll_ctl( epfd,EPOLL_CTL_ADD, fds[i].fd, &ev );
if (ret < 0 && errno == EPERM && nfds == 1 && pollfunc != NULL)
{
if( arg.pPollItems != arr )
{
free( arg.pPollItems );
arg.pPollItems = NULL;
}
free(arg.fds);
free(&arg);
return pollfunc(fds, nfds, timeout);
}
}
//if fail,the timeout would work
}
//3.add timeout
/*加入时间轮*/
unsigned long long now = GetTickMS();
arg.ullExpireTime = now + timeout;
int ret = AddTimeout( ctx->pTimeout,&arg,now );
if( ret != 0 )
{
co_log_err("CO_ERR: AddTimeout ret %d now %lld timeout %d arg.ullExpireTime %lld",
ret,now,timeout,arg.ullExpireTime);
errno = EINVAL;
if( arg.pPollItems != arr )
{
free( arg.pPollItems );
arg.pPollItems = NULL;
}
free(arg.fds);
free(&arg);
return -__LINE__;
}
/*加入epoll和时间轮后,主动放弃CPU,等epoll返回或者超时后,在从这里开始执行代码*/
co_yield_env( co_get_curr_thread_env() );
RemoveFromLink<stTimeoutItem_t,stTimeoutItemLink_t>( &arg );
for(nfds_t i = 0;i < nfds;i++)
{
int fd = fds[i].fd;
if( fd > -1 )
{ /*从epoll删除*/
co_epoll_ctl( epfd,EPOLL_CTL_DEL,fd,&arg.pPollItems[i].stEvent );
}
fds[i].revents = arg.fds[i].revents;
}
int iRaiseCnt = arg.iRaiseCnt;
if( arg.pPollItems != arr )
{
free( arg.pPollItems );
arg.pPollItems = NULL;
}
free(arg.fds);
free(&arg);
return iRaiseCnt;
}
如果套接字是阻塞的,hook的read和write函数实际上会先调用co_poll函数,等返回时再执行系统的read和write,这样达到了用同步编程的方式,实际上是异步执行的效果。例子给的代码是用非阻塞的。
还有共享栈模式,这个以后有时间再详细写吧