spp微线程源码解析1--context switch

spp(server plus plus) 是Tencent SNG大规模使用的服务框架,久经考验,即使是今天,存量的spp服务依旧难以计数,spp历经三个大版本(同步,异步,协程),而最新spp的精髓就在其协程库,之后会出多篇文章解析其用法和核心源码以及设计思想。

spp的源码,已经由tencent开源

https://github.com/Tencent/MSEC/tree/master/spp_rpc/src/sync_frame/micro_thread

预备知识

请先了解 x86_64 c 函数调用约定。但是这里还是简单提下

例如调用 Foo(uint64_t a,uint64_t b……); 这里假设参数大小都不大于8 bytes,因为大于8 bytes的参数应该用指针传递

  1. 64 位下,函数从左到右的参数分布存入 rdi rsi rdx
  2. 参数存储到寄存器后 call Foo ,我们需要知道call 指令发生了什么,call指令会把 下一条指令的地址 push到 stack上,并且rsp 自动减 8,然后Foo的地址会被赋值给pc寄存器,即接下来cpu将从Foo处开始执行
  3. 函数的返回值保存在rax里面

函数签名

1
2
3
extern "C"  int save_context(jmp_buf jbf);
extern "C" void restore_context(jmp_buf jbf, int ret);
extern "C" void replace_esp(jmp_buf jbf, void* esp);

save_context

1
2
3
4
5
6
7
8
9
10
11
12
13
save_context:
pop %rsi
xorl %eax,%eax
movq %rbx,(%rdi)
movq %rsp,8(%rdi)
push %rsi
movq %rbp,16(%rdi)
movq %r12,24(%rdi)
movq %r13,32(%rdi)
movq %r14,40(%rdi)
movq %r15,48(%rdi)
movq %rsi,56(%rdi)
ret

第一步 pop %rsi 是非常精妙的一句汇编,下面解释为何

当cpu call save_context 的时候 ,把save_context 函数的下一条指令入栈了,可以理解就是把当前执行流保存在栈顶了,那么pop %rsi 就是把执行流保存到 rsi寄存器并且把rsp 恢复到call save_context 之前的样子。

第二步,是把返回值设置为0

第三步,保存rbx到 jmp_buf , 这里解释一下 (%rdi) 就是jmp_buf 的地址

第四步,把call save_context 之前的rsp保存到jmp_buf ,注意我们的目的是保存 call save_context 之前的栈顶指针,因为调用 save_context 会破坏栈指针的值,之前的pop %rsi其实就是恢复 rsp的值到 call save_context 之前。

第五步,因为现在其实还是在 save_context 函数内部,并且我们已经成功拿到call save_context 之前的栈顶指针,所以现在恢复call save_context 后的栈结构,于是把 save_context 的下一条指令恢复到栈顶(push %rsi)

第六步到第十步,保存rbp,r12,r13,r14,r15

第七步,这一步是把 save_context 函数的下一条指令 也存入 jmp_buf

save_context 永远返回 零?

对,save_context 确实永远返回0 ,起码从汇编码上看是这样的。但是我们的代码中还是出现了这样的东西

1
2
3
4
if (save_context(_jmpbuf) != 0)
{
ScheduleObj::Instance()->ScheduleStartRun(); // 直接调用 this->run?
} //micro_thread.cpp : Thread::InitContext()

你可能会非常疑惑,save_context 不是永远返回0 吗,怎么会让它去判断 != 0。但是从汇编的层面来看,一定是 save_context 执行完后,然后才去比较rax的值和0 ,那如果是从其他地方直接跳转到 save_context 的下一句,那么rax就有可能不是0 ,所以 我们的cpp代码中就是通过 if (save_context(_jmpbuf) != 0) 来判断当前执行流是从其他微线程跳转过来的还是 save_context 一路执行下来的。这一点在 restore_context 的汇编码中可以得到证实

restore_context

这个函数在整个 源码中,只在 Thread::RestoreContext() 中被调用,且被调用的方式是 restore_context(_jmpbuf, 1); 记住这个入参,它非常重要! 下面解析源码

1
2
3
4
5
6
7
8
9
10
restore_context:
movl %esi,%eax
movq (%rdi),%rbx
movq 8(%rdi),%rsp
movq 16(%rdi),%rbp
movq 24(%rdi),%r12
movq 32(%rdi),%r13
movq 40(%rdi),%r14
movq 48(%rdi),%r15
jmp *56(%rdi)

这个函数第一条指令就印证了我在 “save_context 永远返回零” 那一节中做出的结论。movl %esi,%eax 把第二个参数的值赋值给 eax,按照 restore_context 的调用入参,那eax 就是1 了。而 restore_context 最后一条指令是跳转到 _jmpbuf 保存的save_context 的下一条指令的地址。然后中间的一些指令,就是把 _jmpbuf 保存的指令陆续恢复到 cpu 的寄存器里面