CS330 Interprocess Communication and Signals


Highlights of this lab:


Lab Code

To get the sample and exercise code, please use the following commands in your cs330 directory:
   curl -O -s https://www.labs.cs.uregina.ca/330/Signals/Posix/Lab10.zip 
   unzip Lab10.zip

Introduction to Signals

When is a signal generated?

When a process terminates abnormally it tries to send a signal indicating what went wrong.
C programs and UNIX can catch these signals for diagnostics.

The kernel can send signals. There are two kinds of kernel signals:

  1. Hardware Conditions--For instance SIGSEGV (the segmentation fault signal), which indicates that there has been an addressing violation.
  2. Software Conditions--For instance SIGPOLL or SIGIO (signaling that I/0 is possible on a file descriptor)

You can also send your own signals through:

  1. keyboard sequences such as CTRL-C
  2. the kill command at the shell level
  3. kill() system call in a C program

Signals vs Interrupts

Signals are software generated interrupts that are sent to a process when a event occurs.

Signals are the software version of a hardware interrupt. Just like a hardware interrupt, a signal can be blocked from being delivered in the future.

Signal Default Action

Each signal has a default action which is one of the following:

Signals are listed in a system header file:

There is also usually a man page with a list of signals and their default actions:

NOTE: Certain signals cannot be caught or ignored. These are SIGKILL and SIGSTOP.


Signals on the Shell Level

Let's experiment with signals on the shell.

To do this, we will use an infinitely looping program that prints an incrementing number every second:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

int main (void)
{
        int i;

        for( i = 0;; ++i)
        {
                printf("%i\n",i);
                sleep(1);
        }
}

We are going to experiment with keyboard sequences and the kill command.


Keyboard Sequences

Once you have compiled the above program, run it.

It will loop infinitely counting up from 0.

How can we stop it?

Try:

These keyboard sequences are actually mapped to signals. To see the current mappings of keystrokes for the interrupt and quit signals, use the following command:

stty -a

kill at the Command Line

The general format of the kill command is:

% kill [-signal] pid

When issued, the kill command will send a signal to the indicated process. The signal can be an integer or the symbolic signal name with the SIG prefix removed. If no signal is specified then SIGTERM (terminate) is sent by default.

To find the pid, you can use the ps command.

Try running the program given above and use the following signals to give you the four possible actions of a signal:

To give you a list of signals that you can use with kill use:

kill -l 

A few notes on kill:


System Calls for Signals

In addition to working with signals on the command line, we can also implement signals in C code.

We will be looking at one system call for sending signals:

We will also be looking at the POSIX signal handling interface handling and catching signals:


Sending Signals: kill()

       #include <sys/types.h>
       #include <signal.h>

       int kill(pid_t pid, int sig);

The kill system call can be used to send any signal to any process group or process.

If pid is positive, then signal sig is sent to pid.

If pid equals 0, then sig is sent to every process in the process group of the current process. (the man pages on kill)

Example:

kill(getpid(), SIGINT);

would send the interrupt signal to the id of the calling process.
-This would have an effect similar to the exit() command.

Note: kill(pid, SIGKILL); sends a signal value 9 which is a sure kill. This signal cannot be caught or ignored

 

For completeness, there is another function, called raise(), which also sends signals. However, raise() is a library function rather than a system call.

       #include <signal.h>

       int raise(int sig);

The raise() function sends a signal to the current process. It is equivalent to

       kill(getpid(), sig); 

Handling Signals:

In the beginning, we mentioned that each signal has a default action. Sometimes, you may want to overwrite this default action so that you can control what will happen when a certain signal is received.

You can overwrite the action of all signals except for SIGKILL and SIGSTOP.

Setting the Stage: Initializing a Bit Mask

To give a signal a new action, you first have to create a new signal set, which is a bit mask indicating which signal(s) you will be working with:

  1. You declare a variable of type sigset_t data type

  2. You clear the bitmask of the sigset_t variable using sigemptyset()

  3. You add a signal to the sigset_t variable using sigaddset()

The prototypes for sigemptyset and sigaddset are below.

#include <signal.h>

int sigemptyset (sigset_t *signal_set);
int sigaddset (sigset_t *signal_set, int signal_num);

These two functions return 0 on success. Otherwise, they return -1.

For instance, if you want to work with overwritting the default action of SIGINT, you would have to add that signal to a "cleared" or "emptied" signal set. The partial code might look something like this:

       //Step 1: declare your signal set variable
       sigset_t interruptMask;             

       //Step 2: clear the bitmask of the signal set variable
       sigemptyset (&interruptMask);
       
       //Step 3: add the SIGINT signal to the signal set variable
       sigaddset (&interruptMask, SIGINT);  

Of course, you would want to build in error checking to ensure that everything went well.

Ready...Set...Action

