setcontext getcontext makecontext swapcontext

转自:https://blog.csdn.net/mrpre/article/details/78699865

 

Linux上下文切换以及协程        上下文切换,听起来虚无缥缈,什么是上下文,切换又是指的是什么?其实上下文就可以理解为一个进程所运行的相关的寄存器值,即包括sp/bp/pc等值,换句话说,一个上下文,就是包括了能够恢复进程运行所需要的所有必要的东西。所谓的切换, 那是多进程的操作系统必要的功能,一个CPU能够运行多个进程(看起来),那么必然要在多个进程之间不停的切换,A切换到B时,必然需要保存A相关信息,这样才能从B切换回来时接着运行A,且正确的运行A。     不过上面描述的都是OS的事情, 一个进程或者一个线程在内核都是由struct task_struct描述的,OS的调度对象就是task_struct。而在用户态想实现类似的功能,那么必然需要对应的库函数。根据上面的描述,我们已经知道,要实现所谓的调度,那么必须保存相关寄存器信息。   先看例test1.c     #include #include int done = 0; int main() { ucontext_t context; getcontext(&context); if (done) { printf("return from getcontext,exit\n"); return 0; } done = 1; setcontext(&context); return 0;//never goto here! }     程序先调用getcontext保存当前寄存器信息到了context中,然后执行setcontext,所谓的setcontext就是把context中的寄存器信息恢复到当前的寄存器信息,也就是说,强制把context的pc bp sp等值,赋值到了当前cpu的寄存中,显而易见的是,这又跳回去到了getcontext 处。       上面这个goto一样,那用goto就行了?那看下面这个例子(test2.c):     #include #include int done = 0; int func1(ucontext_t *context) { done = 1; setcontext(context); } int main() { ucontext_t context; getcontext(&context); if (done) { printf("return from getcontext,exit\n"); return 0; } func1(&context); return 0; } goto 一定做不了函数之间的跳转,只能做本地跳转。当然 getcontet 和 setcontext 肯定不仅仅只有这些功能,看下面这个例子。         //test3.c #include #include #include void func() { printf("in func\n"); } int main() { ucontext_t context; getcontext(&context); //指定栈 context.uc_stack.ss_sp = malloc(10000); context.uc_stack.ss_size = 10000; context.uc_link = NULL; makecontext(&context, func, 0); setcontext(&context); return 0;//never goto here! } 先setcontext初始化context,然后makecontext,指定跳转的函数,然后再setcontext切换context到func函数。 除此之外,还为新的context指定了新的栈,为什么的?因为如果不指定栈,那么栈还是getcontext时获取的sp bp指针,sp bp描述的是main函数的栈大小,如果main函数栈大小是100字节,但是你所要执行的func的栈大小需要1000字节,显然不够用。       看到这,肯定好多人心里很是疑惑,如果执行完func,还会返回到main吗?答是不会。为什么不会?难道func不是man调用的吗?怎么会不返回到main呢?下面我们就讲讲这个问题,不过讲这个问题前,希望大家自行去了解一下x86、x64的调用规则。 我们知道一个main作为一个函数,肯定是被其他地方调用的。 我们写一个简单的main函数,然后断点打住,推栈,返现其调用者是 __libc_start_main:   Breakpoint 3, 0x00000000004004d8 in main () at main.c:7 7 } (gdb) disassemble Dump of assembler code for function main: 0x00000000004004c4 : push %rbp 0x00000000004004c5 : mov %rsp,%rbp 0x00000000004004c8 : mov $0xa,%edi 0x00000000004004cd : callq 0x4003b8 0x00000000004004d2 : mov $0x0,%eax 0x00000000004004d7 : leaveq => 0x00000000004004d8 : retq End of assembler dump. (gdb) i r rax 0x0 0 rbx 0x0 0 rcx 0xffffffff 4294967295 rdx 0x7ffff77d1e10 140737345560080 rsi 0x7ffff7ff7000 140737354100736 rdi 0x0 0 rbp 0xcc 0xcc rsp 0x7fffffffe528 0x7fffffffe528 r8 0xffffffff 4294967295 r9 0xa 10 r10 0xffffffff 4294967295 r11 0x246 582 r12 0x4003e0 4195296 r13 0x7fffffffe600 140737488348672 r14 0x0 0 r15 0x0 0 rip 0x4004d8 0x4004d8 eflags 0x246 [ PF ZF IF ] cs 0x33 51 ss 0x2b 43 ds 0x0 0 es 0x0 0 fs 0x0 0 gs 0x0 0 (gdb) x/40xg 0x7fffffffe528 0x7fffffffe528: 0x00007ffff7460d5d 0x0000000000000000 0x7fffffffe538: 0x00007fffffffe608 0x0000000100000000 0x7fffffffe548: 0x00000000004004c4 0x0000000000000000 0x7fffffffe558: 0x6fcc4be0773d5f7c 0x00000000004003e0 0x7fffffffe568: 0x00007fffffffe600 0x0000000000000000 0x7fffffffe578: 0x0000000000000000 0x9033b41fbd5d5f7c 0x7fffffffe588: 0x9033a56c6d0d5f7c 0x00007fff00000000 0x7fffffffe598: 0x0000000000000000 0x0000000000000000 0x7fffffffe5a8: 0x00000000004004f0 0x00007fffffffe608 0x7fffffffe5b8: 0x0000000000000001 0x0000000000000000 0x7fffffffe5c8: 0x0000000000000000 0x00000000004003e0 0x7fffffffe5d8: 0x00007fffffffe600 0x0000000000000000 0x7fffffffe5e8: 0x0000000000400409 0x00007fffffffe5f8 0x7fffffffe5f8: 0x000000000000001c 0x0000000000000001 0x7fffffffe608: 0x00007fffffffe805 0x0000000000000000 0x7fffffffe618: 0x00007fffffffe822 0x00007fffffffe83d 0x7fffffffe628: 0x00007fffffffe84d 0x00007fffffffe861 0x7fffffffe638: 0x00007fffffffe872 0x00007fffffffee7f 0x7fffffffe648: 0x00007fffffffee95 0x00007fffffffeea6 0x7fffffffe658: 0x00007fffffffeebb 0x00007fffffffeec7 (gdb) info symbol 0x00007ffff7460d5d __libc_start_main + 253 in section .text of /lib64/libc.so.6 main函数返回后,执行了0x00007ffff7460d5d,也直接调用了exit,结束进程。 0x00007ffff7460d5d: 0x00007ffff7460d56 : mov (%rax),%rdx 0x00007ffff7460d59 : callq *0x18(%rsp) 0x00007ffff7460d5d : mov %eax,%edi 0x00007ffff7460d5f : callq 0x7ffff7477a40 说了那么多,那么我们 test3.c 的func是谁调用的?如果简单的想 其调用栈肯定是 main->setcontext->func ,但是我上面说了,并不是这样的。   我们在func打断点看一下:   Breakpoint 1, func () at test3.c:6 6 printf("in func\n"); Missing separate debuginfos, use: debuginfo-install glibc-2.12-1.166.alios6.7.x86_64 (gdb) bt #0 func () at test3.c:6 #1 0x00007ffff74858f0 in ?? () from /lib64/libc.so.6 #2 0x0000000000000000 in ?? () (gdb) disassemble Dump of assembler code for function func: 0x00000000004005f4 : push %rbp 0x00000000004005f5 : mov %rsp,%rbp => 0x00000000004005f8 : mov $0x400778,%edi 0x00000000004005fd : callq 0x4004a8 0x0000000000400602 : leaveq 0x0000000000400603 : retq End of assembler dump. (gdb) i r rax 0x0 0 rbx 0x603710 6305552 rcx 0x0 0 rdx 0x7fffffffe618 140737488348696 rsi 0x7fffffffe608 140737488348680 rdi 0x7fffffffe170 140737488347504 rbp 0x603700 0x603700 rsp 0x603700 0x603700 r8 0x7ffff77d1300 140737345557248 r9 0x7ffff7debac0 140737351957184 r10 0x8 8 r11 0x246 582 r12 0x400510 4195600 r13 0x7fffffffe600 140737488348672 r14 0x0 0 r15 0x0 0 rip 0x4005f8 0x4005f8 eflags 0x246 [ PF ZF IF ] cs 0x33 51 ss 0x2b 43 ds 0x0 0 es 0x0 0 fs 0x0 0 gs 0x0 0 (gdb) x/40xg 0x603700 0x603700: 0x00007fffffffe520 0x00007ffff74858f0 0x603710: 0x0000000000000000 0x0000000000000000 0x603720: 0x0000000000000000 0x00000000000208e1 0x603730: 0x0000000000000000 0x0000000000000000 0x603740: 0x0000000000000000 0x0000000000000000 0x603750: 0x0000000000000000 0x0000000000000000 0x603760: 0x0000000000000000 0x0000000000000000 0x603770: 0x0000000000000000 0x0000000000000000 0x603780: 0x0000000000000000 0x0000000000000000 0x603790: 0x0000000000000000 0x0000000000000000 0x6037a0: 0x0000000000000000 0x0000000000000000 0x6037b0: 0x0000000000000000 0x0000000000000000 0x6037c0: 0x0000000000000000 0x0000000000000000 0x6037d0: 0x0000000000000000 0x0000000000000000 0x6037e0: 0x0000000000000000 0x0000000000000000 0x6037f0: 0x0000000000000000 0x0000000000000000 0x603800: 0x0000000000000000 0x0000000000000000 0x603810: 0x0000000000000000 0x0000000000000000 0x603820: 0x0000000000000000 0x0000000000000000 0x603830: 0x0000000000000000 0x0000000000000000 (gdb) info symbol 0x00007ffff74858f0 __start_context in section .text of /lib64/libc.so.6 func的栈地址是0x603700 他是我们的main函数malloc得到的 地址+栈大小 从而得到的栈顶(还有一定的偏移)。 现在问题来了,func是由__start_context调用的 ,而且从 x/40xg 0x603700 指令可以看出,除了__start_context,栈其余全是0,也就意味着没有人调用__start_context。也意味着没有从main->setcontext  …  一路调用到func。很奇怪把,但是仔细想想,所谓栈,也就是内存,内存里是什么东西,肯定有人放的。   其实能够猜到__start_context是被刻意安排在栈中的。即刻意安排在0x603700中的,好让func执行 retq返回时,读取的pc指针是__start_context。   ss_sp就是我们在main函数中malloc的地址,sp = ss_sp + ss_size就指向了栈顶。 我们在sp[0]安排了一个地址&__start_context,而func的函数栈就是这个sp,func在最后执行ret时,会pop 这个 sp[0],然后放到自己的pc指针上,然后跳到pc处,也就是说,func的返回值就是__start_context。 好,现在看看__start_context干了些什么事情,以为从前面推栈的结果来看没人调用了__start_context,所以猜想里面肯定直接调用了exit了。   (gdb) disassemble __start_context Dump of assembler code for function __start_context: 0x00007ffff74858f0 : mov %rbx,%rsp 0x00007ffff74858f3 : pop %rdi 0x00007ffff74858f4 : test %rdi,%rdi 0x00007ffff74858f7 : je 0x7ffff74858fe 0x00007ffff74858f9 : callq 0x7ffff7483090 0x00007ffff74858fe : mov %rax,%rdi 0x00007ffff7485901 : callq 0x7ffff7477a40 0x00007ffff7485906 : hlt End of assembler dump. func返回到__start_context后,就exit。       总结一下, 在执行func时,在其栈顶放了一个__start_context,这样func执行ret时,就执行到了__start_context。换句话说,并不是__start_context调用了func,而是func返回到了__start_context。     仔细看__start_context函数汇编,他有一个可能就是不执行 exit,而是执行setcontext,判断条件就是%rdi是否是0,不是0就指向setcontext了,这个又是什么逻辑呢。且看test4.c       #include #include #include int did = 0; void func() { did = 1; printf("in func\n"); } int main() { ucontext_t context,rt; getcontext(&context); getcontext(&rt); if(did == 1) { printf("continue from func\n"); return 0; } //指定栈 context.uc_stack.ss_sp = malloc(10000); context.uc_stack.ss_size = 10000; context.uc_link = &rt; makecontext(&context, func, 0); setcontext(&context); return 0;//never goto here! }   test4.c比test3.c就多个一个对uc_link的赋值(还有一些流程控制变量)。uc_link就指定了func执行完之后,接着执行的上下文,如果为空,则执行完func后,exit,就像test3.c一样;如果uc_link不为空,则执行完func后,接着执行uc_link就指定的上下文。   就如__start_context逻辑一致。 接下来,看test4.c,我们从getcontext(&rt);处返回了,但是能不能从setcontext处返回呢,当然可以,方法1就是getcontext(&rt);返回后,goto到setcontext 后面。但是glibc还提供了一个接口就是swapcontext。且看下面那个例子。           #include #include #include int did = 0; void func() { did = 1; printf("in func\n"); } int main() { ucontext_t context,rt; getcontext(&context); //指定栈 context.uc_stack.ss_sp = malloc(10000); context.uc_stack.ss_size = 10000; context.uc_link = &rt; makecontext(&context, func, 0); swapcontext(&rt,&context); printf("finally return\n"); return 0;//should goto here! } swapcontext 两件事,一个就是保存当前的上下文,到rt中,然后切换到context。context指定的上下文执行完成之后,就跳到了swapcontext后面了。   注意:这些xxxcontext函数给予了用户态程序“调度的功能”,这和os的调度不同,os是对各个线程,即task_struct进行切换调度,而各个线程内部,也就是用户态程序,可以用上面这些库函数进行颗粒度更小的调度(说切换更合理)。这就是所谓的协程的概念。 --------------------- 本文来自 Mrpre 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/mrpre/article/details/78699865?utm_source=copy

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
setcontext 函数是一个底层的系统调用函数,用于设置当前线程的上下文。它的使用比较复杂,需要先创建一个 ucontext_t 结构体,然后使用 getcontext 函数获取当前线程的上下文,再修改 ucontext_t 结构体中的内容,最后使用 setcontext 函数将修改后的上下文设置为当前线程的上下文。以下是一个简单的示例代码: ``` #include <ucontext.h> #include <stdio.h> void func1() { printf("Hello from func1\n"); } void func2() { printf("Hello from func2\n"); } int main() { ucontext_t ctx1, ctx2; getcontext(&ctx1); ctx1.uc_stack.ss_sp = malloc(8192); ctx1.uc_stack.ss_size = 8192; ctx1.uc_link = &ctx2; makecontext(&ctx1, func1, 0); getcontext(&ctx2); ctx2.uc_stack.ss_sp = malloc(8192); ctx2.uc_stack.ss_size = 8192; ctx2.uc_link = &ctx1; makecontext(&ctx2, func2, 0); setcontext(&ctx1); return 0; } ``` 这个示例代码创建了两个上下文,分别对应两个函数 func1 和 func2。在 main 函数中,先获取了 ctx1 的上下文,然后设置了它的堆栈和链接到 ctx2,最后使用 makecontext 函数将 func1 设置为 ctx1 的执行函数。接着获取了 ctx2 的上下文,设置了它的堆栈和链接到 ctx1,最后使用 makecontext 函数将 func2 设置为 ctx2 的执行函数。最后使用 setcontext 函数将 ctx1 的上下文设置为当前线程的上下文,从而开始执行 func1 函数。当 func1 函数执行完毕后,会自动跳转到 ctx2 的上下文,开始执行 func2 函数。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值