转自: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