CS360 Lecture notes -- Signals


Signals

Signals are complex flow-of-control operations. A signal is an interruption of the program of some sort. For example, when you press CNTL-C, that sends the SIGINT signal to your program. When you press CNTL-\, that sends the SIGQUIT signal to your program. When you generate a segmentation violation, that sends the SIGSEGV signal to your program.

Your program has various ways of dealing with signals. By default, there are certain actions that take place. For example, which you press CNTL-C or CNTL-\, the program usually exits. That is the default action for SIGINT and SIGQUIT. When you get a segmentation violation, your program spits out an error message, potentially creates a core dump, and then exits. That is the default action for SIGSEGV.

You can redefine what happens when you get these signals, which allows you to write very flexible programs. Internally, when a signal is generated, the operating system takes over from the currently running program. It saves the current state of the program on the stack. Then, it calls an "interrupt handler" for the specific signal. For example, the default interrupt handlers for SIGINT and SIGQUIT cause the program to exit. The default interrupt handler for SIGSEGV prints an error, potentially causes the program to dump core, and then exits. If the interrupt handler for a signal calls return, then what happens is that the operating system takes over again, and restores the program from the state that it has saved on the stack. The program resumes from where it left off.

You can use the signal() function to define interrupt handlers for signals. As always, read the man page: man signal.

For example, look at src/sh1.c:

/* Write a signal handler for CNTL-C */

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

/* This is the signal handler that we are setting up for Control-C.  
   Instead of doing the default action, it will call this handler. */

void cntl_c_handler(int dummy)
{
  signal(SIGINT, cntl_c_handler);
  printf("You just typed cntl-c\n");
}

int main()
{
  int i, j;

  /* Register the signal handler with the operating system. */

  signal(SIGINT, cntl_c_handler);

  /* This code spins the CPU for a little while before exiting. */

  for (j = 0; j < 5000; j++) {   
    for (i = 0; i < 1000000; i++);
  }
  return 0;
}

What this does is set up a signal handler for SIGINT. Now, when the user presses CNTL-C, the operating system will save the current execution state of the program, and then execute cntl_c_handler. When cntl_c_handler returns, the operating system resumes the program from where it was interrupted. Thus, when you run sh1, each time you type CNTL-C, it will print "You just typed cntl-c", and the program will continue. It will exit by itself in 10 seconds or so.

The signal handler should follow the prototype of cntl_c_handler. In other words it should return a (void) (i.e. nothing), and should accept an integer argument, even if it will not use the argument. Otherwise, gcc will complain to you.

Also, note that I make a signal() call in the signal handler. On some systems, if you do not do this, then it will reinstall the default signal handler for CNTL-C once it has handled the signal. On some systems, you don't have to make the extra signal() call. Such is life in the land of multiple Unix's.


You can handle each different signal with a call to signal(). For example, src/sh1a.c defines different signal handlers for CNTL-C (which is SIGINT), and CNTL-\ (which is SIGQUIT). They print out the values of i and j when the signal is generated. Note that i and j must be global variables for this to work. This is one example when you have to use global variables.

Try this out by compiling the program and then running it, and pressing CNTL-C and CNTL-\ a bunch of times:

UNIX> bin/sh1a
^CYou just typed cntl-c.  j is 1748 and i is 399828
^\You just typed cntl-\.  j is 1859 and i is 99015
^CYou just typed cntl-c.  j is 1980 and i is 198717
^\You just typed cntl-\.  j is 2142 and i is 304705
^CYou just typed cntl-c.  j is 2244 and i is 220206
^\You just typed cntl-\.  j is 2402 and i is 259873
^CYou just typed cntl-c.  j is 2535 and i is 785388
^\You just typed cntl-\.  j is 2693 and i is 3998
^CYou just typed cntl-c.  j is 2852 and i is 14044
^CYou just typed cntl-c.  j is 4921 and i is 763113
UNIX>

You can also catch the segmentation violation signal. One of those CS legends is that some grad student used to put the following into his code:

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

void segv_handler(int dummy)
{
  signal(SIGINT, sint_handler);
  fprintf(stderr, "nfs server not responding, still trying\n");
  while(1) ;
}

int main()
...
  signal(SIGSEGV, segv_handler();

  rest of the code
}

