IC221: Systems Programming (SP14)


Home Policy Calendar Syllabus Resources Piazza

Lec 17: Alarms, sigaction(), and Reentrant System Calls

Table of Contents

1 Alarm Signals and SIGALRM

In the last lesson, we explored signal handling and user generated signals, particularly those from the terminal, the SIGKILL, and the user signals, or the SIGUSR1 and SIGUSR2. In this lessons, we'll explore O.S. generated signals. We'll start with SIGALRM.

1.1 Setting an alarm

A SIGALRM signal is delivered by the Operating System by request from the user after some amount of time. To request an alarm, use the alarm() system call:

unsigned int alarm(unsigned int seconds);

After seconds have passed since requesting the alarm(), the SIGALRM signal is delivered. The default behavior of SIGALRM is to terminate, so we can catch and handle the signal, leading to a nice hello world program:

#include <stdio.h>
#include <stdlib.h>

#include <unistd.h>
#include <signal.h>
#include <sys/signal.h>

void alarm_handler(int signum){
  printf("Buzz Buzz Buzz\n");
}

int main(){

  //set up alarm handler
  signal(SIGALRM, alarm_handler);

  //schedule alarm for 1 second
  alarm(1);

  //do not proceed until signal is handled
  pause();

}

The program looks very much like our other signal handling programs, except this time the signal is delivered via the alarm() call. Additionally, we are introducing a new system call, pause().

The pause() call will "pause" the program until a signal is delivered and handled. Pausing is an effective way to avoid busy waiting, e.g., while(1);, because the process is suspended during a pause and awoken following the return of the signal handler.

1.2 Recurring Alarms

Alarm can be set continually, but only one alarm is allowed per process. Subsequent calls to alarm() will reset the previous alarm. Suppose, now, that we want to write a program that will continually alarm every 1 second, we would need to reset the alarm once the signal is delivered. The natural place to do that is in the signal handler:

void alarm_handler(int signum){
  printf("Buzz Buzz Buzz\n");

  //set a new alarm for 1 second
  alarm(1);
}

int main(){

  //set up alarm handler
  signal(SIGALRM, alarm_handler);

  //schedule the first alarm
  alarm(1);

  //pause in a loop
  while(1){
    pause();
  }

}

After the first SIGALRM is delivered and "Buzz Buzz Buzz" is printed, another alarm is schedule via alarm(1). The process will resume after the pause(), but since it is in a loop, will return to a suspended state. The result is an alarm clockk buzzing every 1 second. Running with time utility, after about 4 seconds, we saw 4 buzzers.

#> time ./buzz_buzz
Buzz Buzz Buzz
Buzz Buzz Buzz
Buzz Buzz Buzz
Buzz Buzz Buzz
^C

real	0m4.473s
user	0m0.001s
sys	0m0.002s

One thing to note about this example is that while the signal handler is code that runs asynchronously, it is still code that is part of the process. Calling alarm() not in the main method is a perfectly fine thing to do and necessary.

1.3 Resetting Alarms

Let's suppose we want to add a snooze function to our alarm. If the user enters Ctrl-c then we want to reset the alarm to 5 seconds before buzzing again, like snooze. We can easily add a signal handler to do that.

void sigint_handler(int signum){
  printf("Snoozing!\n");

  //schedule next alarm for 5 seconds
  alarm(5);
}

void alarm_handler(int signum){
  printf("Buzz Buzz Buzz\n");

  //set a new alarm for 1 second
  alarm(1);
}

int main(){

  //set up alarm handler
  signal(SIGALRM, alarm_handler);

  //set up signint handler
  signal(SIGINT, sigint_handler);

  //schedule the first alarm
  alarm(1);

  //pause in a loop
  while(1){
    pause();
  }

}

And we can see that the output matches our expectation using the time utility. Note we need to use Ctrl-\ to quit the process.

#>time ./snooze_buzz
Buzz Buzz Buzz
Buzz Buzz Buzz
^CSnoozing!
Buzz Buzz Buzz
Buzz Buzz Buzz
^CSnoozing!
Buzz Buzz Buzz
Buzz Buzz Buzz
^\Quit: 3

real	0m15.469s
user	0m0.001s
sys	0m0.003s

