Inter Process Communication - Signals


Advertisements


A signal is a notification to a process indicating the occurrence of an event. Signal is also called software interrupt and is not predictable to know its occurrence, hence it is also called an asynchronous event.

Signal can be specified with a number or a name, usually signal names start with SIG. The available signals can be checked with the command kill –l (l for Listing signal names), which is as follows −

Signal

Whenever a signal is raised (either programmatically or system generated signal), a default action is performed. What if you don’t want to perform the default action but wish to perform your own actions on receiving the signal? Is this possible for all the signals? Yes, it is possible to handle the signal but not for all the signals. What if you want to ignore the signals, is this possible? Yes, it is possible to ignore the signal. Ignoring the signal implies neither performing the default action nor handling the signal. It is possible to ignore or handle almost all the signals. The signals which can’t be either ignored or handled/caught are SIGSTOP and SIGKILL.

In summary, the actions performed for the signals are as follows −

  • Default Action
  • Handle the signal
  • Ignore the signal

As discussed the signal can be handled altering the execution of default action. Signal handling can be done in either of the two ways i.e., through system calls, signal() and sigaction().

#include <signal.h>

typedef void (*sighandler_t) (int);
sighandler_t signal(int signum, sighandler_t handler);

The system call signal() would call the registered handler upon generation of signal as mentioned in signum. The handler can be either one of the SIG_IGN (Ignoring the Signal), SIG_DFL (Setting signal back to default mechanism) or user-defined signal handler or function address.

This system call on success returns the address of a function that takes an integer argument and has no return value. This call returns SIG_ERR in case of error.

Though with signal() the respective signal handler as registered by the user can be called, fine tuning such as masking the signals that should be blocked, modifying the behavior of a signal, and other functionalities are not possible. This are possible using sigaction() system call.

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact)

This system call is used to either examine or change a signal action. If the act is not null, the new action for signal signum is installed from the act. If oldact is not null, the previous action is saved in oldact.

The sigaction structure contains the following fields −

Field 1 − Handler mentioned either in sa_handler or sa_sigaction.

void (*sa_handler)(int);
void (*sa_sigaction)(int, siginfo_t *, void *);

The handler for sa_handler specifies the action to be performed based on the signum and with SIG_DFL indicating default action or SIG_IGN to ignore the signal or pointer to a signal handling function.

The handler for sa_sigaction specifies the signal number as the first argument, pointer to siginfo_t structure as the second argument and pointer to user context (check getcontext() or setcontext() for further details) as the third argument.

The structure siginfo_t contains signal information such as the signal number to be delivered, signal value, process id, real user id of sending process, etc.

Field 2 − Set of signals to be blocked.

int sa_mask;

This variable specifies the mask of signals that should be blocked during the execution of signal handler.

Field 3 − Special flags.

int sa_flags;

This field specifies a set of flags which modify the behavior of the signal.

Field 4 − Restore handler.

void (*sa_restorer) (void);

This system call returns 0 on success and -1 in case of failure.

Let us consider a few sample programs.

First, let us start with a sample program, which generates exception. In this program, we are trying to perform divide by zero operation, which makes the system generate an exception.

/* signal_fpe.c */
#include<stdio.h>

int main() {
   int result;
   int v1, v2;
   v1 = 121;
   v2 = 0;
   result = v1/v2;
   printf("Result of Divide by Zero is %d\n", result);
   return 0;
}

Compilation and Execution Steps

Floating point exception (core dumped)

Thus, when we are trying to perform an arithmetic operation, the system has generated a floating point exception with core dump, which is the default action of the signal.

Now, let us modify the code to handle this particular signal using signal() system call.

/* signal_fpe_handler.c */
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>

void handler_dividebyzero(int signum);

int main() {
   int result;
   int v1, v2;
   void (*sigHandlerReturn)(int);
   sigHandlerReturn = signal(SIGFPE, handler_dividebyzero);
   if (sigHandlerReturn == SIG_ERR) {
      perror("Signal Error: ");
      return 1;
   }
   v1 = 121;
   v2 = 0;
   result = v1/v2;
   printf("Result of Divide by Zero is %d\n", result);
   return 0;
}

void handler_dividebyzero(int signum) {
   if (signum == SIGFPE) {
      printf("Received SIGFPE, Divide by Zero Exception\n");
      exit (0);
   } 
   else
      printf("Received %d Signal\n", signum);
      return;
}

Compilation and Execution Steps

Received SIGFPE, Divide by Zero Exception

As discussed, signals are generated by the system (upon performing certain operations such as divide by zero, etc.) or the user can also generate the signal programmatically. If you want to generate signal programmatically, use the library function raise().

Now, let us take another program to demonstrate handling and ignoring the signal.

