微信协程库libco简单分析
一、进程的等待以及对CPU资源的释放
在整个框架下,系统将通过co_eventloop阻塞进入系统调用。这个很容易理解,一个进程不可能一直在空跑,所以在不需要系统信息的时候就可以让操作系统把自己挂起来。或者反过来说,当进程无法运行的时候,它一定是在等待一个异步事件,此时就可以在这个等待资源上把自己的运行权返回给操作系统。在libco中,这个等待在linux下就是通过epoll_wait系统调用完成。
二、超时的问题
在进入epoll等待的时候有一个问题:那就是通常等待都需要有一个超时时间,如果epoll_wati进入系统调用之后一直挂起,那么协程中的超时就无法执行。但是好在epoll_wait是有超时接口的。这样libco就可以在尝试进入系统调用之前,计算出最早一个定时器到期的时间,从而保证自己最晚在这个时间点之前醒过来即可。
这个问题是所有异步框架都要考虑的问题,例如在redis的服务器中同样需要在epoll_wait之前计算最早的定时器事件redis-5.0.4\src\ae.c
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
/* Nothing to do? return ASAP */
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
/* Note that we want call select() even if there are no
* file events to process as long as we want to process time
* events, in order to sleep until the next time event is ready
* to fire. */
……
}
看了下libco的等待时间是写死的1,那就是1毫秒都有可能从系统调用中返回,这个频率其实还是挺高的。对于CPU资源有些浪费,但是优点就是实现简单,不用每次都计算下次最早触发的定时器时间。
三、主线程的coroutine如何表示
在每个线程初始化的时候,保证主线程使用协程栈的第一个槽位pCallStack[0]
wxlibco\libco-master\co_routine.cpp
void co_init_curr_thread_env()
{
pid_t pid = GetPid();
g_arrCoEnvPerThread[ pid ] = (stCoRoutineEnv_t*)calloc( 1,sizeof(stCoRoutineEnv_t) );
stCoRoutineEnv_t *env = g_arrCoEnvPerThread[ pid ];
env->iCallStackSize = 0;
struct stCoRoutine_t *self = co_create_env( env, NULL, NULL,NULL );
self->cIsMain = 1;
env->pending_co = NULL;
env->ocupy_co = NULL;
coctx_init( &self->ctx );
env->pCallStack[ env->iCallStackSize++ ] = self;
stCoEpoll_t *ev = AllocEpoll();
SetEpoll( env,ev );
}
四、被切出线程的返回地址及栈顶如何保存
从实现上来看,每个协程私有的被切出进程的堆栈上。所以,关键的信息是要找到切换目标协程的栈顶信息,而这个信息
struct coctx_t
{
#if defined(__i386__)
void *regs[ 8 ];
#else
void *regs[ 14 ];
#endif
size_t ss_size;
char *ss_sp;
};
进入该函数时,esp寄存器指向调用函数返回地址,esp+4为第一个参数,esp+8存储第二个参数
libco-master\coctx_swap.S
coctx_swap:
#if defined(__i386__)
leal 4(%esp), %eax //sp
movl 4(%esp), %esp
leal 32(%esp), %esp //parm a : ®s[7] + sizeof(void*) 由于栈是从上向下增加,而数据结构按照定义顺序从下到上布局,所以这里先跳到®s[7] + sizeof(void*) 处
pushl %eax //esp ->parm a
pushl %ebp
pushl %esi
pushl %edi
pushl %edx
pushl %ecx
pushl %ebx
pushl -4(%eax) 这个地方是把coctx_swap返回地址压入栈顶
movl 4(%eax), %esp //parm b -> ®s[0]。由于eax之前已经被指向第一个参数,此时+4指向第二个参数。
popl %eax //ret func addr
popl %ebx
popl %ecx
popl %edx
popl %edi
popl %esi
popl %ebp
popl %esp
pushl %eax //set ret func addr
xorl %eax, %eax
ret
五、epoll_wait返回和协程的对应关系
man手册中对于epoll_wait系统调用的说明
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *events,
int maxevents, int timeout);
int epoll_pwait(int epfd, struct epoll_event *events,
int maxevents, int timeout,
const sigset_t *sigmask);
……
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event {
uint32_t events; /* Epoll events */
epoll_data_t data; /* User data variable */
};
在每个Event中可以保存一个用户自定义的epoll_data,有了这个指针,就可以指向协程相关的信息。
六、co_yield返回到哪里
返回到切换到这个过来的那个协程。
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);
}
七、如果协程函数返回怎么办
从代码上看,如果协程处理函数返回,将会出现程序崩溃。在co_resume执行时会手动目标协程最为关键的ESP和EIP指针,这个第一次运行时是手动设置的。
int coctx_make( coctx_t *ctx,coctx_pfn_t pfn,const void *s,const void *s1 )
{
//make room for coctx_param
char *sp = ctx->ss_sp + ctx->ss_size - sizeof(coctx_param_t);
sp = (char*)((unsigned long)sp & -16L);
coctx_param_t* param = (coctx_param_t*)sp ;
param->s1 = s;
param->s2 = s1;
memset(ctx->regs, 0, sizeof(ctx->regs));
ctx->regs[ kESP ] = (char*)(sp) - sizeof(void*);
ctx->regs[ kEIP ] = (char*)pfn;
return 0;
}
而这个设置的信息在coctx_swap时会从栈中把这个信息一次性消耗掉,所以这也意味着如果协程函数不调用co_resume将会崩溃。简单试了下,竟然没有崩溃。看了下发现在协程上下文初始化的时候在外面封装了一层接口,如果协程函数返回,则会替用户的协程函数调用co_yield_env( env ),从而保证协程函数return之后不会崩溃。
void co_resume( stCoRoutine_t *co )
{
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 );
}
static int CoRoutineFunc( stCoRoutine_t *co,void * )
{
if( co->pfn )
{
co->pfn( co->arg );
}
co->cEnd = 1;
stCoRoutineEnv_t *env = co->env;
co_yield_env( env );
return 0;
}