一种在C语言中用 System V ucontext 实现的协程切换

Posted 资质平庸的程序员

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了一种在C语言中用 System V ucontext 实现的协程切换相关的知识,希望对你有一定的参考价值。

A coroutine switching implement by System V ucontext in C-language
看了python中基于yield/yield from的轻量级协程,用ucontex在C中也实现了一个。这是碚大在很早时就寄送过来的礼物,我很激动。回顾二〇一九,除了在贫穷方面的相关细节外,其余还算不错。

1 python 协程原理

尝试了解上篇文字“一种在 python 中用 asyncio 和协程实现的IO并发”中协程的基本原理。python 协程可基于其生成器实现,其生成器由 yield 和 yield from1 标识。

1.1 yield

在实例化对象时,python 将包含 yield 语句的函数实例化为生成器。在生成器中,每通过 send() 运行到 yield 时返回,再次通过 send() 运行时从 yield 返回处继续运行。

>>> def fun():
...     yield 0
...     yield 1
...
>>> gen = fun()
>>> print(type(gen))
<class 'generator'>
>>> gen.send(None)
0
>>> gen.send(None)
1

通过生成器字节码进一步理解生成器执行过程。

>>> import dis
>>> def fun():
...     yield 0
...     yield 1
...
>>> gen = fun()
>>> dis.dis(gen)
  2      0 LOAD_CONST        1 (0)
         2 YIELD_VALUE
         4 POP_TOP

  3      6 LOAD_CONST        2 (1)
         8 YIELD_VALUE
        10 POP_TOP
        12 LOAD_CONST        0 (None)
        14 RETURN_VALUE
>>> gen=fun()
>>> gen.gi_frame.f_lasti
-1
>>> gen.send(None)
0
>>> gen.gi_frame.f_lasti
2
>>> gen.send(None)
1
>>> gen.gi_frame.f_lasti
8

python 执行 gen=fun() 语句时,将 gen 实例化为生成器。

python 在堆上为 gen 复制一份函数 fun() 的字节码,同时在堆上为 gen 生成一份维护 fun() 字节码运行的信息,包括记录 gen 运行位置的成员 gi_frame.f_lasti。

每通过 gen.send(None) 执行其(gen)堆上 fun() 的字节码时,即从 gen.gi_frame.f_lasti(-1表未开始或已结束) 位置处执行。

在执行到 yield 语句时返回,并将其堆上 fun() 字节码的当前运行位置更新到 gen.gi_frame.f_lasti 中供下次运行,直到 gen 堆上的函数 fun() 运行结束。

1.2 yield from

yield from 可用于等待一个生成器运行结束。

>>> import dis
>>> def fun():
...     yield 0
...     yield 1
...
>>> def f_fun():
...     gen = fun()
...     yield from gen
...     print('gen done')
...
>>> gen_f = f_fun()
>>> gen_f.gi_frame.f_lasti
-1
>>> dis.dis(gen_f)
  2      0 LOAD_GLOBAL              0 (fun)
         2 CALL_FUNCTION            0
         4 STORE_FAST               0 (gen)

  3      6 LOAD_FAST                0 (gen)
         8 GET_YIELD_FROM_ITER
        10 LOAD_CONST               0 (None)
        12 YIELD_FROM
        14 POP_TOP

  4     16 LOAD_GLOBAL              1 (print)
        18 LOAD_CONST               1 ('gen done')
        20 CALL_FUNCTION            1
        22 POP_TOP
        24 LOAD_CONST               0 (None)
        26 RETURN_VALUE
>>>
>>> gen_f.send(None)
0
>>> gen_f.gi_frame.f_lasti
10
>>>
>>> gen_f.send(None)
1
>>> gen_f.gi_frame.f_lasti
10
>>> gen_f.send(None)
gen done
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

f_fun() 中的 yield from 让 gen_f 被 python 实例化为生成器。

gen_f 与 yield 标识生成的生成器 gen 不同的是——在 gen_f 所等待生成器 gen 运行结束前, gen_f 每次都从 yield from 语句处返回。

直到 gen 运行结束,gen_f yield from 后续语句才会被执行。基于 yield 和 yield from 机制可实现协程并发。

1.3 基于 yield && yield form 机制写个简单的协程并发

了解 yield 和 yield from 后,用他们实现一个简单的协程并发例子吧。

#!/usr/bin/python3
# -*- coding: utf-8 -*-

''' ln_crtn_eg.py '''
import sys

_nr = 0

