理解协程的实现
source link: https://mp.weixin.qq.com/s?__biz=MzI3NzA5MzUxNA%3D%3D&%3Bmid=2664608874&%3Bidx=1&%3Bsn=724f487252bc46bb3b9d8003d74724d4
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
glibc提供了四个函数给用户实现上下文的切换。
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架构)。
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架构下。他的定义是
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函数的时候的栈布局。
在这里插入图片描述movl 4(%esp), %eax
把getcontext函数入参的地址赋值给eax。即ucp指向的地址。
// oEAX是eax字段在ucontext_t结构中的位置,这里就是把ucontext_t中eax的值置为0 movl $0, oEAX(%eax) // 同上 movl %ecx, oECX(%eax) movl %edx, oEDX(%eax) movl %edi, oEDI(%eax) movl %esi, oESI(%eax) movl %ebp, oEBP(%eax) // 把esp指向的内存的内容赋给eip字段,这时候esp指向的内存保存的值是返回地址的值。即getcontext函数的下一条指令 movl (%esp), %ecx movl %ecx, oEIP(%eax) /* 把esp+4(保存第一个入参的内存的地址)对应的地址(而不是这个地址里存的值)赋给esp。 正常的函数执行流程是主函数压参,call指令压eip,然后调用子函数, 子函数压ebp,设置新的esp。返回的时候子函数,恢复esp,ebp。然后弹出eip。回到主函数。 这里模拟正常函数的调用过程。执行本上下文的eip时,相当于从一个子函数中返回, 这时候的栈顶应该是esp+4,即跳过eip和恢复ebp的过程。 */ leal 4(%esp), %ecx /* Exclude the return address. */ movl %ecx, oESP(%eax) movl %ebx, oEBX(%eax) xorl %edx, %edx movw %fs, %dx movl %edx, oFS(%eax)
整个代码下来,对照一开始的结构体。对号入座。这里提一下子函数的调用过程一般是
1 主函数入栈参数
2 call 执行子函数压入eip
3 子函数保存ebp,设置新的esp
4 恢复ebp和esp
5 ret 弹回eip返回到主函数
6 主函数恢复栈,即清除1中的入栈的参数
继续
// 取得ucontext结构体的uc_sigmask字段的地址 leal oSIGMASK(%eax), %edx // ecx清0 xorl %ecx, %ecx // 准备调用系统调用,设置系统调用的入参,ebx是第一个参数,ecx是第二个,edx是第三个 movl $SIG_BLOCK, %ebx // 调用系统调用sigprocmask,SIG_BLOCK是表示设置屏蔽信号,见sigprocmask函数解释,eax保存系统调用的调用号 movl $__NR_sigprocmask, %eax // 通过中断触发系统调用 int $0x80
这里是设置信号屏蔽的逻辑。我们看看该函数的声明。
// how操作类型(这里是设置屏蔽信号),设置信号屏蔽位为set,保存旧的信号屏蔽位oldset int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
所以根据上面的代码,翻译过来就是。
int sigprocmask(SIG_BLOCK, 0, &ucontext.uc_sigmask);
即保存旧的信号屏蔽信息。
getcontext函数大致逻辑就是上面。主要做了两个事情。
1 保存上下文。
2 保存旧的信号屏蔽信息
2 makecontext
makecontext是设置上下文的某些字段的信息
// ucontext_t结构体的地址 movl 4(%esp), %eax // 函数地址,即协程的工作函数,类似线程的工作函数 movl 8(%esp), %ecx // 设置ucontext_t的eip字段的值为函数的值 movl %ecx, oEIP(%eax) // 赋值ucontext_t.uc_stack.ss_sp(栈顶)给edx movl oSS_SP(%eax), %edx // oSS_SIZE为栈大小,这里设置edx指向栈底 addl oSS_SIZE(%eax), %edx
这时候的布局如下。
在这里插入图片描述movl 12(%esp), %ecx movl %ecx, oEBX(%eax)
保存makecontext的第三个参数(表示参数个数),到eax。
// 取负 negl %ecx // edx - ecx * 4 - 4。ecx * 4代表ecx个参数需要的空间,再减去4是保存oLINK的值(ucontext_t.ucontext的值) leal -4(%edx,%ecx,4), %edx // 恢复ecx的值 negl %ecx // 栈顶减4,即可以存储多一个数据,用于保存L(exitcode)的地址,见下面的L(exitcode) subl $4, %ed // 保存栈顶地址到ucontext_t movl %edx, oESP(%eax) // 把ucontext_t.uc_link的内存复制到栈中的第一个元素 movl oLINK(%eax), %eax // edx + ecx * 4 + 4指向保存ucontext_t.ucontext的值的内存地址。即保存ucontext_t.ucontext到该内存里 movl %eax, 4(%edx,%ecx,4) // ecx(参数个数)为0则跳到2,说明不需要复制参数 jecxz 2f // 循环复制参数 1: movl 12(%esp,%ecx,4), %eax movl %eax, (%edx,%ecx,4) decl %ecx jnz 1b // 把L(exitcode)的地址压入栈。L(exitcode)的内容下面继续分析 movl $L(exitcode), (%edx) // makecontext返回 ret
这时候的栈布局如下
在这里插入图片描述从上面的代码中我们知道,makecontext函数主要的功能是
1 设置协程的工作函数地址到上下文(ucontext_t)中。
2 在用户设置的栈上保存一些信息,并且设置栈顶指针的值到上下文中。
3 setcontext
setcontext是设置当前执行上下文。
movl 4(%esp), %eax
把当前需要执行的上下文(ucontext_t)赋值给eax。
xorl %edx, %edx leal oSIGMASK(%eax), %ecx movl $SIG_SETMASK, %ebx movl $__NR_sigprocmask, %eax int $0x80
这里是用getcontext里保存的信息,设置信号屏蔽位。
// 设置fs寄存器 movl oFS(%eax), %ecx movw %cx, %fs // 根据上下文设置栈顶,这个栈顶的值就是在makecontext里设置的(见上面的图) movl oESP(%eax), %esp // 把eip压入栈,setcontext返回的时候,从eip开始执行。eip在makecontext中设置,即工作函数的地址 movl oEIP(%eax), %ecx // 把工作函数的地址入栈 pushl %ecx
这时候的栈布局
在这里插入图片描述// 根据上下文设置其他寄存器 movl oEDI(%eax), %edi movl oESI(%eax), %esi movl oEBP(%eax), %ebp movl oEBX(%eax), %ebx movl oEDX(%eax), %edx movl oECX(%eax), %ecx movl oEAX(%eax), %eax // setcontext返回 ret
然后setcontext函数返回。ret指令会把当前栈顶的元素出栈,赋值给eip。即下一条要执行的指令的地址。我们从上图可以知道,栈顶这时候指向的元素是上下文的工作函数的地址。所以setcontext返回后,执行设置的上下文的工作函数。
这时候的栈布局
在这里插入图片描述当工作函数执行完之后,同样,栈顶的元素出栈,成为下一个eip。即L(exitcode)地址对应的指令会在工作函数执行完后执行。下面我们分析L(exitcode)。
L(exitcode): // 工作函数执行完了,他的入参也不需要了,释放栈空间。栈布局见下图 leal (%esp,%ebx,4), %esp
这时候的栈布局
在这里插入图片描述接着
// 这时候的栈顶指向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 %eax, %eax
4 swapcontext
swapcontext函数把当前执行的上下文保存到第一个参数中,然后设置第二个参数为当前执行上下文。
// 把第一个参数的地址赋值给eax movl 4(%esp), %eax movl $0, oEAX(%eax) // 保存当前执行上下文 movl %ecx, oECX(%eax) movl %edx, oEDX(%eax) movl %edi, oEDI(%eax) movl %esi, oESI(%eax) movl %ebp, oEBP(%eax) movl %ebx, oEBX(%eax) // esp指向的内存保存了swapcontext函数下一条指令的地址,保存到上下文的eip字段中 movl (%esp), %ecx movl %ecx, oEIP(%eax) // 保存栈到上下文。模拟正常函数的调用过程。见getcontext的分析 leal 4(%esp), %ecx movl %ecx, oESP(%eax) // 保存fs寄存器 xorl %edx, %edx movw %fs, %dx movl %edx, oFS(%eax)
swapcontext首先是保存当前执行上下文到第一个参数中。
// 把swapcontext的第二个参数赋值给ecx movl 8(%esp), %ecx // 把旧的信号屏蔽位信息保存到swapcontext的第一个参数中,设置信号屏蔽位为swapcontext的第二个参数中的值 leal oSIGMASK(%eax), %edx leal oSIGMASK(%ecx), %ecx movl $SIG_SETMASK, %ebx movl $__NR_sigprocmask, %eax int $0x80
然后设置新的执行上下文
// 设置fs寄存器 movl oFS(%eax), %edx movw %dx, %fs // 设置栈顶 movl oESP(%eax), %esp // 即将执行的上下文的eip压入栈,swapcontext函数返回的时候从这个开始执行(工作函数) movl oEIP(%eax), %ecx pushl %ecx // 设置其他寄存器 movl oEDI(%eax), %edi movl oESI(%eax), %esi movl oEBP(%eax), %ebp movl oEBX(%eax), %ebx movl oEDX(%eax), %edx movl oECX(%eax), %ecx movl oEAX(%eax), %eax
四个函数分析完了,主要的工作是对寄存器的一些保存和设置,实现任意跳转。最后我们看一下例子。
#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: exiting\n"); exit(EXIT_SUCCESS); }
所以整个流程是uctx_func2->uctx_func1->uctx_main
最后执行
printf("main: exiting\n"); exit(EXIT_SUCCESS);
然后退出。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK