CS360 Lecture notes -- Signals


Signals

Signals are a complex flow-of-control operation. A signal is an interruption of the program of some sort. For example, when you hit CNTL-C, that sends the SIGINT signal to your program. When you hit 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 hit CNTL-C, the program usually exits. That is the default action for SIGINT. When you hit CNTL-\ or get a segmentation violation, your program dumps core and then exits. That is the default action for SIGQUIT and 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 handler for SIGINT causes the program to exit. The default interrupt handler for SIGSEGV and SIGQUIT causes the program to dump core and then exit. 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 (usually -- there are some times when it doesn't).

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

For example, look at sh1.c:

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

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

main()
{
  int i, j;

  signal(SIGINT, cntl_c_handler);

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

What this does is set up an interrupt handler for SIGINT. Now, when the user hits 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, 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 hitting CNTL-C and CNTL-\ a bunch of times:

UNIX> sh1a
^CYou just typed cntl-c.  j is 2 and i is 539943
^CYou just typed cntl-c.  j is 2 and i is 919180
^\You just typed cntl-\.  j is 4 and i is 413031
^CYou just typed cntl-c.  j is 5 and i is 20458
^\You just typed cntl-\.  j is 6 and i is 73316
^\You just typed cntl-\.  j is 6 and i is 683034
^CYou just typed cntl-c.  j is 7 and i is 292244
^CYou just typed cntl-c.  j is 13 and i is 738661
^\You just typed cntl-\.  j is 14 and i is 789583
^\You just typed cntl-\.  j is 16 and i is 42225
^\You just typed cntl-\.  j is 16 and i is 209458
^CYou just typed cntl-c.  j is 17 and i is 260584
^\You just typed cntl-\.  j is 19 and i is 982514
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) ;
}

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 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> sh2
Three seconds just passed: j = 1245.  i = 287469
UNIX>
Finally, sh3.c shows how you can get Unix to send you SIGALRM every second. It's just a tweak to sh2.c where you have the alarm handler call alarm to make Unix generate SIGALRM one second after the current one.
UNIX> sh3
UNIX> sh3
1 second  just passed: j =  398.  i = 804296
2 seconds just passed: j =  804.  i = 487960
3 seconds just passed: j = 1210.  i = 977119
4 seconds just passed: j = 1605.  i = 627513
5 seconds just passed: j = 1997.  i = 468054
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.

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. In Solaris, the signal will be handled, and you'll enter alarm_handler() anew. In SunOS, the signal will be ignored until you return from alarm_handler(), which of course never happens.

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
...
And here was the output on SunOS in 1994:
UNIX> sh4
One second has passed: j = 7.  i = 584436

Which output does your modern operating system produce?

You can generate and handle other signals reliably whether in a signal handler or not. For example, when you hit CNTL-\ in sh4.c, it gets caught properly whether the program or the alarm_handler() is running -- give it a try.

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.


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 sigpipe_1.c

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

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++;
  }
}

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> sigpipe_1 | head -n 1
Iteration 0 done
Iteration 1 done
...
Iteration 2414 done
ABCDEFGHIJKLMNOPQRSTUVWXYZ
Iteration 2415 done
Iteration 2416 done
UNIX> 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. sigpipe_2.c catches the signal and ignores it:

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

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

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

  signal(SIGPIPE, sigpipe_handler);
  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++;
  }
}

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

UNIX> 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 sigpipe_2 | head -n 1 would be deterministic (it would always be the same).

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

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

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> sigpipe_2 | instaclose
Iteration 0 done
...
Iteration 46 done
Ignoring Sigpipe()
Died on iteration 47 on fflush
UNIX> sigpipe_2 | 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 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> sh3
1 second  just passed: j =  398.  i = 804296
2 seconds just passed: j =  804.  i = 487960
3 seconds just passed: j = 1210.  i = 977119
4 seconds just passed: j = 1605.  i = 627513
5 seconds just passed: j = 1997.  i = 468054
UNIX> 
Let's compile it with optimization and run it, to see what happens:
UNIX> gcc -O3 sh3.c
UNIX> a.out
UNIX> time a.out
0.010u 0.000s 0:00.00 0.0%	0+0k 0+0io 0pf+0w
UNIX> 
Man, that was fast -- it seems too fast. Back of the hand: we should be iterating 2,000,000,000 times. At roughtly 2 Ghz, that should be taking roughly a second, 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 sh3a.c
UNIX> time a.out
2000 1000000
0.010u 0.000s 0:00.00 0.0%	0+0k 0+0io 0pf+0w
UNIX> 
Dang it! That's a smart compiler. Ok -- one more try in sh3b.c:

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

int i, j, seconds;

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

main()
{
  int k;
  seconds = 0;

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

  for (j = 0; j < 2000; j++) {
    for (i = 0; i < 1000000; i++) k = 1-k;
  }
  printf("%d %d %d\n", j, i, k);
}

UNIX> gcc -O3 sh3b.c
UNIX> time a.out
1 second  just passed: j =    0.  i =      0
2000 1000000 0
1.630u 0.000s 0:01.62 100.6%	0+0k 0+0io 0pf+0w
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 sh3c.c
UNIX> time a.out
1 second  just passed: j =  482.  i = 913429
2 seconds just passed: j =  965.  i = 918621
3 seconds just passed: j = 1420.  i = 631626
4 seconds just passed: j = 1902.  i = 823540
2000 1000000 0
4.210u 0.000s 0:04.20 100.2%	0+0k 0+0io 0pf+0w
UNIX> 
You'll note that the program takes 2.5 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.