理解协程的实现

2020-04-14 16:44:17 浏览数 (2)

glibc提供了四个函数给用户实现上下文的切换。

代码语言:javascript复制
int getcontext(ucontext_t *ucp);
int setcontext(const ucontext_t *ucp);
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);

glibc提高的功能类似早期setjmp和longjmp。本质上是保存当前的执行上下文到一个变量中,然后去做其他事情。在某个时机再切换回来。从上面函数的名字中,我们大概能知道,这些函数的作用。我们先看一下表示上下文的数据结构(x86架构)。

代码语言:javascript复制
   typedef struct ucontext_t {
           unsigned long int uc_flags;
           // 下一个执行上下文,执行完本上下文,就知道uc_link的上下文
        struct ucontext_t *uc_link;
        // 信号屏蔽位
        sigset_t          uc_sigmask;
        /*
            栈信息
            typedef struct
              {
                void *ss_sp;
                int ss_flags;
                size_t ss_size;
              } stack_t
        */
        stack_t           uc_stack;
        // 平台相关的上下文数据结构
        mcontext_t        uc_mcontext;
        ...
    } ucontext_t;

我们看到ucontext_t是对上下文实现一个更高层次的封装。真正的上下文由mcontext_t结构体表示。比如在x86架构下。他的定义是

代码语言:javascript复制
typedef struct
  {
      /*
          typedef int greg_t;
        typedef greg_t gregset_t[19]
        gregs是保存寄存器上下文的
    */
    gregset_t gregs;
    fpregset_t fpregs;
    unsigned long int oldmask;
    unsigned long int cr2;
  } mcontext_t;

整个布局如下

在这里插入图片描述 我们了解了基本的数据结构,然后开始分析一开始提到的四个函数。

1 int getcontext(ucontext_t *ucp)

getcontext是把当前执行的上下文保存到ucp中。我们看看他大致的实现。他是用汇编实现的。首先看一下开始执行getcontext函数的时候的栈布局。

在这里插入图片描述

代码语言:javascript复制
movl    4(%esp), �x

把getcontext函数入参的地址赋值给eax。即ucp指向的地址。

代码语言:javascript复制
    // oEAX是eax字段在ucontext_t结构中的位置,这里就是把ucontext_t中eax的值置为0
    movl    $0, oEAX(�x)
    // 同上
    movl    �x, oECX(�x)
    movl    �x, oEDX(�x)
    movl    �i, oEDI(�x)
    movl    %esi, oESI(�x)
    movl    �p, oEBP(�x)
    // 把esp指向的内存的内容赋给eip字段,这时候esp指向的内存保存的值是返回地址的值。即getcontext函数的下一条指令
    movl    (%esp), �x
    movl    �x, oEIP(�x)
    /*
     把esp 4(保存第一个入参的内存的地址)对应的地址(而不是这个地址里存的值)赋给esp。

     正常的函数执行流程是主函数压参,call指令压eip,然后调用子函数,
     子函数压ebp,设置新的esp。返回的时候子函数,恢复esp,ebp。然后弹出eip。回到主函数。

     这里模拟正常函数的调用过程。执行本上下文的eip时,相当于从一个子函数中返回,
     这时候的栈顶应该是esp 4,即跳过eip和恢复ebp的过程。
    */
    leal    4(%esp), �x       /* Exclude the return address.  */
    movl    �x, oESP(�x)
    movl    �x, oEBX(�x)

    xorl    �x, �x
    movw    %fs, %dx
    movl    �x, oFS(�x)

整个代码下来,对照一开始的结构体。对号入座。这里提一下子函数的调用过程一般是 1 主函数入栈参数 2 call 执行子函数压入eip 3 子函数保存ebp,设置新的esp 4 恢复ebp和esp 5 ret 弹回eip返回到主函数 6 主函数恢复栈,即清除1中的入栈的参数

继续

代码语言:javascript复制
    // 取得ucontext结构体的uc_sigmask字段的地址
    leal    oSIGMASK(�x), �x
    // ecx清0
    xorl    �x, �x
    // 准备调用系统调用,设置系统调用的入参,ebx是第一个参数,ecx是第二个,edx是第三个
    movl    $SIG_BLOCK, �x
    // 调用系统调用sigprocmask,SIG_BLOCK是表示设置屏蔽信号,见sigprocmask函数解释,eax保存系统调用的调用号
    movl    $__NR_sigprocmask, �x
    // 通过中断触发系统调用
    int $0x80

这里是设置信号屏蔽的逻辑。我们看看该函数的声明。

代码语言:javascript复制
// how操作类型(这里是设置屏蔽信号),设置信号屏蔽位为set,保存旧的信号屏蔽位oldset
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

所以根据上面的代码,翻译过来就是。

代码语言:javascript复制
int sigprocmask(SIG_BLOCK, 0, &ucontext.uc_sigmask);

