Signals
Mandatory assignment
Signals are a limited form of inter-process communication (IPC), typically used in Unix, Unix-like, and other POSIX-compliant operating systems. 1 A signal is used to notify a process of an synchronous or asynchronous event.
When a signal is sent, the operating system interrupts the target process' normal flow of execution to deliver the signal. If the process has previously registered a signal handler, that routine is executed. Otherwise, the default signal handler is executed. 1
Each signal is represented by an integer value. Instead of using the numeric values directly, the named constants defined in signals.h should be used.
Clone repository
If you haven’t done so already, you must clone the processes-and-ipc repository.
Open file
Open the file mandatory/src/signals.c
in
the source code editor of your choice.
Study the source code
Study the C source code.
Header files
First a number of header files are included to get access to a few functions and constants from the C Standard library.
Global variable done
A global variable done
is initialized to false
.
bool done = false;
Later this variable is going to be updated by a signal handler.
divide_by_zero
The divide_by_zero
function attempts to divide by zero.
segfault
The function segfault
attempts to dereference a NULL pointer
causing a segmentation fault.
signal_handler
The signal_handler
function will handle signals sent to
the process. A switch statement is used to determine which
signal has been received. An alternative is to use one signal handling function for each signal
but here a single signal handling function is used.
main
All C programs starts to execute in the main
function.
- The process ID (PID) is obtained using the getpid function and printed to the terminal with printf.
- A number of lines are commented out, we’ll get back to these later.
- The function puts is used to print the string
I'm done!
on a separate line to the terminal. - Finally, exit is used to terminate the process with exit status
EXIT_SUCCESS
defined in stdlib.h.
Program, executable and process
Let’s repeat the differences between a program, an executable and a process.
- Program
- A set of instructions which is in human readable format. A passive entity stored on secondary storage.
- Executable
- A compiled form of a program including machine instructions and static data that a computer can load and execute. A passive entity stored on secondary storage.
- Process
- An excutable loaded into memory and executing or waiting. A process typically executes for only a short time before it either finishes or needs to perform I/O (waiting). A process is an active entity and needs resources such as CPU time, memory etc to execute.
The make build tool
The make build tool is used together with the Makefile to compile all programs in
the mandatory/src
directory.
Compile all programs
From a terminal, navigate to the mandatory
directory. To compile all
programs, type make
and press enter.
make
When compiling, make
places all executables in the bin
directory.
First test run
Run the signals
program.
./bin/signals
You should now see output similar to this in the terminal.
My PID = 81430
I'm done!
Note that the PID value you see will be different.
New process
Run the program a few times. Note that each time you run the same program the process used to execute the program gets a new process ID (PID).
C comments
In C, //
is used to start a comment reaching to the end of the line.
Division by zero
To make the program divide by zero, uncomment the following line.
// divide_by_zero();
Compile with make
.
make
Run the program.
./bin/signals
In the terminal you should see something similar to this.
My PID = 81836
[2] 81836 floating point exception ./bin/signals
Division by zero causes an exception. When the OS handles the exception it sends
the SIGFPE
(fatal arithmetic error) signal to the process executing the division by zero. The default
handler for the SIGFPE
signal terminates the process and this is exactly
what happened here.
On some versions of Mac hardware, integer division by zero does not cause a
SIGFPE
signal to be sent, instead a SIGILL
(illegal instruction) signal
is sent. On other combinations of Mac hardware and C compiler, some other
signal might be sent, or no signal is sent at all.
If you run on Mac hardware and the process does not terminate when dividing by zero, you can:
- try to catch the
SIGILL
signal instead of theSIGFPE
signal - or, you could simply skip the whole division by zero part of this assignment.
Read more:
Run the program a few times. Each time you run the program the same error
(division by zero) happens, causing an exception, causing the OS to send the
process the SIGFPE
signal, causing the process to terminate.
Synchronous signals are delivered to the same process that performed the operation that caused the
signal. Division by zero makes the OS send the process the synchronous signal
SIGFPE
.
Installing a signal handler
A program can install a signal handler using the signal
function.
signal(sig, handler);
- sig
- The signal you want to specify a signal handler for.
- handler
- The function you want to use for handling the signal.
Handling SIGFPE
Uncomment the following line to install the signal_handler
function as the
signal handler for the SIGFPE
signal.
// signal(SIGFPE, signal_handler);
Compile with make
.
make
Run the program.
./bin/signals
In the terminal you should see something similar to this.
My PID = 81979
Caught SIGFPE: arithmetic exception, such as divide by zero.
This time the signal doesn’t terminate the process immediately. When the process
receives the SIGFPE
signal the function signal_handler
is executed with the
signal number as argument. After printing a message to the terminal the signal
handler terminates the process with status EXIT_FAILURE
.
No more division by zero
Comment out the following line.
divide_by_zero();
Compile and run the program Make sure you see output similar to this in the terminal.
My PID = 82040
I'm done!
Segfault
A segmentation fault (aka segfault) are caused by a program trying to read or write an illegal memory location. To make the program cause a segfault, uncomment the following line.
// segfault();
Compile with make
.
make
Run the program.
./bin/signals
In the terminal you should see something similar to this.
My PID = 82084
[2] 82084 segmentation fault ./bin/signals
The illegal memory access causes an exception. When the OS handles the exception it sends
the SIGSEGV
signal to the process executing the illegal memory access. The default
handler for the SIGSEGV
signal terminates the process and this is exactly
what happened here.
Run the program a few times. Each time you run the program the same error
(illegal memory access) happen, causing an exception, causing the OS to send the
process the SIGSEGV
signal, causing the process to terminate.
Synchronous signals are delivered to the same process that performed the operation that caused the
signal. An illegal memory access makes the OS send the process the synchronous signal
SIGSEGV
.
Handling SIGSEGV
Add code to install the function signal_handler
as the signal handler for the
SIGSEGV
signal.
When you run the program you should output similar to this in the terminal.
My PID = 82161
Caught SIGSEGV: segfault.
No more segfault
Comment out the following line.
segfault();
Compile and run the program Make sure you see output similar to this in the terminal.
My PID = 82040
I'm done!
Wait for a signal
The pause
function is used to block a process until it receives a signal (any
signal will do).
Uncomment the following line.
// pause();
Compile and run the program. You should see output similar to this in the terminal.
My PID = 82249
The process is now blocked, waiting for any signal to be sent to the process.
Ctrl+C
To terminate the process, press Ctrl+C
in the terminal.
My PID = 82249
^C
Note that once the process terminates you get the terminal prompt back.
Asynchronous signals are generated by an event external to a running process.
Pressing Ctrl+C
is an external event causing the OS to send the asynchronous SIGINT
(terminal interrupt) signal to the process.
The default signal SIGINT
handler terminates the process.
Handling SIGINT
Add code to install the function signal_handler
as the signal handler for the
SIGINT
signal.
When you run the program the process blocks waiting for any signal. When you
press Ctrl+C
you should now see output similar to this in the terminal.
My PID = 82477
^CCaught SIGINT: interactive attention signal, probably a ctrl+c.
I'm done!
Open a second terminal
Open a second terminal.
Sending signals from the terminal
Compile and run the program in one of the terminals. The program should block waiting for any signal. Note the PID of the blocked process.
My PID = 82629
The command kill
can be used to send signals to processes from the terminal.
To send the SIGINT
signal to the blocked process, execute the following command in
the terminal where you replace <PID>
with the PID of the blocked process.
kill -s INT <PID>
In the other terminal you should now see the blocked process execute the signal
handler, then continue in main
after pause()
, print I'm done!
and terminate.
My PID = 82629
Caught SIGINT: interactive attention signal, probably a ctrl+c.
I'm done!
Handle SIGUSR1
Add code to make the program print “Hello!” when receiving the SIGUSR1
signal.
- Compile and run the program from one terminal.
- Send the
SIGUSR1
signal to the process from the other terminal using thekill
command where you replace<PID>
with the PID of the blocked process.
kill -s SIGUSR1 <PID>
Don’t terminate on SIGUSR1
How can you make the program print Hello!
every time the signal SIGUSR1
is
received without terminating?
Set the global variable done to true
In the signal_hanlder
function, set the global variable done
to true
when handling the
SIGINT
signal.
Block until done
In main
, replace the line:
pause();
, with the following while loop:
while (!done);
In C !
is the logical not operator. This while
loop repeatedly checks the global
variable done
until it becomes true
.
Compile, run and test
Compile and run the program from one terminal and send signals to the process from the other terminal.
- Are you able to send multiple
SIGUSR1
signals to the process? - Are you able to break out of the
while
loop and terminate the process by sending the signalSIGINT
to the process, or by pressingCtrl+C
from the terminal?
Bug?
Depending on your compiler the program may not break out of the while(!done)
loop
An optimizing compiler may detect that the variable done
is not changed in the
while(!done);
loop and replace the loop with if (!false);
.
Volatile
Do you remember the volatile keyword?
The volatile
keyword is used to make sure that the contents of a variable
is always read from memory.
Make the global variable done
volatile.
Compile, run and test
Compile and run the program from one terminal and send signals to the process from the other terminal.
- Make sure you are able to send multiple
SIGUSR1
signals to the process. - Make sure you can terminate the process by sending the signal
SIGINT
to the process, or by pressingCtrl+C
from the terminal.
sig_atomic_t
The data type sig_atomic_t
guarantees that reading and writing a variable happen in a single
instruction, so thereās no way for a signal handler to run āin the middleā of an
access. In general, you should always make any global variables changed by a
signal handler be of the data type sig_atomic_t
.
Change the datatype of the global variable done
from bool
to sig_atomic_t
.
Use pause instead
Using a while
loop to repeatedly check the global variable done
is not a
very efficient use of the CPU. A better way is to change the loop to:
while (pause()) {
if (done) break;
};
Why is this more efficient?
Code grading questions
Here are a few examples of questions that you should be able to answer, discuss and relate to the source code of you solution during the code grading.
- What is a signal?
- How do signals relate to exceptions and interrupts?
- What is a signal handler?
- How do you register a signal handler?
- What happens if you donāt register a signal handler?
- What causes a segfault?
- What is meant by a synchronous signal?
- What do the systemcall pause() do?
- What happens when you press Ctrl-C in the controlling terminal of a process?
- How do you send signals to other processes?
- Why is the keyword volatile needed when declaring the global variable done?
- Why is the datatype sig_atomic_t needed when declaring the global variable done?
- Why is it more efficient to use pause() instead of simply loop and check the done variable?