一种在C语言中用汇编指令和System V ucontext 支撑实现的协程切换
此文在看了 python 中基于 yield 和 yield from (同步基于 yield 的子生成器/协程)机制的生成器/协程后,就想在C语言中实现类似机制。
在阅读 System V ucontext 手册后,便选择基于他来支撑协程切换;此篇文字发布后,Windoze 和 AbbyCin 为 用汇编支持协程切换 提供了信息和时间上的信心,遂用汇编指令实现了对协程切换的最小支持。
由于很喜欢 ramonza/libcoro 中对 sp 指针的运用,所以此文在汇编程序中延用了该指针相关方式,并使用纯汇编语句为协程传递参数——从而避免了全局变量的使用;同时为汇编支撑的协程添加了协程结束函数。
所实现协程机制包括:
- yield
- yield from
- 基于 yield 和 yield from 机制协程的调度器 loop
这些机制皆在 ln_cs.c 中实现;支撑协程上下文和切换的代码在context目录下——经此文测试,汇编指令和ucontext在支撑协程切换时无明显的时间性能差距。experiences目录下为体验以上功能机制的例子(运行体验可参见 doc 目录下的文档)。
以下是源码目录树图(已略去工程管理文件)。
.
├── doc
│ ├── A coroutine ... in C-language.md
│ └── An optimization ... in C-language.md
├── experiences
│ ├── loop_e
│ │ ├── loop_e.c
│ ├── yield_e
│ │ └── yield_e.c
│ └── yield_from_e
│ └── yield_from_e.c
├── include
│ ├── ln_co.h
│ └── ln_comm.h
├── readme.md
└── src
├── ln_co.c
├── context
├── ln_context.h
├── assembly
│ ├── ln_asm.c
└── ucontext
├── ln_uc.c
核心逻辑代码 ln_cs.c 经过此文的一些技巧性修剪后仍不到 500 行,所以还是很想继续分享的——便补写了文档与源码一起上传到了 github 和 gitee之上。
另外,此文本着更好管理工程的理念——写了一大推工程管理文件。
对CPU执行程序原理有所理解的同学应该都能胜任对该程序的阅读理解,有兴趣或与此文具相同程序理念(从源码或文档中可以看出一些吧)的同学一起出出新招继续提升下该程序吧。如
[1] 提升汇编代码以支撑协程的 return 语句
/**
* ln_asm.c,
* coroutine context, switching implement
* by assembly instructions.
*
* lxr, 2020.02 */
#include "ln_context.h"
#include "ln_comm.h"
#include <stdlib.h>
void
_co_arg_medium(void);
/**
* because of the instructions on
* stack-frame would be automatically
* added by C-compiler in C-functions,
* those routines can't be
* inline-assembly in C-function.
*
* @caution: the routines on __i386
* not tested by me. */
__asm__ (
"\t.globl _co_arg_medium\n"
"_co_arg_medium:\n"
#if __i386
"popl %eax\n\t" // get co_fn
"popl %ecx\n\t" // get arg
"popl %edx\n\t" // get ci
/* call convention on arguments
of gcc on __i386 e.g.
void fn(int a, int b);
fn(1, 2);
passing argument by stack
in caller:
push 2 --> b
push 1 --> a */
"movl $0, %esi\n\t"
"pushl %esi\n\t"
"pushl %ecx\n\t"
"pushl %esi\n\t"
"pushl %edx\n\t"
#elif __amd64
"popq %rax\n\t" // get co_fn
"popq %rsi\n\t" // get arg
"popq %rdi\n\t" // get ci
/* call convention on arguments
of gcc on __amd64 e.g.
void fn(uint32_t a, uint32_t b,
uint32_t c, uint32_t e,
uint32_t f, uint32_t g,
uint32_t h);
passing arguments by registers
and stack in caller:
edi --> a, esi --> b, edx --> c,
ecx --> e, r8d --> f, r9d --> g,
push real h --> formal h */
"movl %esi, %edx\n\t"
"movq %rsi, %rcx\n\t"
"shrq $32, %rcx\n\t"
"movq %rdi, %rsi\n\t"
"shrq $32, %rsi\n\t"
"jmpq *%rax\n\t"
#endif
);
__asm__(
"\t.globl co_switch_asm\n"
"co_switch_asm:\n"
#if __i386
"pushl %ebp\n\t"
"pushl %ebx\n\t"
"pushl %esi\n\t"
"pushl %edi\n\t"
"movl %esp, (%eax)\n\t"
"movl (%edx), %esp\n\t"
"popl %edi\n\t"
"popl %esi\n\t"
"popl %ebx\n\t"
"popl %ebp\n\t"
"popl %ecx\n\t"
"jmpl *%ecx\n\t"
#elif __amd64
"pushq %rbp\n\t"
"pushq %rbx\n\t"
"pushq %r12\n\t"
"pushq %r13\n\t"
"pushq %r14\n\t"
"pushq %r15\n\t"
/* I want to save _CORET here
by instructions just like
'pushq _CORET'(etc.)to accept
coroutine return. unfortunately
fail.
so coroutines which use CCTX_ASM
to support coroutine-switching
must use co_end() to terminate
itself before return.
the same situation on __i386,
please do a favor to accept the
'return' statement of coroutine
if you owns the same faith. */
"movq %rsp, (%rdi)\n\t"
"movq (%rsi), %rsp\n\t"
"popq %r15\n\t"
"popq %r14\n\t"
"popq %r13\n\t"
"popq %r12\n\t"
"popq %rbx\n\t"
"popq %rbp\n\t"
"popq %rcx\n\t"
"jmpq *%rcx\n"
"_CORET:"
#else
#error "coroutine-context unsupported"
" on current architecture"
#endif
);
int
co_start_asm(ci_s *ci)
{
cctx_s *ctx = NULL;
int ss = co_ssize(ci);
char *stack = co_stack(ci);
IF_EXPS_THEN_RETURN(!ci || !stack || !ss,
CODE_BADPARAM);
ctx = co_cctx(ci);
ctx->sp = (void **)(stack + ss);
/* ctx->sp points to (void *),
so arithmetic unit of
ctx->sp is sizeof(void *).
initial co-stack as follow:
------+---------+------+----+----+----+
... |co_medium| co_fn| arg| ci |NULL|
------+---------+------+----+----+----+
^ ^ ^
| | |
stack sp stack+ss */
*--ctx->sp = NULL;
*--ctx->sp = ci;
*--ctx->sp = co_arg(ci);
*--ctx->sp = co_cofn(ci);
*--ctx->sp = _co_arg_medium;
/* Reserved for subroutines to
backup registers: ebp, ebx,
esi, edi on i386; rbp, rbx,
r12, r13, r14, r15 on amd64.*/
ctx->sp -= CS_RESERVE_NR;
(void)co_switch_asm(co_bcctx(ci), ctx);
return CODE_NONE;
}
[2] 修整有瑕疵的工程管理文件及管理理念本身。
[3...]