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的参数应该用指针传递
- 64 位下,函数从左到右的参数分布存入 rdi rsi rdx
- 参数存储到寄存器后 call Foo ,我们需要知道call 指令发生了什么,call指令会把 下一条指令的地址 push到 stack上,并且rsp 自动减 8,然后Foo的地址会被赋值给pc寄存器,即接下来cpu将从Foo处开始执行
- 函数的返回值保存在rax里面
函数签名
1 | extern "C" int save_context(jmp_buf jbf); |
save_context
1 | save_context: |
第一步 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 | if (save_context(_jmpbuf) != 0) |
你可能会非常疑惑,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 | restore_context: |
这个函数第一条指令就印证了我在 “save_context 永远返回零” 那一节中做出的结论。movl %esi,%eax 把第二个参数的值赋值给 eax,按照 restore_context 的调用入参,那eax 就是1 了。而 restore_context 最后一条指令是跳转到 _jmpbuf 保存的save_context 的下一条指令的地址。然后中间的一些指令,就是把 _jmpbuf 保存的指令陆续恢复到 cpu 的寄存器里面