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.
If you haven’t done so already, you must clone the module-2 repository.
Open the file module-2/mandatory/src/signals.c
in
the source code editor of your choice.
Study the C source code.
First a number of header files are included to get access to a few functions and constants from the C Standard library.
A global variable done
is initialized to false
.
bool done = false;
Later this variable is going to be used to be changed by a signal handler.
The divide_by_zero
function attempts to divide by zero.
The function segfault
attempts to dereference a NULL pointer
causing a segmentation fault.
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.
All C programs starts to execute in the main
function.
I'm done!
on a separate line
to the terminal.EXIT_SUCCESS
defined in stdlib.h.Let’s repeat the differences between a program, an executable and a process.
The make build tool is used together with the Makefile to compile all programs in
the moudule-2/mandatory/src
directory.
From a terminal, navigate to the module-2/mandatory
directory. To compile all
programs, type make
and press enter.
$ make
When compiling, make
places all executables in the bin
directory.
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.
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).
In C, //
is used to start a comment reaching to the end of the line.
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.
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
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
.
A program can install a signal handler using the signal
function.
signal(sig, handler);
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
.
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!
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
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
.
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.
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!
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.
To terminate the process, press Ctrl+C
in the terminal. Note that once the
process terminates you get the terminal prompt back.
My PID = 82249
^C
$
Asynchronous signals
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.
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.
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!
$
Add code to make the program print “Hello!” when receiving the SIGUSR1
signal.
SIGUSR1
signal to the process from the other terminal using the
kill
command where you replace <PID>
with the PID of the blocked process.$ kill -s SIGUSR1 <PID>
How can you make the program print Hello!
every time the signal SIGUSR1
is
received without terminating?
In the signal_hanlder
function, set the global variable done
to true
when handling the
SIGINT
signal.
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 and run the program from one terminal and send signals to the process from the other terminal.
SIGUSR1
signals to the process?while
loop and terminate the process by sending the signal SIGINT
to the
process, or by pressing Ctrl+C
from the terminal?Depending on your compiler the program may not break out of the while(!done)
loop
An optimizing compiler
An optimizing compiler may detect that the variable done
is not changed in the
while(!done);
loop and replace the loop with if (!false);
.
Do you remember the volatile keyword?
Volatile
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 and run the program from one terminal and send signals to the process from the other terminal.
SIGUSR1
signals to the process.SIGINT
to the
process, or by pressing Ctrl+C
from the terminal.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
.
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?