Short Tutorial on Signals in Linux
Vahab Pournaghshband
Signals
Let's examine the case of power failure while a reliable process is running. When the power cable is pulled out, the power doesn't die out immediately. In fact, it takes a few milliseconds before the power is completely gone. This reliable process may need to be notified of such power failures to, for instance, save states before being forced to exit. Let's examine the possible approaches to accomplish this:
-
A bit in the file "/dev/power" would indicate the power status. In this approach, the reliable program periodically reads the file. If it reads 1, then it means the power is still on and the program would continue whatever it was doing. However, in case of reading 0, the process realizes that the power is gone and it must exit within, say, 10ms. This approach has two major disadvantages: (1) it requires all programs, that want to be reliable, to poll, and, (2) to make this to work, the applications have to incorporate this mechanism in their implementation.
-
Another approach would be reading from a pipe rather than a file. In this case, unlike the previous approach that needed to check for a change of a bit at every time interval, the process will hang until a character is written to the pipe, indicating a power failure. Clearly, this solution suffers from major drawbacks, not to mention the requirement for modification of all applications. In this approach the process is blocked while waiting for a change of power state, so the application can not execute any of its actual code. To fix this we need multithreading. In other words, a separate thread should be delegated to reading the file for a signal of power failure, to ensure that the main thread is not blocked. But now the question is that how would the waiting thread tell the main thread that there is a power failure?
-
As another approach, the kernel can save the entire RAM to the disk once it realizes that the power failure has occurred. Then, later, when the system starts again, the kernel would restore the RAM. This approach, however, is not practical, since writing to disk is extremely slow, so it may take more time to save than the system actually has left.
-
The winner approach is sending
SIGPWR
signal to all processes in case of power failures. In this approach, the kernel signals the processes of such event, and it leaves it up to the processes to do what they want to do with it.
Signal Menagerie
The following table enumerates some of the signals. All signals are defined in signal.h
.
Events | Corresponding Signals |
---|---|
Unusual Hardware Events | SIGPWR : Power failure |
Uncooperative Processes | SIGINT : Terminal interrupt signal |
Invalid Programs | SIGILL : Illegal (bad) instruction SIGFPE : Floating-point exception SIGSEGV : Segmentation violation SIGBUS : Bus error |
I/O Errors | SIGIO : Device is ready SIGPIPE : Broken pipe |
Child Process Died | SIGCHLD : Child status has changed |
User Signals | SIGKILL : Kill processes SIGSTOP : Stop processes for later resumption SIGTSTP : Suspended processes SIGUSR1 : User-defined signal 1 |
User Went Away | SIGHUP : Controlling terminal is closed |
Time Expiration | SIGALRM : Alarm clock |
How to Handle Signals?
Back to our power failure example, here is how the power failure signal is established and handled:
1 int main()
2 {
3 signal(SIGPWR, powerFailureHandler);
4 ...
5 }
6
7 void powerFailureHandler(int signum)
8 {
9 //Saves states to restore later
10 ...
11 }
The first line in main()
establishes a handler for the SIGPWR
signals. The first argument to signal
is an integer specifying what signal is referring to, while the second argument is a function pointer type which points to the signal handler.
In our example, the powerFailureHandler()
is a signal handler. A handler is a function that is executed asynchronously when a particular signal arrives. Since it interrupts the normal flow of execution, it can be called between any pair of instructions. If a handler is not defined for a particular signal, a default handler is used. The only two signals for which a handler cannot be defined are SIGKILL
and SIGSTOP
.
What is Safe to Do Inside a Signal Handler?
There are DO's and DON'T's when it comes to signal handlers. For instance, calling certain functions, called non-reentrant, could potentially lead to havoc. An example of such functions is malloc()
which allocates additional memory on heap. Recall that signals are asynchronous function calls and could be raised at any time. In the case of malloc, havoc can result for the process, if a signal occurs in the middle of allocating additional memory using malloc()
, because malloc usually maintains a linked list of all its allocated area and it may have been in the middle of changing this list. Another example of non-reentrant function calls inside signal handlers is getchar()
which reads a byte from standard input. In that case, the process could lead into an inconsistent state if it was in the middle of dealing with stdio buffer when the signal arrived. On the other hand, reentrant functions like close()
are safe to use in signal handlers.
How to Block Signals?
Sometimes we would benefit more by not having signals at all usually to avoid race conditions. Blocking a signal means telling the operating system to hold it and deliver it later. Generally, a program does not block signals indefinitely, it might as well ignore them by setting their actions toSIG_IGN
. One way to block signals is to use
sigprocmask
which its format is:
sigprocmask(int how,
sigset_t const * restrict set,
sigset_t const * restrict oset)
Where how
is either of three values: SIG_BLOCK
, SIG_UNBLOCK
, SIG_SETMASK
. The first two values specify whether the signals in the new signal mask should be blocked or not, while the last specifies that the new mask should replace the old mask. set
and oset
that hold new and original masks are both types of sigset_t
which is a bitmap that reserves one bit per signal, indicating which signal(s) are blocked. The following code is an example of blocking SIGHUP
signal while performing the string copy.
1 sigset_t newMask, oldMask;
2 sigemptyset(&newMask);
3 sigemptyset(&oldMask);
4
5 //Blocks the SIGHUP signal (by adding SIGHUP to newMask)
6 sigaddset(&newMask, SIGHUP);
7
8 sigprocmask(SIG_BLOCK, &newMask, &oldMask);
9 strcpy(tmp_file,"/tmp/foo");
10 // Restores the old mask
11 sigprocmask(SIG_SETMASK, &oldMask, NULL);
The code segment between the two sigprocmask
-s is called critical section in the operating system context.
Go Volatile on Variables:
Let's examine the following code:
1 int x;
2
3 int main()
4 {
5 x=0
6 ...
7 x=1
8 ...
9 }
x
is defined as a global variable. It is first set to 0
and later in main()
, its value is changed to 1
without involving x
in any statement between these two assignments. While the compiler is compiling this code, it replaces the x=0
statement by x=1
and removes the x=1
in line 7, since it is smart enough to realizes that there is no need for x=0
, for it is never used. This seemingly fine observation could lead to undesired behavior if the associated signal for the following signalHandler
occurs between line 5 and 7:
1 void signalHandler(...)
2 {
3 if (x)
4 unlink("f"); //remove file f
5 }
In this case, an entirely different action would be taken by the process if the compiler does the optimization at the compilation stage. This problem is fixed by telling the compiler to avoid such optimization using the volatile
keyword. volatile
is widely used in codes involving signals, and can be seen as a warning for potential race conditions. The code is then revised as follows:
1 int volatile x;
2
3 int main()
4 {
5 x=0
6 ...
7 x=1
8 ...
9 }