Linux上下文切换以及协程
上下文切换,听起来虚无缥缈,什么是上下文,切换又是指的是什么?其实上下文就可以理解为一个进程所运行的相关的寄存器值,即包括sp/bp/pc等值,换句话说,一个上下文,就是包括了能够恢复进程运行所需要的所有必要的东西。所谓的切换, 那是多进程的操作系统必要的功能,一个CPU能够运行多个进程(看起来),那么必然要在多个进程之间不停的切换,A切换到B时,必然需要保存A相关信息,这样才能从B切换回来时接着运行A,且正确的运行A。
不过上面描述的都是OS的事情, 一个进程或者一个线程在内核都是由struct task_struct描述的,OS的调度对象就是task_struct。而在用户态想实现类似的功能,那么必然需要对应的库函数。根据上面的描述,我们已经知道,要实现所谓的调度,那么必须保存相关寄存器信息。
先看例test1.c
#include <ucontext.h>
#include <stdio.h>
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 <ucontext.h>
#include <stdio.h>
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 <ucontext.h>
#include <stdio.h>
#include <malloc.h>
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 <+0>: push %rbp
0x00000000004004c5 <+1>: mov %rsp,%rbp
0x00000000004004c8 <+4>: mov $0xa,%edi
0x00000000004004cd <+9>: callq 0x4003b8 <putchar@plt>
0x00000000004004d2 <+14>: mov $0x0,%eax
0x00000000004004d7 <+19>: leaveq
=> 0x00000000004004d8 <+20>: 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 <main+20>
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 <+246>: mov (%rax),%rdx
0x00007ffff7460d59 <+249>: callq *0x18(%rsp)
0x00007ffff7460d5d <+253>: mov %eax,%edi
0x00007ffff7460d5f <+255>: callq 0x7ffff7477a40 <exit>
说了那么多,那么我们 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 <+0>: push %rbp
0x00000000004005f5 <+1>: mov %rsp,%rbp
=> 0x00000000004005f8 <+4>: mov $0x400778,%edi
0x00000000004005fd <+9>: callq 0x4004a8 <puts@plt>
0x0000000000400602 <+14>: leaveq
0x0000000000400603 <+15>: 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 <func+4>
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 <+0>: mov %rbx,%rsp
0x00007ffff74858f3 <+3>: pop %rdi
0x00007ffff74858f4 <+4>: test %rdi,%rdi
0x00007ffff74858f7 <+7>: je 0x7ffff74858fe <__start_context+14>
0x00007ffff74858f9 <+9>: callq 0x7ffff7483090 <setcontext>
0x00007ffff74858fe <+14>: mov %rax,%rdi
0x00007ffff7485901 <+17>: callq 0x7ffff7477a40 <exit>
0x00007ffff7485906 <+22>: 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 <ucontext.h>
#include <stdio.h>
#include <malloc.h>
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 <ucontext.h>
#include <stdio.h>
#include <malloc.h>
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进行切换调度,而各个线程内部,也就是用户态程序,可以用上面这些库函数进行颗粒度更小的调度(说切换更合理)。这就是所谓的协程的概念。