Search code examples
csignalsucontext

Switching between the execution of two function contexts


I am trying to write a program where I have defined two functions and one prints odd numbers while the other prints even numbers. The program executes a function for a certain amount of time and when it receives an alarm signal, it starts executing the second function after saving the current function's context. When it receives the next alarm signal, it resumes the execution of the first function from its last saved context.

I have used the functions getcontext and swapcontext for this.

Here is my code:

#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();
}

The output I received is:

odd:1
odd:3
even:0
even:2
odd:4
odd:6
even:8
even:10
odd:12

Why is it restoring contexts every time but still printing the values of the function nextEven() ?


Solution

  • This program contains two outright bugs and several infelicities.

    The first bug is very simple:

    int switch_context = 0, first_call = 1;
    

    The variable switch_context is used to communicate from an asynchronous signal handler to the main program. Therefore, for correct operation, it must be given the type volatile sig_atomic_t. (If you don't do this, the compiler may assume that nobody ever sets switch_context to 1, and delete all the calls to swapcontext!) sig_atomic_t might be as small as char, but you only ever set switch_context to 0 or 1, so that's not a problem.

    The second bug is more involved: you aren't initializing your coroutine contexts at all. This is finicky and poorly explained by the manpages. You must first call getcontext on each context. For each context other than the original context, you must then allocate a stack for it, and apply makecontext to define the entry point. If you don't do all of these things, swapcontext/setcontext will crash. A full initialization looks something like this:

    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);
    

    (There's no good way to know how much stack to allocate, but eight megabytes ought to be enough for anyone. I suppose you could use getrlimit(RLIMIT_STACK). In a production-grade program I would use mmap so that I could then use mprotect to define guard bands on both sides of the stack, but that's a lot of extra code for a demo like this.)

    On to the infelicities. You should always use sigaction to set signal handlers, not signal, because signal is underspecified. (Note that sigaction is not available on Windows. That is because signals are fake on Windows and should not be used at all.) You should also not use alarm nor sleep, because they are also underspecified, and may interact catastrophically with each other. Instead use setitimer (or timer_settime, but that's new in POSIX.1-2008, whereas the ucontext functions were withdrawn in -2008) and nanosleep. This also has the advantage that you can set a repeating timer and forget about it.

    Also, your program can be substantially simplified by realizing that you only need two contexts, not three. Use c2 for the original context, and directly call nextOdd. This eliminates first_call and cmain and the complicated switching logic in nextOdd and nextEven.

    Finally, your loop index variables in nextOdd and nextEven should be unsigned so that the behavior is well-defined upon wrapping around (if you care to wait 2^31 seconds), and you should set stdout to line-buffered so that each line of output appears immediately even if redirected to a file.

    Putting it all together I get this:

    #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 */
    }