def fn_decorator(fn):
    def _w(max):
        global _nr
        for i in range(max):
            _nr += 1
            yield f'increase in fn.__name__: _nr'
    return _w

def f_fn_decorator(fn_x):
    def _w(max):
        gen = fn_x(max)
        yield from gen
        print(f'increase in fn_x.__name__ done')
    return _w
    
@fn_decorator
def fn_m(max):
    pass
    
@fn_decorator
def fn_n(max):
    pass

@f_fn_decorator
def f_fn_m(max):
    return fn_m(max)
    
@f_fn_decorator
def f_fn_n(max):
    return fn_n(max)
    
if __name__ == '__main__':
    max = 3
    gen = [f_fn_m(max), f_fn_n(max)]
    for i in range(max + 1):
        try: print(gen[0].send(None))
        except StopIteration: pass
        
        try: print(gen[1].send(None))
        except StopIteration: pass

例子运行体验。

> python ln_crtn_eg.py
increase in fn_m: 1
increase in fn_n: 2
increase in fn_m: 3
increase in fn_n: 4
increase in fn_m: 5
increase in fn_n: 6
increase in f_fn_m done
increase in f_fn_n done

在 ln_crtn_eg.py 中可看到:协程在编程语言语句层面切换,变量的共享不用加锁;协程并发可发生在单线程中,适用于速度不如CPU快的异步IO并发场景2

这种轻量级编程技术在小并发场景下优雅可爱,在大并发场景下较进程和线程而言能突破资源瓶颈。实在令人忍不住想在C语言中实现一个。

2 开始实现标题内容

此文先不从头开始,先看看有没有描述协程上下文的现成库,最好是C库。还真有—— System V3 的 ucontext。

2.1 先读 ucontext 手册,看其能完成编程目标否

#include <ucontext.h>

int getcontext(ucontext_t *ucp);
int setcontext(const ucontext_t *ucp);

void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
int  swapcontext(ucontext_t *oucp, const ucontext_t *ucp);

描述协程上下文的结构体类型 ucontext_t 至少包含以下成员。

typedef struct ucontext_t 
    struct ucontext_t *uc_link;
    sigset_t          uc_sigmask;
    stack_t           uc_stack;
    mcontext_t        uc_mcontext;
    ...
 ucontext_t;

uc_link,由 makecontext() 创建的协程上下文对应协程运行结束后,程序切换到 uc_link 所指协程上下文处运行(为NULL时则整个线程退出)。

uc_sigmask,用于记录 在当前协程上下文中 所需屏蔽的信号。
uc_stack,指向当前协程运行所需内存空间,以栈的方式使用。
uc_mcontext,真正描述协程上下文的结构体类型,主要用于保存当前CPU各寄存器的状态值。

int getcontext(ucontext_t *ucp);
getcontext() 将程序当前协程级上下文4保存到 ucp 指向的类型为 ucontext_t 的结构体中。

getcontext() 执行成功返回0;执行失败时将错误码保存在 errno 变量中并返回-1。

int setcontext(const ucontext_t *ucp);
setcontext() 跳转 ucp 所指协程上下文处执行。
setcontext() 执行成功不返回;执行失败时将错误码保存在 errno 变量中并返回-1。

若 setcontext() 中 ucp 所指协程上下文由 getcontext() 创建,当 ucp 对应协程终止后将继续执行该协程后续程序。若 setcontext() 中 ucp 所指协程上下文由 makecontext() 创建,当 ucp 对应协程终止时会切换到 ucp->uc_link 所指协程上下文处运行;若 uc_link 为NULL,则当前线程退出。

void makecontext(ucontext_t *ucp, void (*func)(), int argc, …);
makecontext() 用 func 地址处的协程上下文修改由 getcontext() 在 ucp 所指结构体中创建的协程上下文。

在调用 makecontext() 之前,必须为 ucp->uc_stack 分配内存用作协程运行栈,并为 ucp->uc_link 指定 ucp 对应协程运行结束后 将切换运行协程的协程上下文。makecontext() 支持向 func 地址(函数)传递 argc 个 int 类型参数。通过调用 setcontext() 或 swapcontext() 即可跳转执行 func,且在 func 处可获取指定的 argc 个 int 类型实参。

int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);
swapcontext() 将当前协程上下文保存在 oucp 所指结构体中,并跳转执行 ucp 所指协程上下文处。

swapcontext() 执行成功时暂不返回(后续由 oucp 成功切换回来时,该函数会返回0);执行失败时将错误码保存在 errno 变量中随后返回-1。errno 为 ENOMEM时 表明所设置栈内存已不足。

