美文网首页
协程实现原理

协程实现原理

作者: 西门早柿 | 来源:发表于2021-02-11 23:00 被阅读0次

用户空间切换

linux 的系统调用提供了在用户空间进行上下文切换的能力。go 语言中用户空间的上下文切换用的是汇编实现,怀疑可能是为了跨平台及提高效率而为之。后面用 linux 提供的系统调用来实现一个简单的用户空间上下文切换,反汇编它,看与 go 语言的汇编实现有什么异同。下面首先来看想关的四个系统调用。毕竟是系统调用,会带来用户态和内核态之间的切换开销,这可能也是 go 用汇编实现的原因之一。

struct ucontext

先来看一下关键的数据结构:

#include <ucontext.h>
typedef struct ucontext {
        struct ucontext *uc_link;
        sigset_t         uc_sigmask;
        stack_t          uc_stack;
        mcontext_t       uc_mcontext;
        ...
    } ucontext_t;

其中 uc_link 是当前上下文结束,程序继续执行的上下文。 uc_sigmask 是该上下文的信号屏蔽掩码。uc_stack 是该上下文使用的栈。 uc_mcontext 是机器相关的上下文保存内容,主要包括调用线程的寄存器。

getcontext
int getcontext(ucontext_t *ucp);

用当前活跃的用户上下文初始化 ucp 指向的结构体。 getcontext 这个函数是汇编实现的,在 getcontext.S 这个文件里。

#include "offsets.h"

/*  int _Ux86_getcontext (ucontext_t *ucp)

  Saves the machine context in UCP necessary for libunwind.
  Unlike the libc implementation, we don't save the signal mask
  and hence avoid the cost of a system call per unwind.

*/

/*    .global _Ux86_getcontext
    .type _Ux86_getcontext, @function
_Ux86_getcontext:*/
    .global getcontext
    .type getcontext, @function
getcontext:
    .cfi_startproc
    mov    4(%esp),%eax  /* ucontext_t* */

    /* EAX is not preserved. */
    movl    $0, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_EAX_OFF)(%eax)

    movl    %ebx, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_EBX_OFF)(%eax)
    movl    %ecx, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_ECX_OFF)(%eax)
    movl    %edx, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_EDX_OFF)(%eax)
    movl    %edi, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_EDI_OFF)(%eax)
    movl    %esi, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_ESI_OFF)(%eax)
    movl    %ebp, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_EBP_OFF)(%eax)

    movl    (%esp), %ecx
    movl    %ecx, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_EIP_OFF)(%eax)

    leal    4(%esp), %ecx        /* Exclude the return address.  */
    movl    %ecx, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_ESP_OFF)(%eax)

    /* glibc getcontext saves FS, but not GS */
    xorl    %ecx, %ecx
    movw    %fs, %cx
    movl    %ecx, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_FS_OFF)(%eax)

    leal    LINUX_UC_FPREGS_MEM_OFF(%eax), %ecx
    movl    %ecx, (LINUX_UC_MCONTEXT_OFF+LINUX_SC_FPSTATE_OFF)(%eax)
    fnstenv    (%ecx)
    fldenv    (%ecx)

    xor    %eax, %eax
    ret
    .cfi_endproc
    /*.size    _Ux86_getcontext, . - _Ux86_getcontext*/
    .size    getcontext, . - getcontext

    /* We do not need executable stack.  */
    .section        .note.GNU-stack,"",@progbits

与 go 中的汇编实现还是有点相像的。做的事情其实是差不多的,只不过这是通过系统调用实现的。主要就是把各寄存器的值保存到内存结构体中。

setcontext
#include <ucontext.h>
int
setcontext(const ucontext_t *ucp);

setcontext 将之前保存的 ucp 指针指向的 context 恢复到当前线程的上下文中,即恢复各种寄存器。
其中 ucp 指向的 context 要不来自 getcontext 要不来自 makecontext。如果来自 getcontext,则跟什么都没发生过一样。如果来自 makecontext,则执行 makecontext 里指定的函数,如果执行完毕则继续执行 uc_link 指向的 context,如果 uc_link 是 null,程序结束。
setcontext 也是汇编实现的,基本上就是 getcontext 的逆操作。

makecontext switchcontext
#include <ucontext.h>

     void
     makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);

     int
     swapcontext(ucontext_t *oucp, const ucontext_t *ucp);

makecontext 修改 ucp 指向的 context。the caller must allocate a new stack for this context and assign its address to ucp->uc_stack, and define a successor context and assign its address to ucp->uc_link. Also the func.

When this context is later activated (using setcontext(3) or swapcontext()) the function func is called, and passed the series of integer (int) arguments that follow argc; the caller must specify the number of these arguments in argc. When this function returns, the successor context is activated. If the successor context pointer is NULL, the thread exits.

The swapcontext() function saves the current context in the structure pointed to by oucp, and then activates the context pointed to by ucp.

