在两个函数上下文的执行之间切换
Switching between the execution of two function contexts
我正在尝试编写一个程序,我在其中定义了两个函数,一个打印奇数,另一个打印偶数。程序执行一个函数一定时间,当它接收到报警信号时,它在保存当前函数的上下文后开始执行第二个函数。当它收到下一个警报信号时,它会从上次保存的上下文中恢复执行第一个函数。
为此我使用了函数 getcontext 和 swapcontext。
这是我的代码:
#include<stdio.h>
#include<signal.h>
#include<ucontext.h>
ucontext_t c1, c2, cmain;
int switch_context = 0, first_call = 1;
void handler(int k)
{
switch_context = 1;
}
void nextEven()
{
int i;
for(i = 0; ; i += 2)
{
if(switch_context)
{
alarm(2);
switch_context = 0;
if(first_call)
{
first_call = 0;
swapcontext(&c1, &cmain);
}
else
{
swapcontext(&c1, &c2);
}
}
printf("even:%d\n", i);
sleep(1);
}
}
void nextOdd()
{
int j;
for(j = 1; ; j += 2)
{
if(switch_context)
{
alarm(2);
switch_context = 0;
if(first_call)
{
first_call = 0;
swapcontext(&c2, &cmain);
}
else
{
swapcontext(&c2, &c1);
}
}
printf("odd:%d\n", j);
sleep(1);
}
}
int main()
{
signal(SIGALRM, handler);
alarm(2);
getcontext(&cmain);
if(first_call) nextOdd();
nextEven();
}
我收到的输出是:
odd:1
odd:3
even:0
even:2
odd:4
odd:6
even:8
even:10
odd:12
为什么每次都恢复上下文但仍然打印函数 nextEven() 的值?
该程序包含两个明显的错误和一些不当之处。
第一个bug很简单:
int switch_context = 0, first_call = 1;
变量switch_context
用于从异步信号处理程序到主程序的通信。因此,为了正确操作,它 必须 被赋予类型 volatile sig_atomic_t
。 (如果不这样做,编译器可能会假设没有人将 switch_context
设置为 1,并删除对 swapcontext
的所有调用!)sig_atomic_t
可能与 char
,但您只将 switch_context
设置为 0 或 1,所以这不是问题。
第二个错误涉及更多:您根本没有初始化协程上下文。这很挑剔,联机帮助页解释得不好。您必须先在每个上下文中调用 getcontext
。对于原始上下文之外的每个上下文,您必须为其分配一个堆栈,并应用 makecontext
来定义入口点。如果您不执行所有这些操作,swapcontext
/setcontext
将会崩溃。完整的初始化看起来像这样:
getcontext(&c1);
c1.uc_stack.ss_size = 1024 * 1024 * 8;
c1.uc_stack.ss_sp = malloc(1024 * 1024 * 8);
if (!c1.uc_stack.ss_sp) {
perror("malloc");
exit(1);
}
makecontext(&c1, nextEven, 0);
(没有好方法知道要分配多少堆栈,但八兆字节对任何人来说都应该足够了。我想你可以使用 getrlimit(RLIMIT_STACK)
。在生产级程序中我会使用 mmap
这样我就可以使用 mprotect
在堆栈的两侧定义保护带,但是对于像这样的演示来说,这是很多额外的代码。)
关于缺点。您应该始终使用 sigaction
来设置信号处理程序,而不是 signal
,因为 signal
未指定。 (请注意,sigaction
在 Windows 上不可用。那是因为信号在 Windows 上是 伪造的 ,根本不应该使用。)你也不应使用 alarm
或 sleep
,因为它们 也 未指定,并且可能会相互发生灾难性的相互作用。而是使用 setitimer
(或 timer_settime
,但这是 POSIX.1-2008 中的新功能,而 ucontext 函数在 -2008 年被 withdrawn)和nanosleep
。这还有一个好处,就是你可以设置一个重复的定时器,然后忘记它。
另外,您的程序可以通过意识到您只需要 两个 个上下文而不是三个来大大简化。对原上下文使用c2
,直接调用nextOdd
。这消除了 first_call
和 cmain
以及 nextOdd
和 nextEven
.
中复杂的切换逻辑
最后,nextOdd
和 nextEven
中的循环索引变量应该是 unsigned
以便在回绕时明确定义行为(如果您想等待 2^31秒),并且您应该将 stdout 设置为行缓冲,以便即使重定向到文件,每行输出也会立即出现。
把它们放在一起我得到这个:
#define _XOPEN_SOURCE 600 /* ucontext was XSI in Issue 6 and withdrawn in 7 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include <ucontext.h>
#include <unistd.h>
#ifdef __GNUC__
#define UNUSED(arg) arg __attribute__((unused))
#else
#define UNUSED(arg) arg
#endif
static ucontext_t c1, c2;
static volatile sig_atomic_t switch_context = 0;
static void
handler(int UNUSED(signo))
{
switch_context = 1;
}
static void
nextEven(void)
{
struct timespec delay = { 1, 0 };
for (unsigned int i = 0;; i += 2) {
if (switch_context) {
switch_context = 0;
swapcontext(&c1, &c2);
}
printf("even:%d\n", i);
nanosleep(&delay, 0);
}
}
static void
nextOdd(void)
{
struct timespec delay = { 1, 0 };
for (unsigned int i = 1;; i += 2) {
if (switch_context) {
switch_context = 0;
swapcontext(&c2, &c1);
}
printf("odd:%d\n", i);
nanosleep(&delay, 0);
}
}
int
main(void)
{
/* flush each printf as it happens */
setvbuf(stdout, 0, _IOLBF, 0);
/* initialize main context */
getcontext(&c2);
/* initialize coroutine context */
getcontext(&c1);
c1.uc_stack.ss_size = 1024 * 1024 * 8;
c1.uc_stack.ss_sp = malloc(1024 * 1024 * 8);
if (!c1.uc_stack.ss_sp) {
perror("malloc");
exit(1);
}
makecontext(&c1, nextEven, 0);
/* initiate periodic timer signals */
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = handler;
sa.sa_flags = SA_RESTART;
if (sigaction(SIGALRM, &sa, 0)) {
perror("sigaction");
exit(1);
}
struct itimerval it;
memset(&it, 0, sizeof it);
it.it_interval.tv_sec = 2;
it.it_value.tv_sec = 2;
if (setitimer(ITIMER_REAL, &it, 0)) {
perror("setitimer");
exit(1);
}
nextOdd(); /* does not return */
}
我正在尝试编写一个程序,我在其中定义了两个函数,一个打印奇数,另一个打印偶数。程序执行一个函数一定时间,当它接收到报警信号时,它在保存当前函数的上下文后开始执行第二个函数。当它收到下一个警报信号时,它会从上次保存的上下文中恢复执行第一个函数。
为此我使用了函数 getcontext 和 swapcontext。
这是我的代码:
#include<stdio.h>
#include<signal.h>
#include<ucontext.h>
ucontext_t c1, c2, cmain;
int switch_context = 0, first_call = 1;
void handler(int k)
{
switch_context = 1;
}
void nextEven()
{
int i;
for(i = 0; ; i += 2)
{
if(switch_context)
{
alarm(2);
switch_context = 0;
if(first_call)
{
first_call = 0;
swapcontext(&c1, &cmain);
}
else
{
swapcontext(&c1, &c2);
}
}
printf("even:%d\n", i);
sleep(1);
}
}
void nextOdd()
{
int j;
for(j = 1; ; j += 2)
{
if(switch_context)
{
alarm(2);
switch_context = 0;
if(first_call)
{
first_call = 0;
swapcontext(&c2, &cmain);
}
else
{
swapcontext(&c2, &c1);
}
}
printf("odd:%d\n", j);
sleep(1);
}
}
int main()
{
signal(SIGALRM, handler);
alarm(2);
getcontext(&cmain);
if(first_call) nextOdd();
nextEven();
}
我收到的输出是:
odd:1
odd:3
even:0
even:2
odd:4
odd:6
even:8
even:10
odd:12
为什么每次都恢复上下文但仍然打印函数 nextEven() 的值?
该程序包含两个明显的错误和一些不当之处。
第一个bug很简单:
int switch_context = 0, first_call = 1;
变量switch_context
用于从异步信号处理程序到主程序的通信。因此,为了正确操作,它 必须 被赋予类型 volatile sig_atomic_t
。 (如果不这样做,编译器可能会假设没有人将 switch_context
设置为 1,并删除对 swapcontext
的所有调用!)sig_atomic_t
可能与 char
,但您只将 switch_context
设置为 0 或 1,所以这不是问题。
第二个错误涉及更多:您根本没有初始化协程上下文。这很挑剔,联机帮助页解释得不好。您必须先在每个上下文中调用 getcontext
。对于原始上下文之外的每个上下文,您必须为其分配一个堆栈,并应用 makecontext
来定义入口点。如果您不执行所有这些操作,swapcontext
/setcontext
将会崩溃。完整的初始化看起来像这样:
getcontext(&c1);
c1.uc_stack.ss_size = 1024 * 1024 * 8;
c1.uc_stack.ss_sp = malloc(1024 * 1024 * 8);
if (!c1.uc_stack.ss_sp) {
perror("malloc");
exit(1);
}
makecontext(&c1, nextEven, 0);
(没有好方法知道要分配多少堆栈,但八兆字节对任何人来说都应该足够了。我想你可以使用 getrlimit(RLIMIT_STACK)
。在生产级程序中我会使用 mmap
这样我就可以使用 mprotect
在堆栈的两侧定义保护带,但是对于像这样的演示来说,这是很多额外的代码。)
关于缺点。您应该始终使用 sigaction
来设置信号处理程序,而不是 signal
,因为 signal
未指定。 (请注意,sigaction
在 Windows 上不可用。那是因为信号在 Windows 上是 伪造的 ,根本不应该使用。)你也不应使用 alarm
或 sleep
,因为它们 也 未指定,并且可能会相互发生灾难性的相互作用。而是使用 setitimer
(或 timer_settime
,但这是 POSIX.1-2008 中的新功能,而 ucontext 函数在 -2008 年被 withdrawn)和nanosleep
。这还有一个好处,就是你可以设置一个重复的定时器,然后忘记它。
另外,您的程序可以通过意识到您只需要 两个 个上下文而不是三个来大大简化。对原上下文使用c2
,直接调用nextOdd
。这消除了 first_call
和 cmain
以及 nextOdd
和 nextEven
.
最后,nextOdd
和 nextEven
中的循环索引变量应该是 unsigned
以便在回绕时明确定义行为(如果您想等待 2^31秒),并且您应该将 stdout 设置为行缓冲,以便即使重定向到文件,每行输出也会立即出现。
把它们放在一起我得到这个:
#define _XOPEN_SOURCE 600 /* ucontext was XSI in Issue 6 and withdrawn in 7 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <time.h>
#include <sys/time.h>
#include <ucontext.h>
#include <unistd.h>
#ifdef __GNUC__
#define UNUSED(arg) arg __attribute__((unused))
#else
#define UNUSED(arg) arg
#endif
static ucontext_t c1, c2;
static volatile sig_atomic_t switch_context = 0;
static void
handler(int UNUSED(signo))
{
switch_context = 1;
}
static void
nextEven(void)
{
struct timespec delay = { 1, 0 };
for (unsigned int i = 0;; i += 2) {
if (switch_context) {
switch_context = 0;
swapcontext(&c1, &c2);
}
printf("even:%d\n", i);
nanosleep(&delay, 0);
}
}
static void
nextOdd(void)
{
struct timespec delay = { 1, 0 };
for (unsigned int i = 1;; i += 2) {
if (switch_context) {
switch_context = 0;
swapcontext(&c2, &c1);
}
printf("odd:%d\n", i);
nanosleep(&delay, 0);
}
}
int
main(void)
{
/* flush each printf as it happens */
setvbuf(stdout, 0, _IOLBF, 0);
/* initialize main context */
getcontext(&c2);
/* initialize coroutine context */
getcontext(&c1);
c1.uc_stack.ss_size = 1024 * 1024 * 8;
c1.uc_stack.ss_sp = malloc(1024 * 1024 * 8);
if (!c1.uc_stack.ss_sp) {
perror("malloc");
exit(1);
}
makecontext(&c1, nextEven, 0);
/* initiate periodic timer signals */
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = handler;
sa.sa_flags = SA_RESTART;
if (sigaction(SIGALRM, &sa, 0)) {
perror("sigaction");
exit(1);
}
struct itimerval it;
memset(&it, 0, sizeof it);
it.it_interval.tv_sec = 2;
it.it_value.tv_sec = 2;
if (setitimer(ITIMER_REAL, &it, 0)) {
perror("setitimer");
exit(1);
}
nextOdd(); /* does not return */
}