Assume that we have raised a signal using raise(), what happens then? After raising the signal, the execution of the current process is stopped. Then what happens to the stopped process? There can be two scenarios – First, continue the execution whenever required. Second, terminate (with kill command) the process.

To continue the execution of the stopped process, send SIGCONT to that particular process. You can also issue fg (foreground) or bg (background) commands to continue the execution. Here, the commands would only re-start the execution of the last process. If more than one process is stopped, then only the last process is resumed. If you want to resume the previously stopped process, then resume the jobs (using fg/bg) along with job number.

The following program is used to raise the signal SIGSTOP using raise() function. Signal SIGSTOP can also be generated by the user press of CTRL + Z (Control + Z) key. After issuing this signal, the program will stop executing. Send the signal (SIGCONT) to continue the execution.

In the following example, we are resuming the stopped process with command fg.

/* signal_raising.c */
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>

int main() {
   printf("Testing SIGSTOP\n");
   raise(SIGSTOP);
   return 0;
}

Compilation and Execution Steps

Testing SIGSTOP
[1]+ Stopped ./a.out
./a.out

Now, enhance the previous program to continue the execution of the stopped process by issuing SIGCONT from another terminal.

/* signal_stop_continue.c */
#include<stdio.h>
#include<signal.h>
#include <sys/types.h>
#include <unistd.h>

void handler_sigtstp(int signum);

int main() {
   pid_t pid;
   printf("Testing SIGSTOP\n");
   pid = getpid();
   printf("Open Another Terminal and issue following command\n");
   printf("kill -SIGCONT %d or kill -CONT %d or kill -18 %d\n", pid, pid, pid);
   raise(SIGSTOP);
   printf("Received signal SIGCONT\n");
   return 0;
}

Compilation and Execution Steps

Testing SIGSTOP
Open Another Terminal and issue following command
kill -SIGCONT 30379 or kill -CONT 30379 or kill -18 30379
[1]+ Stopped ./a.out

Received signal SIGCONT
[1]+ Done ./a.out

In another terminal

kill -SIGCONT 30379

So far, we have seen the program which handles the signal generated by the system. Now, let us see the signal generated through program (using raise() function or through kill command). This program generates signal SIGTSTP (terminal stop), whose default action is to stop the execution. However, since we are handling the signal now instead of default action, it will come to the defined handler. In this case, we are just printing the message and exiting.

/* signal_raising_handling.c */
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>

void handler_sigtstp(int signum);

int main() {
   void (*sigHandlerReturn)(int);
   sigHandlerReturn = signal(SIGTSTP, handler_sigtstp);
   if (sigHandlerReturn == SIG_ERR) {
      perror("Signal Error: ");
      return 1;
   }
   printf("Testing SIGTSTP\n");
   raise(SIGTSTP);
   return 0;
}

void handler_sigtstp(int signum) {
   if (signum == SIGTSTP) {
      printf("Received SIGTSTP\n");
      exit(0);
   }
   else
      printf("Received %d Signal\n", signum);
      return;
}

Compilation and Execution Steps

Testing SIGTSTP
Received SIGTSTP

We have seen the instances of performing default action or handling the signal. Now, it is time to ignore the signal. Here, in this sample program, we are registering the signal SIGTSTP to ignore through SIG_IGN and then we are raising the signal SIGTSTP (terminal stop). When the signal SIGTSTP is being generated that would be ignored.

/* signal_raising_ignoring.c */
#include<stdio.h>
#include<signal.h>
#include<stdlib.h>

void handler_sigtstp(int signum);

int main() {
   void (*sigHandlerReturn)(int);
   sigHandlerReturn = signal(SIGTSTP, SIG_IGN);
   if (sigHandlerReturn == SIG_ERR) {
      perror("Signal Error: ");
      return 1;
   }
   printf("Testing SIGTSTP\n");
   raise(SIGTSTP);
   printf("Signal SIGTSTP is ignored\n");
   return 0;
}

Compilation and Execution Steps

Testing SIGTSTP
Signal SIGTSTP is ignored

So far, we have observed that we have one signal handler to handle one signal. Can we have a single handler to handle multiple signals? The answer is Yes. Let us consider this with a program.

The following program does the following −

Step 1 − Registers a handler (handleSignals) to catch or handle signals SIGINT (CTRL + C) or SIGQUIT (CTRL + \)

Step 2 − If the user generates signal SIGQUIT (either through kill command or keyboard control with CTRL + \), the handler simply prints the message as return.

Step 3 − If the user generates signal SIGINT (either through kill command or keyboard control with CTRL + C) first time, then it modifies the signal to perform default action (with SIG_DFL) from next time.

Step 4 − If the user generates signal SIGINT second time, it performs a default action, which is the termination of program.

