IPC using signal () and kill



What’s there to C?

As mentioned in the previous blog, we are going to take a look at an IPC mechanism that is provided in the Linux/UNIX environment. This mechanism involves a function called signal () and a command called kill (kill is also available as a system call for use in programs).

 

First things first

In the Linux/UNIX world, each process that is executed will be uniquely identified by a process identifier (process ID). This is usually referred to as the pid of the process. This pid is an unsigned int that has been type-defined as pid_t.

All operating systems provide multiple mechanisms that can be used by processes to communicate with each other. Some of these IPCs are used for synchronization/mutual exclusion whereas others are used for actual communication that involves data transfer.

The most common IPCs for synchronization/mutual exclusion are semaphores, mutex, etc, and common IPCs used for actual data transfer are named pipes, shared memory, sockets, etc.

In Linux/UNIX there is another IPC mechanism called signal. A signal can be considered as a software interrupt that is used to inform a process about the occurrence of an event in its run-time environment.

 

The kill command (kill () system call)

First, we will see how we can send a signal to a process. One way to send a signal to a process is using the kill command (the other way is to use the system call kill () in a program). Using this command, we can send an integer value to another process. This integer value is referred to as a “signal”. How to use this command from the command line is discussed later.

There are about 31 predefined signals. Since the integer values representing various operating system events can be different for different environments, the 31 signals have been given names.

Some examples of these signal names are SIGHUP, SIGINT, SIGTERM, SIGSEGV etc which are used by the operating system to inform processes about various events that have occurred in their run time environments (like when a process statement results in invalid memory access, it receives a SIGSEGV signal, meaning that it has caused a segmentation violation).

There are a couple of signals that have been defined for use by programmers/users – SIGUSR1 and SIGUSR2. All the other signals that have been defined are meant to be used by the operating system, but these two signals are not used by the operating system.

As already mentioned, every process in execution will have a pid. So, the obvious way to send a signal from one process to another would be to use the kill command from the shell (the shell itself is a process). We provide the kill command with the pid of the target process and the name of the signal that has to be sent (the integer value also can be used, but then we will need to know the integer value corresponding to the signal). For example, to send the SIGUSR1 signal to a process with pid 1234 the usage would be:

 

kill -SIGUSR1 1234.

 

If we want to do the same from a program, a sample program is given below. This program will send the signal SIGUSR1 to the program whose pid is provided as a command line argument.

 

#include <stdio.h>

#include <signal.h>

 

int main (int ac, char *av[])

{

  unsigned int pid;

 

  if (ac == 2)

    sscanf (av[1], “%u”, &pid);

  else {

    printf (“Enter pid of target process: “);

    scanf (“%u”, &pid);

  }

 

  printf (“sending signal SIGUSR1 to %u\n”, pid);

  kill (pid, SIGUSR1);

 

  return 0;

}

Let the above program be called mkill.c and the executable be mkill.

Now we need a program whose pid we know and that will wait to receive the SIGUSR1 signal. The program given below will use the get_pid () system call and print it and then start an infinite loop. This program will continue to loop until it receives a signal – in this case we will be using our mkill program to send SIGUSR1 to this program.

 

#include <stdio.h>

#include <unistd.h>

 

int main ()

{

  printf (“process id is %u\n”, getpid ());

  for (;;);

 

  return 0;

}

If this program is saved as tgt.c and the executable is tgt, the result of executing the two programs from two terminals are shown below:

$ ./tgt

process id is 73261

User defined signal 1: 30

$

$ ./mkill 73261

sending signal SIGUSR1 to 73261

$

We can see the same results when we run the tgt program and send the signal using the kill command.

$ ./tgt

process id is 73840

User defined signal 1: 30

$

$ kill -SIGUSR1 73480

$

How this works is as below.

There are predefined “signal handler” functions for all signals in the operating system. In most of the cases, the default action is to print signal that was received and to exit (in some cases a “core image” creation also happens). So when SIGUSR1 is received by the process tgt, the default signal handler is called by the operating system and that signal handler function prints the identity of the signal and causes the tgt process to quit.

For most of the signals, with a couple of exceptions, we can replace with the signal handler functions with our own signal handler.

And that leads us to the signal () function.

 

How to replace the default signal handler with our own function

To tell the operating system to use our function as signal handler in place of the default handler we have to make use of a function called signal ().

(There is another better way to register signal handler functions with the operating system called sigaction also)

This function takes two arguments – the signal that we want to send and the address of the function that we want to be used in place of the default signal handler.

Once this call is made from a program, whenever that program receives the signal that has been passed as an argument to the signal () function, the signal handler function we have registered will be called by the operating system instead of the default one.

Given below is a program that uses the signal () function to register a new function sig_handler () for the signal SIGUSR1 (please note that this function will be called only when the program receives SIGUSR1. If we want to new signal handler for some other signal, we will have to register that signal handler separately. Note that this same signal handler function can be registered for multiple signals, by calling the signal () function multiple times with different signal names):

#include <stdio.h>

#include <signal.h>

#include <sys/types.h>

#include <unistd.h>

 

int *i_ptr;

int sh_val = 0;

 

void sig_handler (int sig)

{

  sh_val++;

  printf (“sig_handler: sh_val = %d\n”, sh_val);

}

 

int main (int ac, char *av[])

{

  i_ptr = &sh_val;

  signal (SIGUSR1, sig_handler);

  printf (“main: pid is %u\n”, getpid ());

 

  while (1) {

    if (*i_ptr > 0) {

      printf (“main: SIGUSR1 received. Quitting\n”);

      break;

    }

  }

 

  return 0;

}

 

In the sig_handler () function we are incrementing the value of the global variable sh_val. And in the main () function we are checking whether the value of the variable sh_val has changed (indirectly using the pointer i_ptr). Even though we are not directly calling the sig_handler () function anywhere inside the loop, the value of sh_val will be changed when we send the signal SIGUSR1 to the process using either the kill command or using our mkill program and the operating system calls the sig_handler () function.

Let us save this program as signal.c, create an executable called signal. The output of the program, when we execute it, and send the signal SIGUSR1 to it, from another terminal is shown below:

$ ./signal

main: pid is 75554

sig_handler: sh_val = 1

main: SIGUSR1 received. Quitting

$

$ kill -SIGUSR1 75554

$

In the next blog, we will see how we can use this concept of sending a signal to a process to understand the keyword volatile.

50% LikesVS
50% Dislikes

Author

  • Venu Kolathur

    Venu Kolathur is Chief Architect and Co-Founder at Vayavya Labs and has over 38 years of industry & academic experience. He is responsible for product technology road-map, and design strategies.