通过阅读 ucontext 手册,用 ucontext_t 及相关一族函数实现一个在C环境下的类似于 yield && yield from 机制的协程切换应该不成问题。

在继续开展剩余工作之前,先写个简单例子跟 ucontext 打个照面。

#include <stdio.h>
#include <unistd.h>
#include <ucontext.h>

#ifdef __GNUC__
#define IF_EXPS_TRUE_THEN_RETURN(exps, retv) \\
( \\
    if (exps) return retv; \\
)

#else
#define IF_EXPS_TRUE_THEN_RETURN(exps, retv) \\
do  \\
    if (exps) return retv; \\
 while(0)

#endif

int main(void)

#define _SYSCALL_ERR (-1)

    int retv;
    ucontext_t cctx;

    retv = getcontext(&cctx);
    IF_EXPS_TRUE_THEN_RETURN(_SYSCALL_ERR == retv, retv);

    fprintf(stderr, "coroutine context comes from %s\\n",
        retv++ ? "main()" : "ucontext");

    usleep(500 * 1000);
    (void)setcontext(&cctx);

    return 0;
#undef _SYSCALL_ERR

例子运行体验。

$ gcc ln_crtn_ctx.c -o cctx
$ ./cctx
coroutine context comes from ucontext
coroutine context comes from ucontext
coroutine context comes from ucontext
...

2.2 初定管理协程切换的数据结构体

#include <ucontext.h>

typedef struct coroutine_info_s    ci_s;
typedef struct coroutine_control_s cc_s;

/* the type of coroutine function */
typedef void (*cfn_f)(cc_s *cc, void *arg);

/* type flag for memory-managing */
typedef enum _mm_e 
    MM_STATIC,
    MM_HEAP_UFREE,

    MM_MAX
 mm_e;

typedef struct coroutine_return_value_s 
    void *buf; /* point to the return-value buffer */
    int  blen; /* return-value buffer length */
    char bflag; /* see mm_e */
 crv_s;

typedef struct coroutine_info_s 
    cfn_f cfn;  /* coroutine function */
    void *arg;  /* coroutine function arguments */
    char *cname;/* coroutine function name/id */
    int state;  /* coroutine current states */
    
    char *stack; /* memory for coroutine stack */
    int   size;  /* stack size */
    ucontext_t cctx; /* current coroutine ucontext */

    /* on the memory bears coroutine's return-value */
    crv_s rv; 

    /* switch to the coroutine corresponded by "back" 
       when current coroutine switching or terminated. */
    ci_s *back;

    /* the cc which current ci belongs to */
    cc_s *cc;
 ci_s;

typedef struct coroutine_control_s 
    ci_s *ci;     /* point to the (ci_s) arrary */
    int running;  /* the index of running coroutine */
    int nr, left; /* coroutines total/left number ci point to */
    cc_s *next;   /* next coroutine control unit */
 cc_s;

这么多!看来是真要打算实现呐。

2.3 将数据结构体的作用转换为C代码

此篇文字打算按照以下方式利用管理协程切换的数据结构体。

  | <--- ci unit ---> |  | <--- ci unit ---> |
  +------+-----+------+  +------+-----+------+
  | ci_s | ... | ci_s |  | ci_s | ... | ci_s | ...
  +------+-----+------+  +------+-----+------+
  ^                      ^
+-|--+-----+------+    +-|--+-----+------+
| ci | ... | next | -> | ci | ... | next |  ...
+----+-----+------+    +----+-----+------+
cc                     cc

以单元量(unit)分配和释放数据结构体的原因是为了减少在协程切换过程中频繁通过系统调用分配内存,以提升协程切换效率。
相关接口如下。

static bool inline 
_put_unit_ci(ci_s *ci, int nr)

    int i;
    for (i = 0; i < nr; ++i)
        free(ci[i].stack);
    free(ci);
    
    return true;


static bool inline 
_put_cc(cc_s *cc)

    (void)_put_unit_ci(cc->ci, cc->nr);
    free(cc);

    return true;


static ci_s * 
_get_unit_ci(int nr, int cmmb)

    int i;
    ci_s *ci = NULL;
    char *stack = NULL;

    ci = (ci_s *)calloc(sizeof(ci_s) * nr, 1);
    IF_EXPS_TRUE_THEN_RETURN(!ci, NULL);

#define _STACK_MIN (1024 * 8)
    cmmb = (cmmb > _STACK_MIN) ? cmmb : _STACK_MIN;
