Watch Out for the Signals!

1
47

We say people are clever when they understand the ‘signals’ in real life. The case is the same with Linux too. The right signals sent across at the right time in the system make it fast and responsive. This article throws light on this ‘signals’ framework in the Linux system, and explores how systems programmers can make use of it.

The framework

The motivation behind this framework of ‘signals’ is to make the process aware that something has happened in the system, and the target process should perform some predefined set of actions to keep the system running smoothly. These actions range from ‘self-termination’ to ‘clean-up’.

The concept of ‘signals’ and ‘signal handling’ is analogous to that of the ‘interrupt’ handling done by a microprocessor. When a microprocessor receives an interrupt, it typically jumps to a fixed location (called the ‘vector’ location for a given interrupt). Similarly, in Linux, a process receiving a ‘signal’, typically invokes a specific function registered with the signal, called the ‘handler’ for a given signal.

There could be multiple entities that can send a ‘signal’ to a given process. The process can send a signal to itself; other running processes can send the signals to a given process or the signals could be sent to a given process by the Linux kernel too.

As defined in include/signal.h, each signal has a specific name starting with ‘SIG’ and a unique number starting from 1. Each signal also has a default action associated with it. It could be either of the four mentioned below:

  • exit: Makes the process exit
  • core: Forces the process to exit and create a core dump file
  • stop: Stops/suspends the process
  • ignore: No action taken

The framework is also flexible enough, so you can change the default disposition of a signal to one of your choice by overriding the default signal handler.

The framework also allows a given process to block some signals so that they don’t get delivered to the process at all.

One restriction imposed here by the system is that the process cannot change the default disposition for SIGKILL and SIGSTOP.

Practical applications

This section talks about the various standard applications based on ‘signals’ in a typical Linux system.

  • IPC 302: Yes! This is the basic one, or the kill command. It sends the SIGTERM to a process specified in the argument, and the process terminates. You can also send SIGKILL to kill a process through the same command if the process ignores SIGTERM.
  • Ctrl+C: When you press Ctrl+C on the keyboard, the process running on the foreground on the given terminal receives SIGINT. The process also terminates by default when it receives SIGINT.
  • Old buddy GDB: Yes, the working of GDB is totally based on the signals SIGSTOP and SIGCONT. When the process being debugged reaches a breakpoint, GDB sends SIGSTOP to a given process and its execution halts. Now, after looking at the available information, when a user makes the process run, GDB sends out SIGCONT which clears SIGSTOP and lets the process go ahead.
  • Alarms: When an application wants to run the timers, they typically make use of APIs like setitimer() specifying the time out value in the arguments. When the timer expires, the process gets SIGALARM.
  • Child care: In the Linux system, process creations are done through the fork() system call and the processes have a parent-child relationship. Now parents need to be informed when a child changes its state so that they can take appropriate action, such as doing a cleanup, spawning one more child if one gets killed, etc. This functionality is achieved through SIGCHLD.
  • Broken pipe: In Linux, processes pass the data to other processes through pipes/fifos/sockets, etc. When a process attempts to write to a broken pipe, the process receives SIGPIPE indicating the same.
  • Check your Memory/Math/Instruction set: When a process attempts to access an invalid memory address, it receives a SIGSEGV from the system. Similarly, when a program attempts to execute invalid floating point computation, it receives SIGFPE for it. Also, if a process attempts to execute illegal instruction, it gets SIGILL indicating the same.

By default, these signals also result in programs to crash with a core dump.

Something left for programmers: There are two signals SIGUSR1 and SIGUSR2, which are left for the programmers, and their meanings need to be set by them.

API support for signal handling

There are two main APIs available for programmers to change the default disposition of the signals. The first is signal(), which looks like what’s shown below:

     void (* signal (int sig, void (*func)(int)))(int);

The equivalent typedef’d version for the same, which is easier to read, is as follows:

     typedef void (*sig_t) (int);
     sig_t  signal(int sig, sig_t func);

The function is very simple to use. You only need to specify the signal number and call-back function that needs to be registered. But the API is getting deprecated, and a more robust and elaborate API called sigaction() is available:

     int sigaction(int sig, const struct sigaction  *act,  struct
     sigaction *oact);

The sigaction structure includes the following members:

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

The function could be used to get and modify the disposition for a specified signal.

The sigprocmask() API is used to block/unblock a specified signal and sigsuspend() is used to suspend the process till it receives a specified signal or the process gets killed.

The sigaddset(), sigdelset(), sigemptyset(), sigfillset(), sigismember() are the auxiliary functions available and should be used to operate on sigset_t.

A piece of code to catch SIGINT

Here is a simple piece of code to catch SIGINT:

/********************* SigInt.c ***********************/
#include
#include
#include
void SigIntHandler(int sig)
{
    printf(“Received signal %dn”, sig);
}
int main()
{
    struct sigaction act;
    int count = 5;
    act.sa_handler = SigIntHandler;
    sigemptyset(&act.sa_mask);
    act.sa_flags = 0;
    sigaction(SIGINT, &act, 0);
  while(count--) {
    printf(“Looping!n”);
    sleep(5);
  }

Here we compile and run the code:

# gcc SigInt.c
#./a.out
Looping!
Looping!
Received signal 2
Looping!
Looping!
Received signal 2
Looping!
#

Watch for some pitfalls

The following are some points to be considered during the design time:

  1. A process might wait for an indefinite period of time when it invokes sigsuspend() but does not receive the desired signal.
  2. Due to a timing mismatch, a process might wait for a signal that has already occurred.
  3. The child process inherits the same signal handlers from the parent after fork().
  4. The final and most important point is that the signal handlers should be short and reentrant. A nice article about signals and reentrancy is available at IBM Developer Works.

Here onwards

This article talks about the basics of the ‘signals’ framework on Linux. Some more advanced and interesting stuff is available in a Linux Journal article on signals.

1 COMMENT

LEAVE A REPLY

Please enter your comment!
Please enter your name here