Unix Signals
Goals
- Learn to work with Unix signals. You now understand how computer systems might use the notion of interrupt to service requests from I/O devices. The concepts of asynchronous arrival of requests and of mapping specific requests to specific procedures to handle them are not exclusive to the lowest level hardware/software interface, though. This lab illustrates the point by exposing how the Unix operating system defines the concept of signal, an asynchronous mechanism for interprocess communication. Signals are be mapped to functions that serve as handlers for their reception, similarly to how interrupt requests are mapped to interrupt handlers.
- Reinforce the concept of function pointers in C. In order to define Unix signal handlers, the programmer must construct functions that follow a well specified prototype. (Remember that you have been using function pointers in you work with threads.) Once that function exists, the programmer passes its address to the operating system with a request to associate it with a specific signal. The idea of defining a data type that encapsulates a function prototype corresponds to the concept of function pointers in C. In this lab, you will get additional practice with function pointers.
Credits
This lab was developed by Prof. L. Felipe Perrone. Permission to reuse this material in parts or in its entirety is granted provided that this “credits” note is not removed. Additional students files associated with this lab, as well as any existing solutions can be provided upon request by e-mail to: perrone[at]bucknell[dot]edu
Set Up
This will be your working directory for this lab:
~/csci315/Labs/Lab10/
Copy the following file to your git repository:
~cs315/Labs/Lab10/timer.c
Background
int i;
int *myintp = &i;
void myfunc (int a, int b);
void *function_pointer (int, int);
function_pointer = myfunc;
Note that we don’t have to use the address operator & to get the address of myfunc because when the name of a function is not followed by parentheses with parameters, the compiler understands that we are referring to the address of the function.
The neat thing is that once you’ve stored the address of a function in a function pointer variable, you can use the variable to invoke the function. We just have to remember to dereference the function pointer variable. For example:
(*function_pointer) (123, 456);
The line above will call whatever function has its address stored in the function_pointer variable passing the actual parameters 123 and 456.
There are many interesting uses of this language feature; we will see one of them in this lab, which relates to the definition of functions to handle special system events or messages called signals. When a signal is received by a running program, the program deviates temporarily from its normal path of execution to invoke the function that is mapped as handler to that specific type of signal.
Signals are asynchronous “messages” to a running program and to each kind of signal we can associate a specific signal handler. In that sense, the framework for working with signals is similar to that of the framework for dealing with hardware interrupts.
You might want to read this brief tutorial on signals before going on. If you are curious about the mapping of signal names to integer numbers, you should take a look at the following file in our lab systems (the specific path and file might change for other Unix installations):
/usr/include/asm-generic/signal.h
Problem 1 (10 points)
In order to earn full credit for this problem, you must submit a Makefile to compile all the programs you are turning in, which will be called timer, clock, and user. That is, in this file, you need to have one rule for each executable you want to generate.
Problem 2 (30 points)
File timer.c is the source code of a program that installs a handler for Unix signal SIGALRM and then schedules timers to generate such signal every so often (5 second intervals). Read the manual page for the signal(2) system call to understand how this program uses it to install a signal handler. It turns out that this system call has been made outdated by a newer one called sigaction(2).
Read the manual page for sigaction(2) and then modify timer.c so to work with this newer system call.
Hint: You must use memset(3) to zero out all the bytes in any struct sigaction you define. Otherwise, you will see some non-deterministic behavior in your program. If this doesn’t make sense to you at this point, it is because you didn’t read sigaction(2)‘s manual page carefully enough.
When you are done with this, you need to:
- cd ~/csci315/Labs/Lab10
- git add Makefile
- git add timer.c
- git commit -m “Lab10, problems 1 and 2 completed”
- git push
Problem 3 (30 points)
Copy your solution to Problem 1 to a new file called clock.c. Now, modify the program so that you can take advantage of your knowledge of the passage of time to implement something like a real time clock, which only shows updates every 5 seconds. Your program should print out the value of the clock in the body of the while loop – the output will scroll by really fast, but you always be able to see the value of the clock.
When you are done with this, you need to:
- cd ~/csci315/Labs/Lab10
- git add Makefile
- git add clock.c
- git commit -m “Lab10, problem 3 completed”
- git push
Problem 4 (30 points)
Copy your solution to Problem 2 to a new file called user.c. Modify your program to install a new signal handler for the general purpose signal SIGUSR1 (which corresponds to the number 10); the body of this handler can be identical to timer_handler. Once your program compiles, launch it in your current terminal window and open a second terminal. Follow the guidelines below to test your program.
In your second terminal, run the line:
ps -ef | grep user
Assuming that you compiled your user.c to an executable called user, the line above does the following:
- Calls the system utility ps(1) to list all processes currently running in your machine,
- Sends the output of ps to another program (the “|” creates a pipe mapping the standard output of one program to the standard input of the next), and finally,
- Calls the system utility grep(1) to sift through the output and find all lines that contain the string “user”.
kill -10 pid
Instead of identifying the signal to send to the process identified by pid by a numerical value (in this case SIGUSR1=10), you could use a string. The actual string for signal 10 may vary according to the shell you use. In order to list the strings associated with each numerical signal value, you can interrogate your shell with:
kill -l
That is the letter “el” above, not the number 1! For bash, the string is exactly SIGUSR1, so you would do:
kill -s SIGUSR1 pid
For tcsh, sh, and zsh, the string is USR1, so you would do, instead:
kill -s USR1 pid
If intellectual curiosity is getting the best of you, remember that you can read the manual pages (in section 1) for the three system utilities mentioned above (that is, ps, grep, and kill.)
When you are done with this, you need to:
- cd ~/csci315/Labs/Lab10
- git add Makefile
- git add user.c
- git commit -m “Lab10, problem 4 completed”
- git push