关于linux下协程的通用实现及libtask库源码解析

协程(coroutine)与subroutine同样作为程序执行单元的抽象被一些语言当作基 础实现,两者的抽象方式大致区别在于:

  1. 多个执行单元之间的关系:

对于subroutine来说,存在一个调用与被调用的关系,比如在a-subroutine里调用 b-subroutine, 那么a-subroutine就是调用者,b-subroutine是被调用者,它们共 享一个线程的堆栈.

而多个coroutine之间的关系是对等的,即便在a-coroutine里创建了 b-coroutine,他们之间也不会有任何层级关系.

subroutine作为一种通用的抽象比较容易实现,而要实现coroutine至少需要两个 条件:

  • 要有一个全局的调度器并且每个coroutine得有一个堆栈空间.

  • 调度器用来在多个对等的coroutine之间做切换操作,每个coroutine的堆栈 用于存储各自的上下文内容.

  1. 执行单元的入口与出口:

在一个典型的subroutine实现里,执行单元的入口和出口只能有一个, 这是共享 调用栈带来的局限性, 比如(x86-64平台)我们在a-subroutine里调用 b-subroutine,那么会先把前6个参数依次写入寄存器:rdi,rsi,rdx,rcx,r8,r9,6 个以上的参数从右至左压栈,rsp不断上移指向栈顶,然后把b-subroutine调用之 后的那条指令地址压栈,rsp上移,然后rbp压栈,把rsp指向rbp的栈地址,最后把 b-subroutine里的局部变量依次压栈,rsp继续上移.这就是调用时的入口过程, 当执行是b-subroutine时,就得依次出栈,然后退出,执行a-subroutine里的下一 条指令,这是出口过程.很明显,这里b-subroutine只有一个入口和一个出口,因为 必须要等b-subroutine执行完成后(出栈)才能继续执行a-subroutine.

而一个典型的coroutine实现,执行单元可以有多个入口和出口, 因为栈不共享, 一个通用的实现模式是用堆来表示coroutine的调用栈, 当我们在a-coroutine里 创建b-coroutine, 在堆上分配一块空间用于表示b-coroutine的堆栈, 在执行 b-coroutine时,我们可以在任意点把当前的执行信息写回这块在堆上分配的空 间,然后把rsp指向a-coroutine的栈顶,rbp指向a-coroutine的栈frame,rip指向 a-coroutine的需要继续执行的指令的地址, 这也就是所谓的用户态的上下文切 换,当下一次需要继续执行b-coroutine的时候,保存当前coroutine的上下文, 恢复b-coroutine的上下文就行了.

上面描述的上下文切换是在用户态进行的,unix-like的环境下, glibc库通常都 会有一个描述上下文结构的定义ucontext_t在ucontext.h文件里,并有四个函 数:getcontext,setcontext,makecontext,swapcontext分别用于在用户态保存上 下文,恢复上下文,创建上下文和保存且恢复上下文,使用它们可以实现一个基本 的协程系统, 比如libtask就是一个运行在unix平台上的基础协程库, 能在用户 态实现多个执行流轮换执行,libtask对glibc的 getcontext,setcontext,makecontext,swapcontext做了简单的封装,因为这四个 函数是libtask实现的基础,所以要研究libtask前,先得了解这4个函数的作用与 实现机制.

这里有一个关键的数据结构,即user level context:

 typedef struct ucontext
      {
        unsigned long int uc_flags;
        // 另一个执行流的上下文地址,在x86x64平台下也就是rbx寄存器里的内容
        struct ucontext *uc_link;
        //用于此上下文结构的堆栈,存储在堆区
        stack_t uc_stack;
        // mcontext_t结构体用于存储完整的进程状态信息
        mcontext_t uc_mcontext;
        // 需要block的信号掩码
        __sigset_t uc_sigmask;
        // fpu寄存器结构
        struct _libc_fpstate __fpregs_mem;
    } ucontext_t;

ucontext结构体里的uc_stack是在堆上分配的,并做为堆栈用于此上下文, makecontext函数将会设置uc_stack与相应寄存器里的值uc_stack的结构大致 是这样的:

    ---------------------------------------
    | 下一个上下文的地址                   |
    ---------------------------------------
    | 参数 7-n(假如回调函数的参数大于7个)   |
    ---------------------------------------
    |  返回地址                            | %rsp ->  ---------------------------------------

另外寄存器里的内容:

 %rdi,%rsi,%rdx,%rcx,%r8,%r9: 分别存储参数1-6

 %rbx   : 下一个上下文的地址

 %rsp   : 指向栈顶

当我

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

weixin_abctee123

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值