即保存旧的信号屏蔽信息。 getcontext函数大致逻辑就是上面。主要做了两个事情。 1 保存上下文。 2 保存旧的信号屏蔽信息

2 makecontext

makecontext是设置上下文的某些字段的信息

代码语言:javascript复制
// ucontext_t结构体的地址
movl    4(%esp), �x
// 函数地址,即协程的工作函数,类似线程的工作函数
movl    8(%esp), �x
// 设置ucontext_t的eip字段的值为函数的值
movl    �x, oEIP(�x)
// 赋值ucontext_t.uc_stack.ss_sp(栈顶)给edx
movl    oSS_SP(�x), �x
// oSS_SIZE为栈大小,这里设置edx指向栈底
addl    oSS_SIZE(�x), �x

这时候的布局如下。

在这里插入图片描述

代码语言:javascript复制
    movl    12(%esp), �x
    movl    �x, oEBX(�x)

保存makecontext的第三个参数(表示参数个数),到eax。

代码语言:javascript复制
    // 取负
    negl    �x
    // edx - ecx * 4 - 4。ecx * 4代表ecx个参数需要的空间,再减去4是保存oLINK的值(ucontext_t.ucontext的值)
    leal    -4(�x,�x,4), �x
    // 恢复ecx的值
    negl    �x
    // 栈顶减4,即可以存储多一个数据,用于保存L(exitcode)的地址,见下面的L(exitcode)
    subl    $4, �
    // 保存栈顶地址到ucontext_t
    movl    �x, oESP(�x)
    // 把ucontext_t.uc_link的内存复制到栈中的第一个元素
    movl    oLINK(�x), �x
    // edx   ecx * 4   4指向保存ucontext_t.ucontext的值的内存地址。即保存ucontext_t.ucontext到该内存里
    movl    �x, 4(�x,�x,4)
    // ecx(参数个数)为0则跳到2,说明不需要复制参数
    jecxz   2f
// 循环复制参数
1:    movl    12(%esp,�x,4), �x
    movl    �x, (�x,�x,4)
    decl    �x
    jnz 1b
    // 把L(exitcode)的地址压入栈。L(exitcode)的内容下面继续分析
    movl    $L(exitcode), (�x)
    // makecontext返回
    ret

这时候的栈布局如下

在这里插入图片描述 从上面的代码中我们知道,makecontext函数主要的功能是 1 设置协程的工作函数地址到上下文(ucontext_t)中。 2 在用户设置的栈上保存一些信息,并且设置栈顶指针的值到上下文中。

3 setcontext

setcontext是设置当前执行上下文。

代码语言:javascript复制
movl    4(%esp), �x

把当前需要执行的上下文(ucontext_t)赋值给eax。

代码语言:javascript复制
    xorl    �x, �x
    leal    oSIGMASK(�x), �x
    movl    $SIG_SETMASK, �x
    movl    $__NR_sigprocmask, �x
    int $0x80

这里是用getcontext里保存的信息,设置信号屏蔽位。

代码语言:javascript复制
    // 设置fs寄存器
    movl    oFS(�x), �x
    movw    %cx, %fs

    // 根据上下文设置栈顶,这个栈顶的值就是在makecontext里设置的(见上面的图)
    movl    oESP(�x), %esp
    // 把eip压入栈,setcontext返回的时候,从eip开始执行。eip在makecontext中设置,即工作函数的地址
    movl    oEIP(�x), �x
    // 把工作函数的地址入栈
    pushl   �x

这时候的栈布局

在这里插入图片描述

代码语言:javascript复制
    // 根据上下文设置其他寄存器
    movl    oEDI(�x), �i
    movl    oESI(�x), %esi
    movl    oEBP(�x), �p
    movl    oEBX(�x), �x
    movl    oEDX(�x), �x
    movl    oECX(�x), �x
    movl    oEAX(�x), �x
    // setcontext返回
    ret

然后setcontext函数返回。ret指令会把当前栈顶的元素出栈,赋值给eip。即下一条要执行的指令的地址。我们从上图可以知道,栈顶这时候指向的元素是上下文的工作函数的地址。所以setcontext返回后,执行设置的上下文的工作函数。 这时候的栈布局

在这里插入图片描述 当工作函数执行完之后,同样,栈顶的元素出栈,成为下一个eip。即L(exitcode)地址对应的指令会在工作函数执行完后执行。下面我们分析L(exitcode)。

代码语言:javascript复制
L(exitcode):
    // 工作函数执行完了,他的入参也不需要了,释放栈空间。栈布局见下图
    leal    (%esp,�x,4), %esp

这时候的栈布局

在这里插入图片描述 接着

