ucontext该结构提供了所谓的用户上下文信息,用于描述调用信号处理器函数前的进程状态,其中包括上一个进程信号掩码以及寄存器的保存值,例如程序计数器(cp)和栈指针寄存器(sp),使用结构 ucontext_t 的其他函数有 getcontext()、makecontext()、setcontext()和 swapcontext(),分别对应的功能是允许进程去接收、创建、改变以及交换执行上下文。(这些操作有点类似于 setjmp()和 longjmp(),但更为通用。)可以使用这些函数来实现协程(coroutines),令进程的执行线程在两个(或多个)函数之间交替。SUSv3 规定了这些函数,但将它们标记为已废止。SUSv4 则将其删去,并建议使用 POSIX 线程来重写旧有的应用程序。glibc 手册页提供了关于这些函数的深入信息。
ucontext使得linux程序可以在用户态执行上下文切换,从而避免了进程或者线程切换导致的切换用户空间、切换堆栈,因此,效率相对更高。
结构体
有两个结构体,分别是mcontext_t和ucontext_t,其中mcontext_t是透明的。我们只需要关注ucontext_t就可以了
ucontext_t定义在头文件ucontext.h中,为:
/* Userlevel context. */
typedef struct ucontext_t
{
unsigned long int __ctx(uc_flags);
struct ucontext_t *uc_link;
stack_t uc_stack;
mcontext_t uc_mcontext;
sigset_t uc_sigmask;
struct _libc_fpstate __fpregs_mem;
} ucontext_t;
关键字段说明:
- uc_link:当前context执行结束之后要执行的下一个context,如果uc_link为空,执行完当前context后退出程序。
- uc_stack:当前上下文所需要的stack。
- uc_mcontext:保存具体的程序执行上下文,如PC值、堆栈指针和寄存器的值。它的实现依赖于底层,是硬件平台相关的,因此不透明。
- uc_sigmask:执行上下文过程中,要屏蔽的信号集合,即信号掩码。
函数
int getcontext(ucontext_t *ucp);
初始化ucp结构体,将当前上下文保存在ucp中。成功时,返回0,错误返回-1,并设置errno
int setcontext(const ucontext_t *ucp);
设置当前上下文为ucp所指向的上下文。setcontext的上下文ucp应该通过getcontext或者makecontext取得,如果调用成功则不返回。
- 如果程序是通过getcontext取得,则程序会继续执行这个调用。
- 如果context是通过makecontext取得,则程序调用makecontext函数的第二个参数指向的函数,这时,如果函数返回,则恢复ucp->uc_link,如果func函数返回,则恢复makecontext第一个参数指向的上下文第一个参数指向的上下文context_t中指向的uc_link.如果uc_link为NULL,则线程退出。
void makecontext(ucontext_t *ucp, void (*func)(), int argc, ...);
makecontext修改通过getcontext取得的上下文ucp(这意味着调用makecontext前必须先调用getcontext)。然后给该上下文指定一个栈空间ucp->stack,设置后继的上下文ucp->uc_link.
当上下文通过setcontext或者swapcontext激活后,执行func函数,argc为func的参数个数,后面是func的参数序列。当func执行返回后,继承的上下文被激活,如果继承上下文为NULL时,线程退出。
int swapcontext(ucontext_t *oucp, const ucontext_t *ucp);
保存当前上下文到oucp结构体中,然后激活upc上下文。
如果执行成功,getcontext返回0,setcontext和swapcontext不返回;如果执行失败,getcontext,setcontext,swapcontext返回-1,并设置对于的errno.
简单说来, getcontext获取当前上下文,setcontext设置当前上下文,swapcontext切换上下文,makecontext创建一个新的上下文。
例子
#include <ucontext.h>
#include <stdio.h>
int done = 0;
int main()
{
ucontext_t context;
getcontext(&context);
if (done)
{
printf("return from getcontext,exit\n");
return 0;
}
done = 1;
setcontext(&context);
printf("finished");
return 0;//never goto here!
}
程序先调用getcontext保存当前寄存器信息到了context中,然后执行setcontext,所谓的setcontext就是把context中的寄存器信息恢复到当前的寄存器信息,也就是说,强制把context的pc bp sp等值,赋值到了当前cpu的寄存中,显而易见的是,这又跳回去到了getcontext 处。
上面这个goto一样,那用goto就行了?那看下面这个例子(test2.c):
#include <ucontext.h>
#include <stdio.h>
int done = 0;
int func1(ucontext_t *context)
{
done = 1;
setcontext(context);
}
int main()
{
ucontext_t context;
getcontext(&context);
if (done)
{
printf("return from getcontext,exit\n");
return 0;
}
func1(&context);
printf("finished");
return 0;
}
goto 一定做不了函数之间的跳转,只能做本地跳转。当然 getcontet 和 setcontext 肯定不仅仅只有这些功能,看下面这个例子。
//test3.c
#include <ucontext.h>
#include <stdio.h>
#include <malloc.h>
void func()
{
printf("in func\n");
}
int main()
{
ucontext_t context;
getcontext(&context);
//指定栈
context.uc_stack.ss_sp = malloc(10000);
context.uc_stack.ss_size = 10000;
context.uc_link = NULL;
makecontext(&context, func, 0);
setcontext(&context);
printf("finished");
return 0;//never goto here!
}
先setcontext初始化context,然后makecontext,指定跳转的函数,然后再setcontext切换context到func函数。
除此之外,还为新的context指定了新的栈,为什么呢?因为如果不指定栈,那么还是getcontext时获取的sp bp指针,sp bp描述的是main函数的栈大小,如果main函数栈大小是100字节,但是你所要指定的fun的栈大小需要1000字节,显然不够用。
看到这,肯定好多人心里很是疑惑,如果执行完func,还会返回到main吗?答是不会。因为这里没有设置后继上下文。
例子:
#include <ucontext.h>
#include <stdio.h>
void func1(void * arg)
{
puts("1");
puts("11");
puts("111");
puts("1111");
}
void context_test()
{
char stack[1024*128];
ucontext_t child,main;
getcontext(&child); //获取当前上下文
child.uc_stack.ss_sp = stack;//指定栈空间
child.uc_stack.ss_size = sizeof(stack);//指定栈空间大小
child.uc_stack.ss_flags = 0;
child.uc_link = &main;//设置后继上下文
makecontext(&child,(void (*)(void))func1,0);//修改上下文指向func1函数
swapcontext(&main,&child);//切换到child上下文,保存当前上下文到main
puts("main");//如果设置了后继上下文,func1函数指向完后会返回此处
}
int main()
{
context_test();
return 0;
}
在context_test中,创建了一个用户线程child,其运行的函数为func1.指定后继上下文为main
func1返回后激活后继上下文,继续执行主函数。
例子:
#include <ucontext.h>
#include <stdio.h>
#include <malloc.h>
int did = 0;
void func()
{
did = 1;
printf("in func\n");
}
int main()
{
ucontext_t context,rt;
getcontext(&context);
getcontext(&rt);
if(did == 1)
{
printf("continue from func\n");
return 0;
}
//指定栈
context.uc_stack.ss_sp = malloc(10000);
context.uc_stack.ss_size = 10000;
context.uc_link = &rt;
makecontext(&context, func, 0);
setcontext(&context);
return 0;//never goto here!
}
test4.c比test3.c就多个一个对uc_link的赋值(还有一些流程控制变量)。uc_link就指定了func执行完之后,接着执行的上下文,如果为空,则执行完func后,exit,就像test3.c一样;如果uc_link不为空,则执行完func后,接着执行uc_link就指定的上下文。
就如__start_context逻辑一致。
接下来,看test4.c,我们从getcontext(&rt);处返回了,但是能不能从setcontext处返回呢,当然可以,方法1就是getcontext(&rt);返回后,goto到setcontext 后面。但是glibc还提供了一个接口就是swapcontext。且看下面那个例子。
#include <ucontext.h>
#include <stdio.h>
#include <malloc.h>
int did = 0;
void func()
{
did = 1;
printf("in func\n");
}
int main()
{
ucontext_t context,rt;
getcontext(&context);
//指定栈
context.uc_stack.ss_sp = malloc(10000);
context.uc_stack.ss_size = 10000;
context.uc_link = &rt;
makecontext(&context, func, 0);
swapcontext(&rt,&context);
printf("finally return\n");
return 0;//should goto here!
}
swapcontext 两件事,一个就是保存当前的上下文,到rt中,然后切换到context。context指定的上下文执行完成之后,就跳到了swapcontext后面了。
注意:这些xxxcontext函数给予了用户态程序“调度的功能”,这和os的调度不同,os是对各个线程,即task_struct进行切换调度,而各个线程内部,也就是用户态程序,可以用上面这些库函数进行颗粒度更小的调度(说切换更合理)。这就是所谓的协程的概念。