There is some interesting dilemas here: What happened to the last alarm? And, what happens if I type Ctrl-C multiple times, how long will it snooze? The anser is, only one alarm may be schedule at one time. Calling alarm() again will reset any previous alarms, so the answer to the questions is that (1) the previous alarm is replaces, and subsequent snoozes only resets the previous snozoe back to 5 seconds.

If that's the case, how might we unschedule a previously scheduled alarm. The way to do that is by scheduling an alarm for 0 seconds, alarm(0). For example, we can finish our alarm clock by adding an "off" switch that listens for Ctrl-\ or SIGQUIT, which will unschedule the alarm and reset the signal handler for Ctrl-c back to the default.

void sigquit_handler(int signum){
  printf("Alarm Off\n");

  //turn off all pending alarms
  alarm(0);

  //reinstate default handler for SIGINT
  // Ctrl-C will now terminate program
  signal(SIGINT, SIG_DFL);
}

void sigint_handler(int signum){
  printf("Snoozing!\n");

  //schedule next alarm for 5 seconds
  alarm(5);
}

void alarm_handler(int signum){
  printf("Buzz Buzz Buzz\n");

  //set a new alarm for 1 second
  alarm(1);
}

int main(){

  //set up alarm handler
  signal(SIGALRM, alarm_handler);

  //set up signint handler
  signal(SIGINT, sigint_handler);

  //set up signint handler
  signal(SIGQUIT, sigquit_handler);

  //schedule the first alarm
  alarm(1);

  //pause in a loop
  while(1){
    pause();
  }

}

And we can track the output:

#>./offswitch_buzz
Buzz Buzz Buzz
Buzz Buzz Buzz
Buzz Buzz Buzz
^CSnoozing!
^\Alarm Off
^C

While this is a simple example, it demonstrates a lot of power in signal handling and how even in asynchronous settings, a lot of power programming concepts can be used. It's a totally different way to communicate with programs.

2 sigaction() and Reentrant Functions

Now, we're starting to get a bit better with signal handling, we need to turn our attention back to the pesky asynchronous nature of signal handling. In the previous part, we harnessed asynchronicity to program with just the signal handlers, but we now need to consider how signal handlers might affect the primary control flow as it is being interrupted.

2.1 sigaction()

To start, we need to learn a more advanced form of the signal() system call, sigaction(). Like signal(), sigaction() allows the programmer to specify a signal handler for a given signal, but it also enables the programmer to retrieve additional information about the signaling process and set additional flags and etc.

The decleration of sigaction() is as follows:

int sigaction(int signum, const struct sigaction *act,
                   struct sigaction *oldact);

The first argument is the signal to be handled, while the second and third arguments are references to a struct sigaction. It is in the struct sigaction that we set the handler function and additional arguments. It has the following members:

struct sigaction {
  void     (*sa_handler)(int);
  void     (*sa_sigaction)(int, siginfo_t *, void *);
  sigset_t   sa_mask;
  int        sa_flags;
};

The first two fields, sa_handler and sa_sigaction are function references to signal handlers; sa_handler has the same type as the handlers we've been using previously, and we can now write a simple hello world program with sigaction().

void handler(int signum){
  printf("Hello World!\n");
}

int main(){

  //declare a struct sigaction
  struct sigaction action;

  //set the handler
  action.sa_handler = handler;

  //call sigaction with the action structure
  sigaction(SIGALRM, &action, NULL);

  //schedule an alarm
  alarm(1);

  //pause
  pause();
}

We'll look at using the more advanced sa_sigaction handler later, but there are other important differences between signal() and sigaction() that are worth exploring. In particular, using sigaction() allows us to explores reentrant functions.

2.2 Reentrant Functions

Recall that when a signal handler is invoked, this is done outside the control flow of the program. Normally, the asynchronous invocation of the signal handler is not problematic: the context of the program is saved, the signal handler runs, and the original context of the program is restored.

However, what happens when the context of the program is within a blocking function, like reading from a device (read()) or waiting for a process to terminate (wait()). The arrival of the signal and invocation of the signal handler will interrupt the process, waking it from a blocking state to execute the signal handler, but what happens when it returns to the program?

The answer to that question depends on the operation being performed. Most functions are reentrant and can be restarted in such cases, but others are explicitly not. For example pause() is explicitely not reentrant by design; once intrupted, it should not return to a blocking state. But functions like read() and wait() need to be told to restart.