You are only half-way there. You still have to indicate what you want to do if you do receive the signal that you are interested in. This involves a further three steps:

  1. Define a special function referred to as a signal handler

  2. Fill a variable of type struct sigaction with information about that signal handler function

  3. Use the sigaction() function to bind the signal to the function (using the variable from step 2)

 

First, let's look at what the prototype for our function will look like:

static void your_own_signal_handler (int signal_number, siginfo_t *signal_info, void *context);

 

Second, let's look at what is inside a struct sigaction (that we have to fill with information about our function)

struct sigaction
{
       int sa_flags;
       void (*sa_handler) (int);
       void (*sa_sigaction) (int, siginfo_t *, void *);
       sigset_t sa_mask;
}

 

Third, let's look at the prototype for sigaction() (that will hook the signal up to our signal handler):

#include <signal.h>

int sigaction(int sig_num, const struct sigaction *act, struct sigaction *old_act);

 

To continue the example from above, let's say instead of terminating your process on a SIGINT, you want to print a message instead:

//Define what your function will do when it receives the signal
static void yourSignalHandler (int signalNo, siginfo_t *info, void *context)
{
        printf ("\nSignal %d received. \n", signalNo);
}

int main()
{
        struct sigaction act;
        //Do all the bitmask stuff for setting the stage

        //Load the sigaction structure
        act.sa_sigaction = &yourSignalHandler;
        act.sa_mask =  interruptMask;
        act.sa_flags = SA_SIGINFO;
              
        //Use sigaction to hook up the signal to the function
        sigaction (SIGINT, &act, NULL)
}              

Of course, you would want to build in error checking to ensure that everything went well.

By setting this code up, you are saying that if the process receives a signal SIGINT, then a call will be made to the user-defined function called yourSignalHandler.

A Complete Example:

/* set CTRL-C and CTRL-\ to be trapped by a function called signal_catcher */
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <stdlib.h>

static void yourSignalHandler (int signalNo, siginfo_t *info, void *context);

int main (void)
{
       int i;

       //Variables needed for signals
       sigset_t interruptMask;             
       struct sigaction act;

       //Set the stage
       sigemptyset (&interruptMask);
       sigaddset (&interruptMask, SIGINT);  
       sigaddset (&interruptMask, SIGQUIT);  

       //Hook up the signalhandler to SIGINT and SIGQUIT
       act.sa_sigaction = &yourSignalHandler;
       act.sa_mask =  interruptMask;
       act.sa_flags = SA_SIGINFO;

       if (sigaction (SIGINT, &act, NULL) == -1)
       {
               perror("sigaction cannot set SIGINT");
               exit(SIGINT);
       }
       if (sigaction (SIGQUIT, &act, NULL) == -1)
       {
               perror("sigaction can not set SIGQUIT");
               exit(SIGQUIT);
       }
       for( i = 0; ; ++i)
       {
               printf("%i\n",i);
               sleep(1);
       }
}
static void yourSignalHandler (int signalNo, siginfo_t *info, void *context)
{
        printf("\nSignal %d received. \n", signalNo);
        if (signalNo == SIGQUIT)
                exit(1);
}

Note: instead of the signal names, the following integers could have been used:

SIGINT - 2 - traps CTRL-C

SIGQUIT - 3 - traps CTRL-\


Communicating Between Processes

A process can send signals to other processes

To send a signal, use the kill command (really a misnomer in this case)

Remember there are different ways of getting process id's from within a C program:

What does the following code do?

// original code from http://jan.netcomp.monash.edu.au/OS/l8_1.html
// now available from: https://jan.newmarch.name/ProgrammingUnix/processes/lecture.html
// modified for Posix signals
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>     //added this to the original
#include <signal.h>
#include <sys/types.h>

static void intHandler (int signalNo, siginfo_t *info, void *context)
{
       printf("Ouch - shot in the ...\n");
       exit(2);
}

int main(int argc, char *argv[])
{
       pid_t pid;

       //Variables needed for signals
       sigset_t interruptMask;             
       struct sigaction act;

       if ((pid = fork()) < 0)
       {
               fprintf(stderr, "fork failed\n");
               exit(1);
       }

       if (pid == 0)
       {
	       //set the stage (the bitmask)
               sigemptyset (&interruptMask);
               sigaddset (&interruptMask, SIGINT);  

               //Hook up the signalhandler to SIGINT 
               act.sa_sigaction = &intHandler;
               act.sa_mask = interruptMask;
               act.sa_flags = SA_SIGINFO;

               if (sigaction (SIGINT, &act, NULL) == -1)
               {
                       perror("sigaction cannot set SIGINT");
                       exit(SIGINT);
               }
               while (1)
                       printf("Running amok!!!\n");
       }

       sleep(3);
       kill(pid, SIGINT);
       exit(0);
}

References

Interprocess Communications in Unix--The Nooks and Crannies by John Shapley Gray (pages 100 to 117)

man pages

Dr. Hilderman's notes