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.
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.
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>
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 chat server lab:
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.
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.
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>