在两个函数上下文的执行之间切换

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 上是 伪造的 ,根本不应该使用。)你也不应使用 alarmsleep,因为它们 未指定,并且可能会相互发生灾难性的相互作用。而是使用 setitimer(或 timer_settime,但这是 POSIX.1-2008 中的新功能,而 ucontext 函数在 -2008 年被 withdrawn)和nanosleep。这还有一个好处,就是你可以设置一个重复的定时器,然后忘记它。

另外,您的程序可以通过意识到您只需要 两个 个上下文而不是三个来大大简化。对原上下文使用c2,直接调用nextOdd。这消除了 first_callcmain 以及 nextOddnextEven.

中复杂的切换逻辑

最后,nextOddnextEven 中的循环索引变量应该是 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 */
}