Chapter 10 in the book gives a very complete description of signals. It will make for better reading after you read these lecture notes, but you will profit from reading it nonetheless.
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 >
void cntl_c_handler(int dummy)
{
printf("You just typed cntl-c\n");
signal(SIGINT, cntl_c_handler);
}
main()
{
int i, j;
signal(SIGINT, cntl_c_handler);
for (j = 0; j < 40; 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 >
void segv_handler(int dummy)
{
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).
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. Note that 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 = 26. i = 638663 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 1 second just passed: j = 8. i = 823534 2 seconds just passed: j = 17. i = 715735 3 seconds just passed: j = 26. i = 610604 4 seconds just passed: j = 35. i = 513675 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.
Note that 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.
So, here's the output on Solaris (try it on kenner):
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 is the output on SunOS (try it on duncan):
UNIX> sh4 One second has passed: j = 7. i = 584436You 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.