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 module-2 repository.

Open file

Open the file module-2/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 used to be changed 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
A program 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 moudule-2/mandatory/src directory.

Compile all programs

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.

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.

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.

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

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. 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.

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 the kill 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 signal SIGINT to the process, or by pressing Ctrl+C from the terminal?

Bug?

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);.

Volatile

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, 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 pressing Ctrl+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?