This is so that if he was demo-ing his code, and a segmentation violation occured (which always seems to happen when you're demo-ing code), it would look like the network had frozen. Very clever. (I.e. look at and run src/sh1b.c. It should cause a segmentation violation, but instead looks like the network is hanging). Note also that CNTL-C is disabled, since that's what happens when NFS hangs.


Alarm()

Another use of signal handlers is the "alarm clock" that Unix provides. Read the man page for alarm(). What alarm(n) does is return, and then n seconds later, it will cause the SIGALRM signal to occur. If you have set a signal handler for it, then you can catch the signal, and do whatever it is that you wanted to do. For example, sh2.c is like sh1.c, only it prints out a message after the program has executed 3 seconds. alarm() is approximate -- it's not exactly 3 seconds, but we'll consider it close enough for the purposes of this class.

UNIX> bin/sh2
Three seconds just passed: j = 1245.  i = 287469
UNIX>
Finally, src/sh3.c shows how you can get Unix to send you SIGALRM every second. It's just a tweak to src/sh2.c where you have the alarm handler call alarm() to make Unix generate SIGALRM one second after the current one. I'll include the code below:

/* Use alarm() / SIGALRM to interrupt the program every second and print out its state. */

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

int i, j, seconds;         // Global variables, because we're accessing them in the signal handler.

/* Print out the time and the state, and then generate SIGALRM in another second. */

void alarm_handler(int dummy)
{
  seconds++;
  printf("%d second%s just passed: j = %4d.  i = %6d\n", seconds,
     (seconds == 1) ? " " : "s", j, i);
  signal(SIGALRM, alarm_handler);
  alarm(1);
}

int main()
{
  seconds = 0;

  /* Register the signal handler, and generate SIGALRM in a second. */

  signal(SIGALRM, alarm_handler);
  alarm(1);

  /* Spin the CPU */

  for (j = 0; j < 5000; j++) {
    for (i = 0; i < 1000000; i++);
  }
  return 0;
}

UNIX> bin/sh3
1 second  just passed: j =  805.  i = 305534
2 seconds just passed: j = 1608.  i = 783876
3 seconds just passed: j = 2419.  i =  77897
4 seconds just passed: j = 3217.  i = 643283
5 seconds just passed: j = 4019.  i =  30497
6 seconds just passed: j = 4823.  i = 907698
UNIX> 
On some systems, when you are in a signal handler for one signal, you cannot process that same signal again until the handler returns. On other systems, you can handle that same signal again. For example, look at sh4.c -- here's the code for the signal handlers:

/* Put a while loop into the alarm handler, to see if it
   can catch SIGALRM while it's in the alarm handler. */

int i, j;

void alarm_handler(int dummy)
{
  printf("One second has passed: j = %d.  i = %d\n", j, i);
  signal(SIGALRM, alarm_handler);
  alarm(1);
  while(1);
}

Alarm_handler() has an infinite loop in it, meaning that it never returns. The program runs for a second, and then SIGALRM is generated, and alarm_handler() is entered. It goes into an infinite loop, and one second later, SIGALRM is generated again. Depending on your version of Unix, different things may happen. For example, on my Macbook in 2021, I got the following:

UNIX> bin/sh4
One second has passed: j = 476.  i = 853593
(and it hangs)
In other words, the signal was to be ignored until you return from alarm_handler(), which of course never happened.

Here was the output on Solaris in something like 1996:

UNIX> sh4
One second has passed: j = 7.  i = 697646
One second has passed: j = 7.  i = 697646
One second has passed: j = 7.  i = 697646
One second has passed: j = 7.  i = 697646
...
In other words, you could catch SIGALRM from SIGALRM. Which output does your operating system produce?

You can generate and handle other signals reliably whether in a signal handler or not. For example, when you press CNTL-\ in sh4.c, it gets caught properly whether the program or the alarm_handler() is running -- give it a try. Here's an interesting question -- when SIGALRM is generated, and I'm in the alarm handler code, is the signal simply ignored, or is it queued until the alarm handler returns? Here's a program, (src/sh5.c), that tests this:

/* Test to see if SIGALRM is queued up if it's already in the alarm handler. */

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

int i, j;

/* Generate SIGALRM while in the alarm handler, but eventually return. */

void alarm_handler(int dummy)
{
  printf("One second has passed: j = %d.  i = %d\n", j, i);
  signal(SIGALRM, alarm_handler);
  alarm(1);

  for (; j < 5000; j++) {
    for (i = 0; i < 1000000; i++);
  }
}

int main()
{
  signal(SIGALRM, alarm_handler);
  alarm(1);

  for (j = 0; j < 5000; j++) {
    for (i = 0; i < 1000000; i++);
  }
  return 0;
}

When you run this program, here's the output:

UNIX> bin/sh5
One second has passed: j = 817.  i = 645456
One second has passed: j = 5000.  i = 1000000
UNIX> 
Here's what happened:

Finally, you can send any signal to a program with the kill command. Read the man page. Signal number 9 (SIGKILL) cannot be caught by your program, meaning that you cannot write a signal handler for it. This is nice because if you mess up writing a signal handler, then "kill -9" is the only way to kill the program.


Alarm() and exec

Here's another interesting question: If you call alarm(2), and then you call execlp or one of its variants, then your memory is overwritten by a new program, and the execlp() call doesn't return (because it can't). Does the SIGALRM signal still get delivered?

Here is a simple program (src/call_exec.c) to answer that question:

/* Test to see if alarm() works even after an exec call. */

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

int main()
{
  alarm(2);
  execlp("cat", "cat", NULL);
  perror("execl");
  return 0;
}

It calls alarm(2), and then execlp for the program cat. That program will block, reading from standard input. After two seconds, it dies, because SIGALRM was delivered to it!

UNIX> bin/call_exec
(Two seconds pass)
Alarm clock: 14
UNIX> 

SIGPIPE and the Stdio Library

When you write to a file descriptor that has no read end, the SIGPIPE signal will be generated. If uncaught, it will kill your program. Unfortunately, since the operating system often does its own buffering of writes, you don't usually generate SIGPIPE as soon as you should. It usually comes later. This can prove difficult to handle.

Let's take a look at the interaction of SIGPIPE and the standard I/O library, since that's usually where you are when SIGPIPE is generated. Take a look at src/sigpipe_1.c

/* This program repeatedly writes the string ABCDEFGHIJKLMNOPQRSTUVWXYZ to stdout, 
   testing the return values of fputs() and fflush() to see if there's
   a problem. */

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

int main()
{
  int i;
  char *s = "ABCDEFGHIJKLMNOPQRSTUVWXYZ\n";

  i = 0;
  while (1) {
    if (fputs(s, stdout) == EOF) {
      fprintf(stderr, "Died on iteration %d on fputs\n", i);
      exit(0);
    }
    if (fflush(stdout) != 0) {
      fprintf(stderr, "Died on iteration %d on fflush\n", i);
      exit(0);
    }
    fprintf(stderr, "Iteration %d done\n", i);
    i++;
  }
  return 0;
}

We're going to pipe this program into "head -n 1." You would think that this would generate SIGPIPE on the second fflush(), since head will exit after reading the first line. However, we get something different from what we expect (the exact output will of course differ from machine to machine and even from run to run):

UNIX> bin/sigpipe_1 | head -n 1
Iteration 0 done
Iteration 1 done
...
Iteration 2414 done
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Iteration 2415 done
Iteration 2416 done
UNIX> bin/sigpipe_1 | head -n 1
Iteration 0 done
Iteration 1 done
...
Iteration 2094 done
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Iteration 2095 done
Iteration 2096 done
UNIX> 
Since we don't get an error statement, we can assume that SIGPIPE was generated and killed the process. src/sigpipe_2.c catches the signal and ignores it:

/* This is the same as sigpipe_1.c, except we catch and ignore SIGPIPE. */

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

void sigpipe_handler(int dummy)
{
  fprintf(stderr, "Ignoring Sigpipe()\n");
  signal(SIGPIPE, sigpipe_handler);
}

(The rest is the same as sigpipe_1.c)

Now, we see that SIGPIPE is indeed generated. Better yet, when we ignore it, the subsequent fflush() fails:

UNIX> bin/sigpipe_2 | head -n 1
Iteration 0 done
Iteration 1 done
...
Iteration 2469 done
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Ignoring Sigpipe()
Died on iteration 2470 on fflush
UNIX> 
(When you run this, you may find that it fails on fputs() instead of fflush()).

With this information, you should take the following statement as gospel in your Lab 9:

Since head is most likely written with the stdio library, it performs buffering too, so perhaps it's not surprising that SIGPIPE is not generated for many iterations. However, were the operating system not buffering, the output of bin/sigpipe_2 | head -n 1 would be deterministic (it would always be the same).

To hammer this home further, check out src/instaclose.c:

/* Close standard input and then wait a second, before exiting. */

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

int main()
{
  close(0);
  sleep(1);
  exit(0);
}

This does no reading from standard input before closing it. When we call it, we still get nondeterministic output:

UNIX> bin/sigpipe_2 | bin/instaclose
Iteration 0 done
...
Iteration 46 done
Ignoring Sigpipe()
Died on iteration 47 on fflush
UNIX> bin/sigpipe_2 | bin/instaclose
Ignoring Sigpipe()
Died on iteration 0 on fflush
UNIX> 
Obviously, the operating system does buffering.

Of Compilers, Optimization, and the "volatile" keyword

When optimization is enabled in the compiler, it performs quite a bit of analysis, turning the program into one that produces equivalent output, but runs faster. Sometimes, you have no clue what the optimizer does, but you should be prepared to be mystified. Let's go back to src/sh3.c as an example. When we run it without optimization, the alarm handler is executed every second, printing out the value of i and j.
UNIX> bin/sh3
1 second  just passed: j =  812.  i = 988927
2 seconds just passed: j = 1629.  i = 866986
3 seconds just passed: j = 2461.  i = 757553
4 seconds just passed: j = 3277.  i = 335089
5 seconds just passed: j = 4101.  i = 606281
6 seconds just passed: j = 4911.  i = 922094
UNIX> 
Let's compile it with optimization and run it, to see what happens:
UNIX> gcc -O3 src/sh3.c
UNIX> ./a.out
UNIX> time ./a.out

real	0m0.004s
user	0m0.001s
sys	0m0.002s
UNIX> 
Man, that was fast -- it seems too fast. Back of the hand: we should be iterating 5,000,000,000 times. At roughtly 2 Ghz, that should be taking a few seconds, not 0.01 seconds. Something's amiss.

What's going on is that the compiler's analysis has figured out that the loops don't do anything, so it has deleted them.

Let's make the compiler keep the loops. How about putting this after the loop:

  printf("%d %d\n", j, i);

UNIX> gcc -O3 src/sh3a.c
UNIX> time ./a.out
5000 1000000

real	0m0.314s
user	0m0.001s
sys	0m0.002s
UNIX> 
Dang it! That's a smart compiler. Ok -- one more try in src/sh3b.c:

/* We're trying to outsmart the compiler.  Instead of having
   our loop do nothing, we'll have it repeatedly do a small calculation.
   We're hoping that the compiler can't figure this out and delete the loop! */

// Everything is the same except this loop:

  k = 0;
  for (j = 0; j < 5000; j++) {
    for (i = 0; i < 1000000; i++) k = (k < 0) ? (i -j) : (j - i);
  }
  printf("%d %d %d\n", j, i, k);
  return 0;
}

UNIX> gcc -O3 src/sh3b.c
UNIX> time ./a.out
1 second  just passed: j =    0.  i =      0
2 seconds just passed: j =    0.  i =      0
5000 1000000 995000

real	0m2.453s
user	0m2.087s
sys	0m0.003s
UNIX> 
There -- the compiler's not smarter than I am!!!! But now we have another problem -- i and j are zero. That can't be right. I won't make you look through the assembler, but what has happened is that the compiler has cached i and j into registers during main(), and thus their in-memory values remain 0 and 1 while main() is running.

The solution to this is rather inelegant. You need to preface the declarations of i and j with the keyword volatile. That's not a natural keyword to me, but that tells the compiler that it needs to store i and j in memory because the program's control flow may be weird. That's done in sh3c.c:

UNIX> gcc -O3 src/sh3c.c
UNIX> time ./a.out
1 second  just passed: j =  510.  i = 627530
2 seconds just passed: j = 1038.  i = 575758
3 seconds just passed: j = 1561.  i = 974312
4 seconds just passed: j = 2093.  i = 806465
5 seconds just passed: j = 2622.  i = 683371
6 seconds just passed: j = 3135.  i = 362346
7 seconds just passed: j = 3662.  i = 769581
8 seconds just passed: j = 4187.  i = 169098
9 seconds just passed: j = 4716.  i = 462009
5000 1000000 995000

real	0m9.889s
user	0m9.558s
sys	0m0.006s
UNIX> 
You'll note that the program takes 4 times longer to run, since i and j can't be cached in registers anymore.

Remember volatile when you learn setjmp/longjmp -- the same types of problems can occur.


sort_compare.c - Alternating selection and insertion sort

I typically don't get to this in class, but it's here in case you're interested. The program src/sort_compare.c generates two identical random arrays, and sorts one of them with selection sort, and the other with insertion sort. At each main loop, it checks a variable called switch_algorithms, which, if set, changes the algorithm from insertion to selection, and back again.

And of course, we use alarm() and an alarm handler to set switch_algorithms to one every second. This is what causes the program to change algorithms every second:

#include <stdio.h>
#include <signal.h>
#include <time.h>
#include <stdlib.h>
#include <unistd.h>

int switch_algorithms;        /* This will be set to one by the alarm handler. */
int selection_index;          /* The index of selection sort. */
int insertion_index;          /* The index of insertion sort. */

/* The alarm handler sets switch_algorithms to one, prints out the two 
   indices, and then sets up SIGLARM to be generated again in another second. */

void alarm_handler(int dummy)
{
  switch_algorithms = 1;
  printf("Selection_index: %10d        - Insertion_index: %10d\n", selection_index, insertion_index);
  signal(SIGALRM, alarm_handler);
  alarm(1);
}

/* This generates two identical integer arrays, sorts one by
   selection sort and the other by insertion sort.  */

int main(int argc, char **argv)
{
  int *array1, *array2;
  int size;
  int i, j, tmp, best;

  if (argc != 2) {
    fprintf(stderr, "usage: sort_compare size\n");
    exit(1);
  }
  size = atoi(argv[1]);
  array1 = (int *) malloc(sizeof(int) * size);
  array2 = (int *) malloc(sizeof(int) * size);
    
  for (j = 0; j < size; j++) {
    array1[j] = rand();
    array2[j] = array1[j];
  }

  /* Set the two indices, and set it so that SIGLARM 
     is generated in a second.  */

  selection_index = 0;
  insertion_index = 0;

  signal(SIGALRM, alarm_handler);
  alarm(1);

  /* While one of the sorts is not complete: */

  while (selection_index < size || insertion_index < size) {

    /* First do selection sort on array1, checking the switch_algorithms
       variables at each interation of the main loop. */

    switch_algorithms = 0;
    while (!switch_algorithms && selection_index < size) {
      best = selection_index;
      for (j = selection_index+1; j < size; j++) {
        if (array1[j] < array1[best]) best = j;
      }
      tmp = array1[best];
      array1[best] = array1[selection_index];
      array1[selection_index] = tmp;
      selection_index++;
    }

    /* Second, do insertion sort on array2, checking the switch_algorithms
       variables at each interation of the main loop. */

    switch_algorithms = 0;
    while (!switch_algorithms && insertion_index < size) {
      tmp = array2[insertion_index];
      for (j = insertion_index-1; j >= 0 && array2[j] > tmp; j--) array2[j+1] = array2[j];
      j++;
      array2[j] = tmp;
      insertion_index++;
    }
  }

  printf("Done\n");
  // for (i = 0; i < size; i++) printf("%10d %10d\n", array1[i], array2[i]);
  return 0;
}

Here it is running -- as anticipated, insertion sort is faster than selection sort!

UNIX> bin/sort_compare 100000
Selection_index:       6998        - Insertion_index:          0
Selection_index:       6999        - Insertion_index:      46498
Selection_index:      14607        - Insertion_index:      46499
Selection_index:      14608        - Insertion_index:      65655
Selection_index:      23049        - Insertion_index:      65656
Selection_index:      23050        - Insertion_index:      80043
Selection_index:      32463        - Insertion_index:      80044
Selection_index:      32464        - Insertion_index:      92482
Selection_index:      43280        - Insertion_index:      92483
Selection_index:      47380        - Insertion_index:     100000
Selection_index:      62480        - Insertion_index:     100000
Selection_index:      90692        - Insertion_index:     100000
Done
UNIX>