动态原理图 协程_理解协程的实现

原标题:理解协程的实现

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

intgetcontext( ucontext_t*ucp);

intsetcontext( constucontext_t*ucp);

voidmakecontext( ucontext_t*ucp, void(*func), intargc, ...);

intswapcontext( ucontext_t*oucp, constucontext_t*ucp);

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

typedefstructucontext_t{

unsignedlongintuc_flags;

// 下一个执行上下文,执行完本上下文,就知道uc_link的上下文

structucontext_t* uc_link;

// 信号屏蔽位

sigset_tuc_sigmask;

/*

栈信息

typedef struct

{

void *ss_sp;

int ss_flags;

size_t ss_size;

} stack_t

*/

stack_tuc_stack;

// 平台相关的上下文数据结构

mcontext_tuc_mcontext;

...

} ucontext_t;

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

typedefstruct

{

/*

typedef int greg_t;

typedef greg_t gregset_t[19]

gregs是保存寄存器上下文的

*/

gregset_tgregs;

fpregset_tfpregs;

unsignedlongintoldmask;

unsignedlongintcr2;

} 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

intsigprocmask( inthow, constsigset_t* set, sigset_t*oldset);

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

intsigprocmask(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

# include

# include

staticucontext_tuctx_main, uctx_func1, uctx_func2;

# definehandle_error(msg)

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

staticvoid

func1( void)

{

if(swapcontext(&uctx_func1, &uctx_func2) == -1)

handle_error( "swapcontext");

}

staticvoid

func2( void)

{

if(swapcontext(&uctx_func2, &uctx_func1) == -1)

handle_error( "swapcontext");

}

int

main( intargc, char*argv[])

{

charfunc1_stack[ 16384];

charfunc2_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

最后执行

printf( "main: exitingn");

exit(EXIT_SUCCESS);

责任编辑:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值