Linux Signal Handling

Reading Time: 6 minutes

Signals are a way to convey information to the running program asynchronously. We can classify signals into two categories,

  1. Critical Signals which a program can’t escape. For example SIGKILL, SIGSEGV.
  2. Non-Critical signals which a program can choose to ignore, for example SIGCHLD.

To handle a signal we need to install a signal handler and there are two ways in which this can be done

  • Use the signal function.
  • Use the sigaction system call.

Also note that glibc’s signal function is a wrapper over the sigaction system call. The following is from the man page of both

Signal Function

 #include<signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

As can be seen, sighandler_t is the function pointer to the actual handler function and the only argument it takes is an integer which is the signal number. This argument is useful when the same handler is used for handling more than one signal. So a simple switch case can be used to execute the corresponding code.

One common case where a signal handler is often used is when a process creates child processes. If a process doesn’t call waitpid on the child process then there would be a big list of zombie processes created. This would gradually build up if the parent process is not killed and would eventually trigger OOM where the Linux kernel tries to reclaim some memory by killing processes.

Let’s see an example code using waitpid to clean-up child process’s resources without using a signal handler

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>
#include <errno.h>
 
int main()
{
        pid_t child_pid;
        int child_sleep_time = 10; /*Wait at least 10 seconds in child*/
        int exit_status = 0;
 
        srand(time(NULL));
        child_pid = fork();
        if (child_pid < 0) 
          printf("Unable to create a child process\n"); 
        else if (child_pid > 0) {
                printf("Waiting for child process to exit...\n");
                /*
                 * Block until child exits.
                 * */
                waitpid(child_pid, &exit_status, 0);
                if (WIFEXITED(exit_status))
                        printf("Child process with pid %d exited with status %d\n",
                                        child_pid, WEXITSTATUS(exit_status));
                printf("Parent is now exiting...\n");
        } else {
                child_sleep_time += rand() % 60; /*Maximum 70 seconds*/
                printf("Child process with pid %d is sleeping for %d seconds\n",
                                getpid(), child_sleep_time);
                sleep(child_sleep_time);
                printf("Child process is now exiting...\n");
        }
        return 0;
}

As can be seen the parent process just blocks waiting for the child process to exit, though we can also set the WNOHANG flag for waitpid so it doesn’t block but then we just risk busy looping which is worse than the wait.

This is where signals come in handy, in the above example the parent process can continue to do it’s work and be notified if any of it’s child process is done. This is more efficient but then has it’s own caveats as described below

  • The signal handling function can’t block. Therefore using WNOHANG becomes mandatory.
  • The signal handler function can’t use any signal handler unsafe code. Many trivial things viz, printf are NOT signal safe and thus should be avoided.
  • The signal handler can’t use locks used by the main program. This is the reason why functions like printf is not safe to call. The reason being if suppose the normal execution of the program acquired a lock and just after acquiring the lock signal handler function was called then since the program is already holding the lock it won’t be able to continue further.

 Use signal handler to reap child process

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>
#include <errno.h>
 
bool child_has_exited = false;
pid_t child_pid;
void sigchld_handler(int signum)
{
        /*
         * Reap any child process.
         * */
        if (waitpid(child_pid, NULL, WNOHANG) > 0) {
                child_has_exited = true;
        }
}
int main()
{
        int child_sleep_time = 10; /*Wait at least 10 seconds in child*/
        int exit_status = 0;
 
        srand(time(NULL));
        signal(SIGCHLD, sigchld_handler);
        child_pid = fork();
        if (child_pid < 0) 
          printf("Unable to create a child process\n"); 
        else if (child_pid > 0) {
                while (!child_has_exited) {
                        printf("Waiting for child process to exit...\n");
                        sleep(1);
                }
                printf("Parent is now exiting...\n");
        } else {
                child_sleep_time += rand() % 60; /*Maximum 70 seconds*/
                printf("Child process with pid %d is sleeping for %d seconds\n",
                                getpid(), child_sleep_time);
                sleep(child_sleep_time);
                printf("Child process is now exiting...\n");
        }
        return 0;
}

Whenever a signal handler is called then the signal currently being handled is blocked. Therefore in the above code if there were multiple processes created and a process exits while we’re currently executing the SIGCHLD handler then handler code won’t be interrupted and since signal count are not kept but only a flag of occurrence is kept the signal being handled will be delivered again once the handler code is over.

However any other signal, apart from the one currently being handled, can interrupt the signal handler code unless we setup the signal handler to be not interrupted by any specific set of signals while it’s currently being executed. Thus the signal function is not robust enough to allow for such actions to take place.

NOTE: System calls are usually interrupted when a signal is delivered to a program. Some system calls are restarted by themselves (viz waitpid) while others are not.