2.3 Interrupting System call EINTR

Let's first consider a simple example. Here is a rude little program that will ask for a users name, but if they don't answer within 1 second, it starts barking at the user "What's taking so long?"

void handler(int signum){
  printf("What's taking so long?\n");
  alarm(1);
}

int main(){

  char name[1024];

  struct sigaction action;
  action.sa_handler = handler;

  sigaction(SIGALRM, &action, NULL);

  alarm(1);

  printf("What is your name?\n");

  //scanf returns the number of items scanned
  if( scanf("%s", name) != 1){ 
    perror("scanf fail");
    exit(1);
  }

  printf("Hello %s!\n", name);

}

Running it, you can see that, yes, if you were to enter your name quickly, the program plays nice:

#> ./scanf_fail
What is your name?
adam
Hello adam!

But, if your late at all, it should start the barking process, but that's actually not what happens:

#> ./scanf_fail
What is your name?
What's taking so long?
scanf fail: Interrupted system call

Instead, we get an error in the scanf() function, which is a library function that must call read() to read from standard input. The read() is interupted, which results in the error message "Interrupted system call" whose error number is EINTR. From the man page for read:

EINTR  The  call  was interrupted by a signal before any data was read;
       see signal(7).

And we can follow up by reading in man 7 signal:

Interruption of System Calls and Library Functions by Signal Handlers
      If  a signal handler is invoked while a system call or library function
      call is blocked, then either:

      * the call is automatically restarted after the signal handler returns;
        or

      * the call fails with the error EINTR.

      Which  of  these  two  behaviors  occurs  depends  on the interface and
      whether or not the signal handler was established using the  SA_RESTART
      flag  (see sigaction(2)).

To avoid this scenario we need to set an additional flag for sigaction():

struct sigaction {
  void     (*sa_handler)(int);
  void     (*sa_sigaction)(int, siginfo_t *, void *);
  sigset_t   sa_mask;
  int        sa_flags; //<---
};

What we are going to do, is update our annoying program from before to use a SA_RESTART flag so that read() will be restarted after the signal handler returns:

2.4 SA_RESTART

If we take another look at the struct sigaction, there is a field fo flags:

void handler(int signum){
  printf("What's taking so long?\n");
  alarm(1);
}

int main(){

  char name[1024];

  struct sigaction action;
  action.sa_handler = handler;
  action.sa_flags = SA_RESTART; //<-- restart 

  sigaction(SIGALRM, &action, NULL);

  alarm(1);

  printf("What is your name?\n");

  //scanf returns the number of items scanned
  if( scanf("%s", name) != 1){ 
    perror("scanf fail");
    exit(1);
  }

  printf("Hello %s!\n", name);

}

2.5 Not all System Calls are Reentrant

It might seem like we've solved all the problems with the SA_RESTART flag, but not all system calls are reentrant. You can see a complete listing in man 7 signal, but we'll focus on one you might encounter in your programing. The sleep() system call is not reentrant.

We can see this with a simple example:

void handler(int signum){
  printf("Alarm\n");
  alarm(1);
}

int main(){

  struct sigaction action;
  action.sa_handler = handler;
  action.sa_flags = SA_RESTART; //<-- restart 

  sigaction(SIGALRM, &action, NULL);

  //alarm in 1 second
  alarm(1);

  //meanwhile sleep for 2 seconds
  sleep(2);

  //how long does the program run for?

}

A handler for SIGALRM is established with the SA_RESTART flag, so all should be good. An alarm is scheduled for 1 second and then the program should sleep for 2 seconds. The question is: How long does the program take to run?

There are two possibilities. First, it could take 2 seconds because SIGALRM is handled the sleep() is restarted with an remaining 1 second to sleep, totaling 2 seconds worth runtime. Alternatively, the program will run for 1 second; once SIGALRM is handled, the sleep will not be restared, and the program terminates with 1 second of runtime. Let's run it and find out.

>time ./sleep_restart
Alarm

real	0m1.005s
user	0m0.001s
sys	0m0.002s

The program runs for only 1 second, and that is because sleep() is not reentrant. It cannot be restarted after a signal handler. This is just a singular example, but there are other system calls that meet these conditions, some you might also use, like send() and recv() for network socket programming, and understanding the properties of reentrant system calls is important to becoming an effective systems programmer.