#undef _STACK_MIN
    for (i = 0; i < nr; ++i) 
        stack = (char *)calloc(cmmb, 1);
        IF_EXPS_TRUE_THEN_RETURN(!stack && _put_unit_ci(ci, i), NULL);
        ci[i].stack = stack;
        ci[i].size  = cmmb;
    
    
    return ci;


static cc_s * 
_get_cc(int nr, int cmmb)

    ci_s *ci = NULL;
    cc_s *cc = NULL;

    cc = (cc_s *)calloc(sizeof(cc_s), 1);
    IF_EXPS_TRUE_THEN_RETURN(!cc, NULL);

    ci = _get_unit_ci(nr, cmmb);
    IF_EXPS_TRUE_THEN_RETURN(!ci && ln_free(cc), NULL);
#define _SET_CC_MBR(mbr, v) cc->mbr = v
    _SET_CC_MBR(ci, ci);
    _SET_CC_MBR(running, NONE);
    _SET_CC_MBR(nr, nr);
    _SET_CC_MBR(left, nr);
#undef _SET_CC_MBR

    return cc;


cc_s * 
cs_init(int cnr, int cmmb)

    return _get_cc(cnr, cmmb);


void 
cs_deinit(cc_s *cc)

    cc_s *t, *_cc;

    IF_EXPS_TRUE_THEN_RETURN(!cc, VOIDV);
    for (_cc = t = cc; _cc; _cc = t) 
        t = t->next;
        _put_cc(_cc);
    

    return ;

为协程分配数据结构体的相关函数如下。

static int inline 
_get_cis_idle_ci(ci_s *ci, int nr)

    int f = NONE;
    int h, t = nr - 1;

#define _ST(i) ci[i].state
    for (h = 0; h <= t; ++h, --t) 
        IF_EXPS_TRUE_THEN_BREAK(!_ST(h) && ((f = h) + 1));
        IF_EXPS_TRUE_THEN_BREAK(!_ST(t) && (f = t));
    
#undef _ST

    return f;


static ci_s * 
_get_cc_idle_ci(cc_s *cc)

    int f = NONE;
    cc_s *p = NULL;
    cc_s *_cc = NULL;

    for (_cc = cc; _cc; _cc = _cc->next)
        p = _cc;
        IF_EXPS_TRUE_THEN_CONTINUE(!_cc->left);
        f  = _get_cis_idle_ci(_cc->ci, _cc->nr);
        IF_EXPS_TRUE_THEN_BREAK(NONE != f);
    

    if (!_cc) 
        p->next = _get_cc(CI_UNIT, STACK_UNIT);
        IF_EXPS_TRUE_THEN_RETURN(!p->next, NULL);
        _cc = p->next;
        f = 0;
    
    _cc->left--;
    _cc->ci[f].cc = _cc;
    
    return (_cc->ci + f);


ci_s *
cs_co(cc_s *cc, 
    char *cname, void *cfn, void *arg)

    ci_s *ci  = NULL;
    IF_EXPS_TRUE_THEN_RETURN(!cc || !cfn, NULL);

    ci = _get_cc_idle_ci(cc);
    IF_EXPS_TRUE_THEN_RETURN(!ci, NULL);
#define _SET_CI_MBR(mbr, v) ci->mbr = v
    _SET_CI_MBR(cfn, (cfn_f)cfn);
    _SET_CI_MBR(arg, arg);
    _SET_CI_MBR(cname, cname);
    _SET_CI_MBR(state, BORN);
#undef _SET_CI_MBR

    ci->back = _get_cc_idle_ci(cc);
    IF_EXPS_TRUE_THEN_RETURN(!ci->back && !(ci->state = PREGNANT), NULL);
    ci->back->state = BACKCI;

    return ci;

2.4 转换协程切换逻辑为C代码

(1) 实现 yield

yield 相关函数是后续 yield from 和 loop switching 的基础,相关代码如下。

static int 
_co_start(ci_s *ci)

    int ret;
    char *_arg  = NULL;
    ucontext_t *ctx = NULL;

    ctx = &ci-以上是关于一种在C语言中用 System V ucontext 实现的协程切换的主要内容,如果未能解决你的问题,请参考以下文章

一种在C语言中用汇编指令和 System V ucontext 支撑实现的协程切换

一种在 python 中用 asyncio 和协程实现的IO并发

记录一种在C语言中的打桩实现及原理

记录一种在C语言中的打桩实现及原理

记录一种在C语言中的打桩实现及原理

几种在js中循环数组的方法