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 theSIGTERM
to a process specified in the argument, and the process terminates. You can also sendSIGKILL
to kill a process through the same command if the process ignoresSIGTERM
. - 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 receivesSIGINT
. - Old buddy GDB: Yes, the working of GDB is totally based on the signals
SIGSTOP
andSIGCONT
. When the process being debugged reaches a breakpoint, GDB sendsSIGSTOP
to a given process and its execution halts. Now, after looking at the available information, when a user makes the process run, GDB sends outSIGCONT
which clearsSIGSTOP
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 getsSIGALARM
. - 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 throughSIGCHLD
. - 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 receivesSIGFPE
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:
- A process might wait for an indefinite period of time when it invokes
sigsuspend()
but does not receive the desired signal. - Due to a timing mismatch, a process might wait for a signal that has already occurred.
- The child process inherits the same signal handlers from the parent after
fork()
. - 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.
Nice article…Thanks
Also, we can’t block the SIGKILL and SIGSTOP signals,