/* Filename: sigHandler.c */
#include<stdio.h>
#include<unistd.h>
#include<signal.h>

void handleSignals(int signum);

int main(void) {
   void (*sigHandlerInterrupt)(int);
   void (*sigHandlerQuit)(int);
   void (*sigHandlerReturn)(int);
   sigHandlerInterrupt = sigHandlerQuit = handleSignals;
   sigHandlerReturn = signal(SIGINT, sigHandlerInterrupt);
   if (sigHandlerReturn == SIG_ERR) {
      perror("signal error: ");
      return 1;
   }
   sigHandlerReturn = signal(SIGQUIT, sigHandlerQuit);
   
   if (sigHandlerReturn == SIG_ERR) {
      perror("signal error: ");
      return 1;
   }
   while (1) {
      printf("\nTo terminate this program, perform the following: \n");
      printf("1. Open another terminal\n");
      printf("2. Issue command: kill %d or issue CTRL+C 2 times (second time it terminates)\n", getpid());
      sleep(10);
   }
   return 0;
}

void handleSignals(int signum) {
   switch(signum) {
      case SIGINT:
      printf("\nYou pressed CTRL+C \n");
      printf("Now reverting SIGINT signal to default action\n");
      signal(SIGINT, SIG_DFL);
      break;
      case SIGQUIT:
      printf("\nYou pressed CTRL+\\ \n");
      break;
      default:
      printf("\nReceived signal number %d\n", signum);
      break;
   }
   return;
}

Compilation and Execution Steps

To terminate this program, perform the following:
1. Open another terminal
2. Issue command: kill 74 or issue CTRL+C 2 times (second time it terminates)
^C
You pressed CTRL+C
Now reverting SIGINT signal to default action
          
To terminate this program, perform the following:
1. Open another terminal
2. Issue command: kill 74 or issue CTRL+C 2 times (second time it terminates)
^\You pressed CTRL+\
To terminate this program, perform the following:
1. Open another terminal
2. Issue command: kill 120
Terminated

Another Terminal

kill 71

Second Method

To terminate this program, perform the following:
1. Open another terminal
2. Issue command: kill 71 or issue CTRL+C 2 times (second time it terminates)
^C
You pressed CTRL+C
Now reverting SIGINT signal to default action

To terminate this program, perform the following:
1. Open another terminal
2. Issue command: kill 71 or issue CTRL+C 2 times (second time it terminates)
^C

We know that to handle a signal, we have two system calls i.e., either signal() or sigaction(). Till now we have seen with signal() system call, now it is time for sigaction() system call. Let us modify the above program to perform using sigaction() as follows −

/* Filename: sigHandlerSigAction.c */
#include<stdio.h>
#include<unistd.h>
#include<signal.h>

void handleSignals(int signum);

int main(void) {
   void (*sigHandlerReturn)(int);
   struct sigaction mysigaction;
   mysigaction.sa_handler = handleSignals;
   sigemptyset(&mysigaction.sa_mask);
   mysigaction.sa_flags = 0;
   sigaction(SIGINT, &mysigaction, NULL);
   
   if (mysigaction.sa_handler == SIG_ERR) {
      perror("signal error: ");
      return 1;
   }
   mysigaction.sa_handler = handleSignals;
   sigemptyset(&mysigaction.sa_mask);
   mysigaction.sa_flags = 0;
   sigaction(SIGQUIT, &mysigaction, NULL);
   
   if (mysigaction.sa_handler == SIG_ERR) {
      perror("signal error: ");
      return 1;
   }
   while (-1) {
      printf("\nTo terminate this program, perform either of the following: \n");
      printf("1. Open another terminal and issue command: kill %d\n", getpid());
      printf("2. Issue CTRL+C 2 times (second time it terminates)\n");
      sleep(10);
   }
   return 0;
}

void handleSignals(int signum) {
   switch(signum) {
      case SIGINT:
      printf("\nYou have entered CTRL+C \n");
      printf("Now reverting SIGINT signal to perform default action\n");
      signal(SIGINT, SIG_DFL);
      break;
      case SIGQUIT:
      printf("\nYou have entered CTRL+\\ \n");
      break;
      default:
      printf("\nReceived signal number %d\n", signum);
      break;
   }
   return;
}

Let us see the compilation and execution process. In the execution process, let us see issue CTRL+C twice, remaining checks/ways (as above) you can try for this program as well.

Compilation and Execution Steps

To terminate this program, perform either of the following:
1. Open another terminal and issue command: kill 3199
2. Issue CTRL+C 2 times (second time it terminates)
^C
You have entered CTRL+C
Now reverting SIGINT signal to perform default action
To terminate this program, perform either of the following:
1. Open another terminal and issue command: kill 3199
2. Issue CTRL+C 2 times (second time it terminates)
^C


Advertisements