微信协程库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 : &regs[7] + sizeof(void*) 由于栈是从上向下增加,而数据结构按照定义顺序从下到上布局,所以这里先跳到&regs[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 -> &regs[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;
}

微信协程库libco简单分析

上一篇:一个简单的php文件,实现微信网页授权回调域名的代理转发


下一篇:微信开发SDK使用教程--朋友圈评论删除任务