Using sigaction

The sigaction system call is another way to code signal handlers. This system call is more versatile in that it allows one

  1. To get more information from the kernel in addition to signal number.
  2. To block a set of signals, in addition to the one being handled when the signal handler is executed.
  3. To ask Linux kernel to restart an interrupted system call if the signal handler execution caused one to be interrupted.

Let’s take a look at the sigaction system call

#include<signal.h>
int sigaction(int signum, const struct sigaction *act,
                     struct sigaction *oldact);
struct sigaction {
               void     (*sa_handler)(int);
               void     (*sa_sigaction)(int, siginfo_t *, void *);
               sigset_t   sa_mask;
               int        sa_flags;
               void     (*sa_restorer)(void);
};

NOTE: Don’t assign both sa_handler and sa_sigaction.

The sa_mask is the set of signals to be blocked while the signal handler is being executed. Note that the signal for which the signal handler is installed is always blocked. The signals are masked using the system call sigprocmask.

NOTE: calling sigprocmask from within signal handler is not really useful. Also if you change the signal mask is restored after the signal handler is finished to whatever it was before executing the signal handler. So any changes from within the signal handler to signal mask would be lost.

Using sigaction with above code

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <signal.h>
#include <stdbool.h>
#include <errno.h>
 
bool child_has_exited = false;
bool sigalrm_is_blocked = false;
pid_t child_pid;
void sigchld_handler(int signum)
{
        sigset_t current_signal_mask;
        /*
         * Reap any child process.
         * */
        if (waitpid(child_pid, NULL, WNOHANG) > 0) {
                child_has_exited = true;
        }
        /*
         * Let's check if SIGALRM is really blocked.
         * Note that first argument is ignored if the second argument
         * is NULL. Thus we can get the current signal mask of the process
         * for inspection.
         */
         if (sigprocmask(SIG_SETMASK, NULL, &current_signal_mask) == 0) {
                /*
                 * Check if SIGALRM is part of this signal set. The set contains
                 * the signals which are currently blocked.
                 */
                 if (sigismember(&amp;current_signal_mask, SIGALRM))
                        sigalrm_is_blocked = true;
         }
}
 
int main()
{
        int child_sleep_time = 10; /*Wait at least 10 seconds in child*/
        int exit_status = 0;
        struct sigaction chld_action = {
                .sa_handler = sigchld_handler,
                /* Attempts to restart the system call post signal handler execution.*/
                .sa_flags = SA_RESTART,
        };
        /*
         * Clear the signal set first.
         */
        sigemptyset(&chld_action.sa_mask);
        /*
         * Call sigaddset to add signals to the mask.
         * Let's block SIGALRM while we're handling SIGCHLD
         */
        /*
         * To block all signals while executing signal handler,
         * use the function sigfillset instead of sigemptyset.
         */
        sigaddset(&chld_action.sa_mask, SIGALRM);
        srand(time(NULL));
        /*
         * signal(SIGCHLD, sigchld_handler);
         */
        if (sigaction(SIGCHLD, &chld_action, NULL)) {
                printf("Unable to install signal handler. Exiting..\n");
                return errno;
        }
        child_pid = fork();
        if (child_pid < 0) 
           printf("Unable to create a child process\n"); 
        else if (child_pid > 0) {
                while (!child_has_exited) {
                        printf("Waiting for child process to exit...\n");
                        sleep(1);
                }
                printf("SIGALRM was blocked when SIGCHLD was handled = %s\n",
                                (sigalrm_is_blocked ? "true" : "false"));
                printf("Parent is now exiting...\n");
        } else {
                child_sleep_time += rand() % 60; /*Maximum 70 seconds*/
                printf("Child process with pid %d is sleeping for %d seconds\n",
                                getpid(), child_sleep_time);
                sleep(child_sleep_time);
                printf("Child process is now exiting...\n");
        }
        return 0;
}

Common Strategies used when handling signals

As I discussed earlier the number of signal safe functions isn’t very large, check the list. Functions which might be needed viz, malloc, calloc, free are not part of this list. Usually if we are worrying about signal handlers it implies that the parent process would sit in a tight loop somewhere doing some processing. 

The signal handler should try to do absolute minimum so that the main program, sitting in the tight loop, can recognise what triggered the signal and act upon it later. Not only this makes signal handlers small but also allows to write more complex action functions. Some of these include

  1. Setting a flag that signal has occurred and main program should do something about it.
  2. Gather data from the signal, if any, and send it to main program using a socketpair/pipe or a message queue.
  3. This is similar to 2, but you can also share data via file.

The above are from my experience but there may be other methods which can be used. We’ll use one these ways to add a signal handling framework in my next post.

Leave a Reply