代码语言:javascript复制
    // 这时候的栈顶指向ucontext_t.uc_link的值,即下一个要执行的协程。
    cmpl    $0, (%esp) 
    // 如果没有要执行的协程。则跳到2正常退出  
    je  2f          /* If it is zero exit.  */
    // 否则继续setcontext,入参是上图esp指向的ucontext_t.uc_link
    call    HIDDEN_JUMPTARGET(__setcontext)
    // setcontext返回后会从新的eip开始执行,如果执行下面的指令说明setcontext执行出错了。调用exit退出
    jmp L(call_exit)

2:
    /* Exit with status 0.  */
    xorl    �x, �x

4 swapcontext

swapcontext函数把当前执行的上下文保存到第一个参数中,然后设置第二个参数为当前执行上下文。

代码语言:javascript复制
    // 把第一个参数的地址赋值给eax
    movl    4(%esp), �x
    movl    $0, oEAX(�x)
    // 保存当前执行上下文
    movl    �x, oECX(�x)
    movl    �x, oEDX(�x)
    movl    �i, oEDI(�x)
    movl    %esi, oESI(�x)
    movl    �p, oEBP(�x)
    movl    �x, oEBX(�x)

    // esp指向的内存保存了swapcontext函数下一条指令的地址,保存到上下文的eip字段中
    movl    (%esp), �x
    movl    �x, oEIP(�x)
    // 保存栈到上下文。模拟正常函数的调用过程。见getcontext的分析
    leal    4(%esp), �x
    movl    �x, oESP(�x)

    // 保存fs寄存器
    xorl    �x, �x
    movw    %fs, %dx
    movl    �x, oFS(�x)

swapcontext首先是保存当前执行上下文到第一个参数中。

代码语言:javascript复制
// 把swapcontext的第二个参数赋值给ecx
movl    8(%esp), �x
// 把旧的信号屏蔽位信息保存到swapcontext的第一个参数中,设置信号屏蔽位为swapcontext的第二个参数中的值
leal    oSIGMASK(�x), �x
leal    oSIGMASK(�x), �x
movl    $SIG_SETMASK, �x
movl    $__NR_sigprocmask, �x
int $0x80

然后设置新的执行上下文

代码语言:javascript复制
    // 设置fs寄存器
    movl    oFS(�x), �x
    movw    %dx, %fs
    // 设置栈顶
    movl    oESP(�x), %esp
    // 即将执行的上下文的eip压入栈,swapcontext函数返回的时候从这个开始执行(工作函数)
    movl    oEIP(�x), �x
    pushl   �x
    // 设置其他寄存器
    movl    oEDI(�x), �i
    movl    oESI(�x), %esi
    movl    oEBP(�x), �p
    movl    oEBX(�x), �x
    movl    oEDX(�x), �x
    movl    oECX(�x), �x
    movl    oEAX(�x), �x

四个函数分析完了,主要的工作是对寄存器的一些保存和设置,实现任意跳转。最后我们看一下例子。

代码语言:javascript复制
       #include <ucontext.h>
       #include <stdio.h>
       #include <stdlib.h>

       static ucontext_t uctx_main, uctx_func1, uctx_func2;

       #define handle_error(msg) 
           do { perror(msg); exit(EXIT_FAILURE); } while (0)

       static void
       func1(void)
       {
           if (swapcontext(&uctx_func1, &uctx_func2) == -1)
               handle_error("swapcontext");
       }

       static void
       func2(void)
       {
           if (swapcontext(&uctx_func2, &uctx_func1) == -1)
               handle_error("swapcontext");
       }

       int
       main(int argc, char *argv[])
       {
           char func1_stack[16384];
           char func2_stack[16384];
            // 保存当前的执行上下文
           if (getcontext(&uctx_func1) == -1)
               handle_error("getcontext");
           // 设置新的栈
           uctx_func1.uc_stack.ss_sp = func1_stack;
           uctx_func1.uc_stack.ss_size = sizeof(func1_stack);
           // uctx_func1对应的协程执行完执行uctx_main
           uctx_func1.uc_link = &uctx_main;
           // 设置协作的工作函数
           makecontext(&uctx_func1, func1, 0);
           // 同上
           if (getcontext(&uctx_func2) == -1)
               handle_error("getcontext");
           uctx_func2.uc_stack.ss_sp = func2_stack;
           uctx_func2.uc_stack.ss_size = sizeof(func2_stack);
           // uctx_func2执行完执行uctx_func1
           uctx_func2.uc_link = (argc > 1) ? NULL : &uctx_func1;
           makecontext(&uctx_func2, func2, 0);
           // 保存当前执行上下文到uctx_main,然后开始执行uctx_func2对应的上下文
            if (swapcontext(&uctx_main, &uctx_func2) == -1)
               handle_error("swapcontext");

           printf("main: exitingn");
           exit(EXIT_SUCCESS);
       }

所以整个流程是uctx_func2->uctx_func1->uctx_main 最后执行

代码语言:javascript复制
printf("main: exitingn");
exit(EXIT_SUCCESS);

然后退出。

0 人点赞