咱们先弄清如何进行协程的切换,程序能够在某个地方挂起,跳转到另外的流程中执行,而且能够从新在挂起处继续运行。那如何实现呢?app
咱们先来看一个例子,有下面2个函数,若是在一个单线程中让输出结果依次是 funcA1 funcB1 funcA2 funcB2 ... ,你会怎么作呢?函数
void funcA(){ int i = 0; while(true){ //to do something printf("funcA%d ",i); i++; } } void funcB(){ int i = 0; while(true){ //to do something printf("funcB%d ",i); i++; } }
若是从c代码的角度来看,若是单线程运行到func1 的 while循环中,如何能调用到func2的while循环呢?必须使用跳转。spa
首先想到是goto。goto是能够实现跳转,可是goto不能实现函数间的跳转。没法知足这个要求。即便能够实现函数间跳转,难道就可行吗?
那这里不得不说下C函数调用过程.net
具体相见这篇文章https://blog.csdn.net/jelly_9/article/details/53239718 子程序或者称为函数,在全部语言中都是层级调用,好比A调用B,B在执行过程当中又调用了C, C执行完毕返回,B执行完毕返回,最后是A执行完毕。 因此子程序调用是经过栈实现的,子程序调用老是一个入口,一次返回,调用顺序是明确的。
程序运行有2个部分,指令,数据。栈保存的数据,指令经过寄存器(rip)控制。线程
2个函数内部的跳转必须保证栈是正确,因此跳转以前须要保存好当前的栈信息,而后跳转。 另外咱们能够获得另外一个信息,在一个栈上实现多个流程直接的跳转是不能实现的。 因此须要多个栈来维护。
那咱们来看jump_fcontext是怎么实现跳转指针
c语言函数声明 int jump_fcontext(fcontext_t *ofc, fcontext_t nfc, void* vp, bool preserve_fpu); 汇编代码以下
.text .globl jump_fcontext .type jump_fcontext,@function .align 16 jump_fcontext: pushq %rbp /* save RBP */ pushq %rbx /* save RBX */ pushq %r15 /* save R15 */ pushq %r14 /* save R14 */ pushq %r13 /* save R13 */ pushq %r12 /* save R12 */ /* prepare stack for FPU */ leaq -0x8(%rsp), %rsp /* test for flag preserve_fpu */ cmp $0, %rcx je 1f /* save MMX control- and status-word */ stmxcsr (%rsp) /* save x87 control-word */ fnstcw 0x4(%rsp) 1: /* store RSP (pointing to context-data) in RDI */ movq %rsp, (%rdi) /* restore RSP (pointing to context-data) from RSI */ movq %rsi, %rsp /* test for flag preserve_fpu */ cmp $0, %rcx je 2f /* restore MMX control- and status-word */ ldmxcsr (%rsp) /* restore x87 control-word */ fldcw 0x4(%rsp) 2: /* prepare stack for FPU */ leaq 0x8(%rsp), %rsp popq %r12 /* restrore R12 */ popq %r13 /* restrore R13 */ popq %r14 /* restrore R14 */ popq %r15 /* restrore R15 */ popq %rbx /* restrore RBX */ popq %rbp /* restrore RBP */ /* restore return-address */ popq %r8 /* use third arg as return-value after jump */ movq %rdx, %rax /* use third arg as first arg in context function */ movq %rdx, %rdi /* indirect jump to context */ jmp *%r8 .size jump_fcontext,.-jump_fcontext /* Mark that we don't need executable stack. */ .section .note.GNU-stack,"",%progbits
寄存器的用途能够先了解下https://www.jianshu.com/p/571...rest
一、保存寄存器code
pushq %rbp /* save RBP */ pushq %rbx /* save RBX */ pushq %r15 /* save R15 */ pushq %r14 /* save R14 */ pushq %r13 /* save R13 */ pushq %r12 /* save R12 */ “被调函数有义务保证 rbp rbx r12~r15 这几个寄存器的值在进出函数先后一致” rbx 是基址寄存器 做用存放存储区的起始地址 被调用者保存 rbp (base pointer)基址指针寄存器,用于提供堆栈内某个单元的偏移地址,与rss段寄存器联用, 能够访问堆栈中的任一个存储单元,被调用者保存
二、预留fpu 8个字节空间视频
/* prepare stack for FPU */ leaq -0x8(%rsp), %rsp 表示 %rsp中的内容减8。因为栈是从高到底,此处的意思表示预留8字节的栈空间。 FPU:(Float Point Unit,浮点运算单元)
三、判断是否保存fpu协程
cmp $0, %rcx je 1f rcx是第四个参数,判断是否等于0。若是为0,跳转到1标示的位置。也就是preserve_fpu 。 当preserve_fpu = true 的时候,须要执行2个指令是将浮点型运算的2个32位寄存器数据保存到第2步中预留的8字节空间。 /* save MMX control- and status-word */ stmxcsr (%rsp) /* save x87 control-word */ fnstcw 0x4(%rsp)
四、修改rsp 此时已经改变到其余栈
将rsp 保存到第一参数(第一个参数保存在rdi)指向的内存。fcontext_t *ofc 第一参数ofc指向的内存中保存是 rsp 的指针。第二条指令,实现了将第二个参数复制到 rsp.
1: /* store RSP (pointing to context-data) in RDI */ movq %rsp, (%rdi) /* restore RSP (pointing to context-data) from RSI */ movq %rsi, %rsp
五、判断是否保存了fpu,若是保存了就恢复保存在nfx 栈上的 fpu相关数据到响应的寄存器。
/* test for flag preserve_fpu */ cmp $0, %rcx je 2f /* restore MMX control- and status-word */ ldmxcsr (%rsp) /* restore x87 control-word */ fldcw 0x4(%rsp)
六、将rsp 存储的地址+8(8字节fpu),按顺序将栈中数据恢复到寄存器中。
2: /* prepare stack for FPU */ leaq 0x8(%rsp), %rsp popq %r12 /* restrore R12 */ popq %r13 /* restrore R13 */ popq %r14 /* restrore R14 */ popq %r15 /* restrore R15 */ popq %rbx /* restrore RBX */ popq %rbp /* restrore RBP */
七、设置返回值,实现指令跳转。
接下来继续pop 数据,那栈上存的是什么呢,在c函数调用文章中能够知道,call的时候会保存rip(指令寄存器)到栈。因此此时POP的数据是rip 也就是下一条指令。这是下一条指令是nfx 栈保存的,因此这是另外一个协程的下一条指令。保存到 r8。最后跳转下一条指令就恢复到另外一个协程运行 jmp *%r8。
movq %rdx, %rax 是将上一个协程A jump_fcontext第三个参数做为当前协程B jump_fcontext 的返回值,能够实现2个协程直接的数据传递。
movq %rdx, %rdi 若是跳转过去的新的协程,将第三个参数做为协程B 启动入口void func(int param)的第一参数。
/* restore return-address */ popq %r8 /* use third arg as return-value after jump */ movq %rdx, %rax /* use third arg as first arg in context function */ movq %rdx, %rdi /* indirect jump to context */ jmp *%r8
了解了程序是如何跳转后,我门在看下如何建立一个协程栈呢。make_fcontext
c语言函数声明 fcontext_t make_fcontext(void sp, size_t size, void (fn)(int));
.text .globl make_fcontext .type make_fcontext,@function .align 16 make_fcontext: /* first arg of make_fcontext() == top of context-stack */ movq %rdi, %rax /* shift address in RAX to lower 16 byte boundary */ andq $-16, %rax /* reserve space for context-data on context-stack */ /* size for fc_mxcsr .. RIP + return-address for context-function */ /* on context-function entry: (RSP -0x8) % 16 == 0 */ leaq -0x48(%rax), %rax /* third arg of make_fcontext() == address of context-function */ movq %rdx, 0x38(%rax) /* save MMX control- and status-word */ stmxcsr (%rax) /* save x87 control-word */ fnstcw 0x4(%rax) /* compute abs address of label finish */ leaq finish(%rip), %rcx /* save address of finish as return-address for context-function */ /* will be entered after context-function returns */ movq %rcx, 0x40(%rax) ret /* return pointer to context-data */ finish: /* exit code is zero */ xorq %rdi, %rdi /* exit application */ call _exit@PLT hlt .size make_fcontext,.-make_fcontext /* Mark that we don't need executable stack. */ .section .note.GNU-stack,"",%progbits
一、第一个参数是程序申请的内存地址高位(栈是从高到低),将第一个参数放到rax,将地址取16的整数倍。
andq $-16, %rax 表示低4位取0。 -16 的补码表示为0xfffffffff0.
/* first arg of make_fcontext() == top of context-stack */ movq %rdi, %rax /* shift address in RAX to lower 16 byte boundary */ andq $-16, %rax
二、预留72字节栈空间,将第3个参数(void (*fn)(int)函数指针)保存在当前偏移0x38位置(大小8字节)。
/* reserve space for context-data on context-stack */ /* size for fc_mxcsr .. RIP + return-address for context-function */ /* on context-function entry: (RSP -0x8) % 16 == 0 */ leaq -0x48(%rax), %rax /* third arg of make_fcontext() == address of context-function */ movq %rdx, 0x38(%rax)
三、保存fpu 和jump_fcontext 相似总大小8字节。
/* save MMX control- and status-word */ stmxcsr (%rax) /* save x87 control-word */ fnstcw 0x4(%rax)
四、计算finish的绝对地址,保存到栈的0x40位置。
leaq finish(%rip), %rcx 表示finish是相对位置+rip 就是finish的函数的地址。
/* compute abs address of label finish */ leaq finish(%rip), %rcx /* save address of finish as return-address for context-function */ /* will be entered after context-function returns */ movq %rcx, 0x40(%rax)
五、返回,rax 做为返回值,目前的指向能够当作新栈的栈顶,至关于rsp
ret /* return pointer to context-data */
咱们回头在看看为何会预留72字节大小。首先知道jump_fcontext 在新栈须要 pop 的大小为,fpu(8字节)+ rbp rbx r12 ~ r15 (8*6 = 48 字节) = 56 字节。还会继续POP rip 8 字节,因此能够看到第二步中 movq %rdx, 0x38(%rax),就是将rip 保存到这个位置。
目前已经64字节了,栈还有存储什么呢,协程(fn 函数)运行完成后会退出调用ret,其实就是POP到 rip.因此保存是finish 函数指针 大小8字节。总共 72 字节。
make_fcontext 建立协程的栈。jump_fcontext实现跳转。
网校内部培训视频by李乐 https://biglive.xueersi.com/L...