

SRS是一个单进程多协程的服务器,保持高并发同时还能利用ST协程避免异步回调的问题,这也导致新的平台需要移植ST,而且是汇编代码。
其实,移植ST比想象的要简单很多,最关键的就是实现setjmp/longjmp,也就是保存寄存器和恢复寄存器,所以步骤如下:
1.分析你的平台的寄存器使用,也就是函数调用规范。一般是由系统(Linux/OSX/Windows)和CPU(x86/ARM/MIPS)决定的。有个小工具打印这些信息,参考porting.c[1]。2.使用汇编实现寄存器的保存和恢复,不同系统的汇编语法有差异,目前几个系统都已经实现,可以参考现有的汇编实现。有个小工具调用汇编的函数,显示jmpbuf信息,参考verify.c[2]。3.实现ST的Makefile的适配,编译并在新的平台验证ST。有个小工具验证ST是否正常工作,参考helloworld.c[3]。
目前已经实现的OS和CPU如下:
OS | CPU | Status | Command | Description |
|---|---|---|---|---|
Linux | x86-64 | Stable | make linux-debug | For CentOS,Ubuntu server, etc. |
Linux | arm | Stable | make linux-debug | For ARM(v7) device, #1[4] |
Linux | aarch64 | Stable | make linux-debug | For ARM(v8) server, #9[5] |
Linux | mips | Dev | make linux-debug | For OpenWRT device, #21[6] |
OSX | x86-64 | Stable | make darwin-debug | For OSX(MacPro, etc.) #11[7] |
Windows | x86-64 | Dev | make cygwin64-debug | For Windows(x64) desktop, #20[8] |
Note: 早期ST直接使用setjmp,然后修改jmpbuf的SP寄存器内容,这依赖于知道glibc如何使用jmpbuf的布局,而后来glibc改变了(加密了)布局所以就出现很多平台无法使用。其实全部使用汇编实现,移植性会更好,因为要支持的系统和CPU有限,寄存器的布局是确定的,资料也很好找。
编译ST需要明确指定OS,比如:
•Linux: make linux-debug•OSX: make darwin-debug•Windows: make cygwin64-debug
不同的OS的依赖的文件可能不同,如果需要支持其他OS则需要修改Makefile[9]。
Note: 如果你的系统的规范和现有的一样,就可以尝试用现有的OS,比如Unix一般可以指定为Linux或OSX。
不同CPU的寄存器布局不同,比如Linux下支持多种CPU,一般可以通过宏定义检测到,所以一般都使用如下命令编译:
make linux-debug如果发现报错Unknown CPU architecture,那么可以明确指定你的CPU体系:
•x86-64: make linux-debug EXTRA_CFLAGS="-D__x86_64__"•arm: make linux-debug EXTRA_CFLAGS="-D__x86_64__"•aarch64: make linux-debug EXTRA_CFLAGS="-D__aarch64__"•mips: make linux-debug EXTRA_CFLAGS="-D__mips__"
如果你的CPU不属于已经适配的CPU,就需要适配,也并不难。参考下面的步骤。
以MIPS为例,我们找下MIPS Calling Conventions[10],可以看到Callee主要保存以下寄存器:
•$gp global pointer•$fp frame pointer•$sp stack pointer•$s0–$s7 saved temporaries
我们修改porting.c[11],增加MIPS下的print_jmpbuf,并在OpenWRT上执行,可以看到setjmp还是明文并没有混淆:
root@OpenWrt:~# ./porting OS specs:
__linux__: 1
CPU specs:
__mips__: 1, __mips:32, __mips_isa_rev:2, _MIPSEL:1
Compiler specs:
sizeof(long)=4
sizeof(long long int)=8
sizeof(void*)=4
sizeof(__ptr_t)=4
Calling conventions:
ra=0x400818, sp=0x7f968898, s0=0x7f968a8c, s1=0x1, s2=0x7f968a84, s3=0x4006b0, s4=0x77e029d0,
s5=0x77e01660, s6=0x77e14c38, s7=0, fp=0x7f968898, gp=0x419000
sizeof(jmp_buf)=104 (unsigned long long [13])
0x18 0x08 0x40 0x00 # ra, the return address
0x98 0x88 0x96 0x7f # sp
0x8c 0x8a 0x96 0x7f # s0
0x01 0x00 0x00 0x00 # s1
0x84 0x8a 0x96 0x7f # s2
0xb0 0x06 0x40 0x00 # s3
0xd0 0x29 0xe0 0x77 # s4
0x60 0x16 0xe0 0x77 # s5
0x38 0x4c 0xe1 0x77 # s6
0x00 0x00 0x00 0x00 # s7
0x98 0x88 0x96 0x7f # fp/s8
0x00 0x90 0x41 0x00 # gp
0x00 0x00 0x00 0x00
............
0x00 0x00 0x00 0x00Note: 最简单的办法,就是将jmpbuf[1],直接设置为
_sp也就是协程从堆上开辟的堆栈地址,但这样依赖于glibc的布局,我们还是选择使用汇编实现,自己定义jmpbuf如何使用,不给以后挖坑了。
可以调试下setjmp,可以看到它保存的寄存器:
sw ra,0(a0)
sw sp,4(a0)
sw s0,8(a0)
sw s1,12(a0)
sw s2,16(a0)
sw s3,20(a0)
sw s4,24(a0)
sw s5,28(a0)
sw s6,32(a0)
sw s7,36(a0)
sw s8,40(a0)
sw gp,44(a0)
jr ra同样的,可以看下longmp,可以发现恢复寄存器后,就是直接跳转到ra的地址:
lw ra,0(a0)
lw sp,4(a0)
......
jr raNote: 只是用这种方式确认下使用的寄存器,我们并不需要严格按照glibc的方式布局jmpbuf,因为各种版本的glibc实现都不相同,我们使用汇编实现所有平台的setjmp时,可以让布局尽量一致。
接下来就是关键的用汇编实现寄存器保存,根据OS的不同,分成了不同的汇编文件:
•md_linux.S,所有Linux平台的汇编,根据CPU架构(宏)实现不同平台的函数。•md_darwin.S,针对OSX/Mac的汇编,目前实现了x86_64架构,还没支持M1(ARM)。•md_cygwin64.S,针对Cygwin64/Windows的汇编,目前实现了x86_64架构,还没有支持32位Windows。
显然OpenWRT/MIPS是Linux平台,所以我们先实现两个空函数:
#elif defined(__mips__)
#define JB_SP 0
.text
.globl _st_md_cxt_save
_st_md_cxt_save:
.size _st_md_cxt_save, .-_st_md_cxt_save
.globl _st_md_cxt_restore
_st_md_cxt_restore:
.size _st_md_cxt_restore, .-_st_md_cxt_restore
#endifNote: 实际上,
_st_md_cxt_save就是setjmp,而_st_md_cxt_restore就是longjmp。
然后我们编译ST,用verify.c[12]验证这两个函数是否正常工作。
cd tools/verify && make && ./verify
root@OpenWrt:~# ./verify
gp=0x419000, fp=0x7fe3af20, sp=0x7fe3af20, s0=0x7fe3b10c, s1=0x1, s2=0x7fe3b104, s3=0x400670,
s4=0x77e759d0, s5=0x77e74660, s6=0x77e87c38, s7=0
0x00 0x00 0x00 0x00
............
0x00 0x00 0x00 0x00Note: 由于没有实现,所以jmpbuf都是空的。
最后,就是用汇编实现函数,需要找下平台相关的资料(也可以直接通过调试setjmp和longjmp的实现,来学习如何将寄存器保存到jmpbuf,以及如何从jmpbuf恢复),详细参考 #21[13]。
实现汇编后,有些地方需要修改,比如MIPS的jmpbuf定义不太一样。
一般的jmpbuf定义如下,字段名是__jmpbuf:
typedef struct __jmp_buf_tag jmp_buf[1];
struct __jmp_buf_tag {
__jmp_buf __jmpbuf;
int __mask_was_saved;
__sigset_t __saved_mask;
};而在MIPS中定义的字段不同,它的字段名是__jb:
typedef struct __jmp_buf_tag {
__jmp_buf __jb;
unsigned long __fl;
unsigned long __ss[128/sizeof(long)];
} jmp_buf[1];因此,需要我们在md.h中定义如何使用jmpbuf,SP是在__jb[0]的位置:
#elif defined(__mips__)
/* https://github.com/ossrs/state-threads/issues/21 */
#define MD_USE_BUILTIN_SETJMP
#define MD_GET_SP(_t) *((long *)&((_t)->context[0].__jb[0]))Note: 在MIPS中,指针是4字节的,而
__jb是long long类型8字节的,所以需要转换类型。
其中,宏定义MD_GET_SP,就是如何将jmpbuf的SP,更新为协程的栈地址。这是在MD_INIT_CONTEXT,也就是创建协程时调用的。
Note: 创建协程时,当时的SP可能是在另外一个协程,所以创建的协程并不能直接使用当前的SP,而需要从堆上重新申请虚拟的stack,所以在setjmp后需要更新jmpbuf中的SP地址。
编译成功后,我们使用一个小工具验证,会初始化ST后,不断打印日志,参考helloworld.c[14]。
root@OpenWrt:~# ./helloworld
#000, Hello, state-threads world!
#001, Hello, state-threads world!
#002, Hello, state-threads world!
#003, Hello, state-threads world!大功告成。
[1] porting.c: https://github.com/ossrs/state-threads/blob/srs/tools/porting/porting.c
[2] verify.c: https://github.com/ossrs/state-threads/blob/srs/tools/verify/verify.c
[3] helloworld.c: https://github.com/ossrs/state-threads/blob/srs/tools/helloworld/helloworld.c
[4] #1: https://github.com/ossrs/state-threads/issues/1
[5] #9: https://github.com/ossrs/state-threads/issues/9
[6] #21: https://github.com/ossrs/state-threads/issues/21
[7] #11: https://github.com/ossrs/state-threads/issues/11
[8] #20: https://github.com/ossrs/state-threads/issues/20
[9] Makefile: https://github.com/ossrs/state-threads/blob/srs/Makefile
[10] MIPS Calling Conventions: https://github.com/ossrs/state-threads/issues/21#issue-1012719185
[11] porting.c: https://github.com/ossrs/state-threads/blob/ed6c73030881daea8f4f25f8a55120d2d4c9c7c1/porting/porting.c#L46
[12] verify.c: https://github.com/ossrs/state-threads/blob/srs/tools/verify/verify.c
[13] #21: https://github.com/ossrs/state-threads/issues/21
[14] helloworld.c: https://github.com/ossrs/state-threads/blob/srs/tools/helloworld/helloworld.c