其中 makecontext 是 c 实现的,因为不涉及寄存器的操作,switchcontext 是汇编实现的,可以看做是 getcontext 和 setcontext 的结合。

一个例子
#include <ucontext.h>
#include <stdlib.h>
#include <stdio.h>

ucontext_t ctx[3];
ucontext_t* running;

int c = 80;

void shedule() {
    swapcontext(running, &ctx[0]);
}

void foo1(int a, int b) {
    printf("foo1 %d %d\n", a, b);
    printf("c: %d\n", c);
    printf("foo1 yield....\n");
    shedule();
    printf("foo1 resume and exit\n");
}

void foo2() {
    printf("foo2...\n");
    printf("foo2 yield....\n");
    shedule();
    printf("foo2 resume and exit\n");
}

int main() {
    char st1[8192];
    char st2[8192];

    // shedule g foo1
    getcontext(&ctx[1]);
    ctx[1].uc_stack.ss_sp = st1;
    ctx[1].uc_stack.ss_size = sizeof(st1);
    ctx[1].uc_link = &ctx[0];
    makecontext(&ctx[1], foo1, 2, 7, 8);
    running = &ctx[1];
    swapcontext(&ctx[0], &ctx[1]);

    // shedule g foo2
    getcontext(&ctx[2]);
    ctx[2].uc_stack.ss_sp = st2;
    ctx[2].uc_stack.ss_size = sizeof(st2);
    ctx[2].uc_link = &ctx[0];
    makecontext(&ctx[2], foo2, 0);
    running = &ctx[2];
    swapcontext(&ctx[0], &ctx[2]);

    // shedule g foo2
    swapcontext(&ctx[0], &ctx[2]);
    // shedule g foo1
    swapcontext(&ctx[0], &ctx[1]);

    // m over
    printf("m over...\n");

    return 0;
}

上面的小程序简单的揭示了协程实现的底层原理。在 go 协程实现中,有三个概念 G,P,M。G 是用户级协程,M 是系统级线程。每个 M 只有绑定了 P,才可以运行 G。P 的个数是有限的,通常与系统的 CPU 核心数相同,用来限制并发数。三者具体的关系将在另一篇文章中描述。
在上面的程序中,M 对应的就是 main 主线程。而 foo1 和 foo2 分别对应两个 G。M 先是调度运行了 foo1,在 foo1 里调用 schedule 函数主动让出 cpu ,schedule 有点类似 python 中的 yield。foo1 让出 cpu 之后,M 继续进行调度,继续调度 foo2 ,在 foo2 里同样利用 schedule 让出 cpu。回到 M 进行调度,M 调度 foo2, foo2 从上次切换之后的位置继续运行直到退出,回到 M。然后调度 foo1,同样退出。M 退出。
在实际的应用中,G 通常会组成一个队列。M 循环从队列里取出 G 进行运行,中途切换出去的 G 也会放会队列中,等待 M 下次调度。

相关文章

  • 并发编程-协程

    协程greenlet模块 (gevent实现原理)gevent模块 (注册协程,实现异步编程) 协程的应用eg:...

  • Kotlin Primer·第七章·协程库(上篇)

    本篇只讲了协程库的使用。还有中篇讲协程的启动和切换实现原理,下篇核心讲解kotlin协程在JVM层的实现原理。这可...

  • Kotlin 协程入门

    本文主要介绍协程长什么样子, 协程是什么东西, 协程挂起的实现原理以及整理了协程学习的资料. 协程 HelloWo...

  • 韩天峰 - Swoole4-全新的PHP编程模式2018-10-

    介绍 Swoole 2.2 全新协程引擎底层实现原理 2. Go(协程)+ Chan(通道)实现有别于传统 PHP...

  • 关于Coroutine\Channel的几点注意

    通道,类似于go语言的chan,支持多生产者协程和多消费者协程。底层自动实现了协程的切换和调度。 实现原理 通道与...

  • 协程实现原理

    [toc] 进程/线程/协程 背景 单任务处理:早期的计算机是什么样的, 卡带机? 真空管? 多任务处理系统: 多...

  • 协程

    对于协程做一个整体的描述,从概念、原理、实现三个方面叙述。侧重有栈协程。 1 概览 1.1 什么是协程 有很多与协...

  • Coroutine与LifecycleOwner绑定自动跟随生命

    原文链接 前言 前面讲了Coroutine的实现原理。对协程有个初步的认识。我们都知道协程是运行在Coroutin...

  • Kotlin协程实现原理

    CSDN同步发布 为什么需要协程? 协程可以简化异步编程,可以顺序地表达程序,协程也提供了一种避免阻塞线程并用更廉...

  • Kotlin 协程实现原理

    目录 前言先从线程谈起 设计思想CPS 变换续体与续体拦截器状态机标准库 实现细节Main 调度器Default ...

网友评论

      本文标题:协程实现原理

      本文链接:https://www.haomeiwen.com/subject/cjwjxltx.html