zoukankan      html  css  js  c++  java
  • 微信协程库libco简单分析

    一、进程的等待以及对CPU资源的释放

    在整个框架下,系统将通过co_eventloop阻塞进入系统调用。这个很容易理解,一个进程不可能一直在空跑,所以在不需要系统信息的时候就可以让操作系统把自己挂起来。或者反过来说,当进程无法运行的时候,它一定是在等待一个异步事件,此时就可以在这个等待资源上把自己的运行权返回给操作系统。在libco中,这个等待在linux下就是通过epoll_wait系统调用完成。

    二、超时的问题

    在进入epoll等待的时候有一个问题:那就是通常等待都需要有一个超时时间,如果epoll_wati进入系统调用之后一直挂起,那么协程中的超时就无法执行。但是好在epoll_wait是有超时接口的。这样libco就可以在尝试进入系统调用之前,计算出最早一个定时器到期的时间,从而保证自己最晚在这个时间点之前醒过来即可。
    这个问题是所有异步框架都要考虑的问题,例如在redis的服务器中同样需要在epoll_wait之前计算最早的定时器事件redis-5.0.4srcae.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]

    wxlibcolibco-masterco_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-mastercoctx_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;
    }

  • 相关阅读:
    为zabbix穿上一件漂亮的外衣
    CentOS7 Ceph分布式集群部署
    SSH 免秘钥登录
    zabbix监控Tomcat/JVM 实例性能
    zabbix 监控 IPMI
    2装饰者模式
    1代理模式
    3单例模式
    2抽象工厂模式
    1工厂模式
  • 原文地址:https://www.cnblogs.com/tsecer/p/10656335.html
Copyright © 2011-2022 走看看