0%

glibc协程的使用方法

有很多实现协程的方式,glibc里实现的协程叫做ucontext。我们可以先从相关的头文件里
看下ucontext的基本数据结构和API。

glibc代码的glibc/stdlib/ucontext.h是ucontext的几个API,ucontext数据结构是构架相关
的,定义在sys/ucontext.h,比如对于riscv的定义在:sysdeps/unix/sysv/linux/riscv/sys/ucontext.h。

具体看API之间,先看下协程(coroutine)的基本概念。协程可以在用户态实现执行上下文的
切换,可以看成一个轻量级的线程,但是,线程是系统层面实现的机制,没办法自己控制调度,
线程本身的调度就是内核调度,线程自己看不到调度,而协程API可以控制程序执行流直接
跳到一个上下文上执行。

在用户态一个上下文的定义就寄存器集合+栈(严格讲还要加上内存),协程在两个用户态上下
文上切换也就是要:1. 定义用户态上下文;2. 给出相关的用户态上下文获取、修改、配置以及
切换的API。

ucontext_t描述一个用户态上下文,显示是体系结构相关的,程序员可以直接操作里面的数据。
riscv下这个结构大概是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
typedef struct ucontext_t
{
unsigned long int __uc_flags;
struct ucontext_t *uc_link;
/* 其中有stack的基地址(ss_sp),大小(ss_size)和flag(ss_flags) */
stack_t uc_stack;
sigset_t uc_sigmask;
/* There's some padding here to allow sigset_t to be expanded in the
future. Though this is unlikely, other architectures put uc_sigmask
at the end of this structure and explicitly state it can be
expanded, so we didn't want to box ourselves in here. */
char __glibc_reserved[1024 / 8 - sizeof (sigset_t)];
/* We can't put uc_sigmask at the end of this structure because we need
to be able to expand sigcontext in the future. For example, the
vector ISA extension will almost certainly add ISA state. We want
to ensure all user-visible ISA state can be saved and restored via a
ucontext, so we're putting this at the end in order to allow for
infinite extensibility. Since we know this will be extended and we
assume sigset_t won't be extended an extreme amount, we're
prioritizing this. */
/* 其中定义riscv的通用寄存器和浮点寄存器 */
mcontext_t uc_mcontext;
} ucontext_t;

如下函数就是所有ucontext_t相关的API,头文件的注释写的比较清楚了,我们后面只做补充解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Get user context and store it in variable pointed to by UCP.  */
extern int getcontext (ucontext_t *__ucp) __THROWNL;

/* Set user context from information of variable pointed to by UCP. */
extern int setcontext (const ucontext_t *__ucp) __THROWNL;

/* Save current context in context variable pointed to by OUCP and set
context from variable pointed to by UCP. */
extern int swapcontext (ucontext_t *__restrict __oucp,
const ucontext_t *__restrict __ucp)
__THROWNL __INDIRECT_RETURN;

/* Manipulate user context UCP to continue with calling functions FUNC
and the ARGC-1 parameters following ARGC when the context is used
the next time in `setcontext' or `swapcontext'.

We cannot say anything about the parameters FUNC takes; `void'
is as good as any other choice. */
extern void makecontext (ucontext_t *__ucp, void (*__func) (void),
int __argc, ...) __THROW;

makecontext是对__ucp的配置,当下次触发__ucp运行时,使用__ucp的上下文,但是要执行
__func,__argc是__func的入参个数,后面依次是每个入参。这里__func的定义是没有参数,
但是后面确给了入参信息,使用的时候可以定义带入参的函数,传给makecontext时强制转换
成void (* __func)(void)。

程序员可以直接通过makecontext构造一个新的执行上下文,而不仅仅是通过getcontext获取
当前程序的上下文:

1
2
3
4
5
6
7
8
9
10
11
typedef void (* task_t) (void);
ucontext_t nc;

void new_context(ctx);
void *stack = malloc(4096);

nc.uc_stack.ss_sp = stack;
nc.uc_stack.ss_size = 4096;
nc.uc_link = NULL;

makecontext(&uc, (task_t)new_context, 1, ctx);

程序在必要的时候可以直接使用swapcontext执行makecontext构造的上下文:

1
2
3
ucontext_t curr;

swapcontext(&curr, &nc);