2024年最新腾讯协程库libco的原理分析_st协程库原理(1),C C++工程师面试题

img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取

for(int i=0;i<cnt;i++) {
    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 );
}

主进程执行cnt次循环,来启动cnt个协程。每次调用co_resume时,主进程便会挂起,直到被激活运行的协程执行到某个步骤后,它主动放弃CPU,把执行权再次交给主进程。

那么在每个task(协程)内,到底做了哪些动作呢?

static void *readwrite_routine( void *arg )
{

    co_enable_hook_sys();

    task_t *co = (task_t*)arg;
    char buf[ 1024 * 16 ];
    for(;;)
    {
// 初始化阶段,fd为-1,表示当前没有就绪的任务需要处理
        if( -1 == co->fd )
        {     
// 当前协程入队列。这个动作的意义在于,当有任务要处理时,
// 从g\_readwrite里依次取出,并分配任务给他们去执行。
            g_readwrite.push( co );
// 挂起当前协程,让出执行权给其他协程。
// 原则很简单,就是让上次挂起的协程执行,可以认为是返回到上次执行的运行点。
            co_yield_ct();
            continue;
        }
                ...
}

回到主进程,在启动了cnt个task之后的处理:

for(int k=0;k<proccnt;k++) {
     ....
    stCoRoutine_t *accept_co = NULL;

// 启动一个协程专门做accept
    co_create( &accept_co,NULL,accept_routine,0 );

// accept协程会一直接受新连接,直到它交出执行权,才会重新回到主进程
    co_resume( accept_co );

    co_eventloop( co_get_epoll_ct(),0,0 );

    exit(0);
}

看看accept_routine的内部:

static void *accept_routine( void * )
{

    for(;;) {
// 如果工作协程队列为空,就等待1秒或者等再来事件,重试
        if( g_readwrite.empty() ) {
            printf("empty\n"); //sleep
            struct pollfd pf = { 0 };
            pf.fd = -1;
            poll( &pf,1,1000);

            continue;

        }
        struct sockaddr_in addr;
        memset( &addr,0,sizeof(addr) );
        socklen_t len = sizeof(addr);

        int fd = co_accept(g_listen_fd, (struct sockaddr *)&addr, &len);
 // 未就绪,等待下次事件继续处理
        if( fd < 0 ) {
            struct pollfd pf = { 0 };
            pf.fd = g_listen_fd;
            pf.events = (POLLIN|POLLERR|POLLHUP);
// 当前运行在accept协程,co\_poll会在等待事件的时候交出cpu,回到主进程
            co_poll( co_get_epoll_ct(),&pf,1,1000 );
            continue;
        }

// Fun!这里工作协程用尽,直接关闭当前连接...
        if( g_readwrite.empty() ) {
            close( fd );
            continue;
        }

// 弹出一个协程,去处理新连接
        SetNonBlock( fd );
        task_t *co = g_readwrite.top();
        co->fd = fd;
        g_readwrite.pop();

// 此时执行权会转移到某个线程,知道它交出cpu,当前协程才会再次执行
        co_resume( co->co );
    }

    return 0;
}

当readwrite_routine和accept_routine都会调用co_poll,但是accept会将执行权交给主进程,而task协程挂起后,执行权则会交给accept协程。这里交给accept协程,是为了进行后续的新连接的接收。那么前面由于执行碰到EAGAIN而挂起的task协程,则通过co_eventloop来驱动继续执行。

主进程中通过co_eventloop来调度事件来驱动各个协程的处理。具体的是通过stTimeoutItem_t结构中的pfnProcess来处理的。代码比较直观,就不细说了。

说完了上层的核心逻辑,我们关注下底层。先注意下co_resume函数:

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;
    coctx_swap( &(lpCurrRoutine->ctx),&(co->ctx) );
}

其中的调用coctx_make的一些实现涉及到x86_64的架构细节:

16个64位寄存器,分别是:%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,
%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。
其中:%rax 作为函数返回值使用。%rsp 栈指针寄存器,指向栈顶。%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。。。

有了上面的说明,我们再看coctx_make函数中,regs[RIP]是执行入口点,它的原型:

static int CoRoutineFunc( stCoRoutine_t *co,void * );

而regs[ RDI ]和regs[ RSI ]就是它的两个参数。而运行的栈帧,通过regs[ RBX ]和regs[ RSP ]来指定。

int coctx_make( coctx_t *ctx,coctx_pfn_t pfn,const void *s,const void *s1 )
{
        ...
    ctx->regs[ RBX ] = stack + ctx->ss_size - 1;
    ctx->regs[ RSP ] = (char*)(ctx->param) + 8;
    ctx->regs[ RIP ] = (char*)pfn;

    ctx->regs[ RDI ] = (char*)s;
    ctx->regs[ RSI ] = (char*)s1;

    return 0;
}

coctx_swap.S:

#define \_esp 0
#define \_eip 4

#define \_rsp 0
#define \_rip 8
#define \_rbx 16
#define \_rdi 24
#define \_rsi 32

.globl coctx_swap 
.type  coctx_swap, @function
coctx_swap:

    leaq -8(%rsp),%rsp
    pushq %rbp
    pushq %r12
    pushq %r13
    pushq %r14
    pushq %r15
    pushq %rdx
    pushq %rcx  
    pushq %r8
    pushq %r9
    leaq 80(%rsp),%rsp  

// rdi是coctx_swap的第一个参数,需要挂起的协程对应的coctx_t结构。
// 最开头的,是5个指针(void \*regs[ 5 ]),用来保存一些寄存器信息
    movq %rbx,_rbx(%rdi) // +16 Bytes
    movq %rdi,_rdi(%rdi)  // +24Bytes
    movq %rsi,_rsi(%rdi)   // +32Bytes

    /* 保存coctx_swap第一个参数的运行状态  \*/
    movq (%rsp), %rcx       // +8Bytes 保存返回地址
    movq %rcx, _rip(%rdi)  
    leaq 8(%rsp), %rcx       // +0Bytes 来保存之前的栈顶
    movq %rcx, _rsp(%rdi)

    /* 将当前的环境设置为 coctx_swap第二个参数提供的值 \*/
    movq _rip(%rsi), %rcx
    movq _rsp(%rsi), %rsp
    pushq %rcx // 将执行入口点入栈,这样在ret执行之后,会取栈顶指针作为执行点

    movq _rbx(%rsi),%rbx
    movq _rdi(%rsi),%rdi
    movq _rsi(%rsi),%rsi

    leaq -80(%rsp),%rsp
    popq %r9
    popq %r8
    popq %rcx   


![img](https://img-blog.csdnimg.cn/img_convert/3178ac6c13f1d81d3fd197ff7dbf6f68.png)
![img](https://img-blog.csdnimg.cn/img_convert/915ae4a380f96f5d12189b53d655a3b9.png)

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**

)]
[外链图片转存中...(img-AWXFcg3Z-1715673683247)]

**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上C C++开发知识点,真正体系化!**

**由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新**

**[如果你需要这些资料,可以戳这里获取](https://bbs.csdn.net/topics/618668825)**

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值