Linux Signal–Part 2. Signal Handler

Previous post covers the basics of Linux signals. This post illustrates how to install a Linux signal handler with examples.

Linux provides two methods to install signal handler, signal and sigaction. sigaction is the preferred method over signal because signal behaves differently on different UNIX and UNIX-like OSes and therefore less compatible than sigaction. This post covers sigaction only.

Before going to sigaction system call, we go through several functions which we’ll need later. You can also go the end of the post to read the source code first and refer to the explaination later.

sigprocmask System Call

A signal can be blocked. In this case, it is not delivered to the process until it is unblocked. After the signal is generated and before it is delivered, the signal is in pending state. sigprocmask system call is used to query blocked signals. It has the following prototype,

#include <signal.h>

int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);

how: specify how to change the block signal set. It can be one of the three values.  SIG_BLOCK: the set of blocked signal is the union of current set and the input parameter set SIG_UNBLOCK: the signals specified in set is removed from the set of blocked signals SIG_SETMASK: the set of blocked signals is set to same as the input parameter set

set: if not null, the set value is used to according to the description for how. If null, the blocked signal set is unchanged and how has no meeting.

oldset: if not null, the previous value of the signal mask is returned through oldset.

Signal Sets System Calls

The sigprocmask function returns a data structure of sigset_t type, several functions are defined to manipulate the signal set.

#include <signal.h>

int sigemptyset(sigset_t *set);

int sigfillset(sigset_t *set);

int sigaddset(sigset_t *set, int signum);

int sigdelset(sigset_t *set, int signum);

int sigismember(const sigset_t *set, int signum);

One can tell what the functions do from their names.

pause and sigsuspend System Calls

Two system calls are defined to suspend the execution of a process to wait for a signal to be caught, pause() and sigsuspend(). Their prototypes are as below,

#include <unistd.h>

int pause(void);

 

#include <signal.h>

int sigsuspend(const sigset_t *mask);

pause: Suspends execution until any signal is caught. It only returns when a signal was caught and the signal catching function returned. In this case, it returns -1 and errno is set to EINTR.

sigsuspend: Temporarily changes the signal mask and suspends execution until one of the unmasked signals is caught. The calling process’s signal maks is temporarily replaced by value given in mask input parameter until the sigsuspend returns or the process is terminated.

Note that sigsuspend always returns -1, normally with the error EINTR.

Now we explain the sigaction system call.

sigaction System Call

sigaction is a system call to query and change the actions taken when a process receives a particular signal. The sigaction system call has the following prototype,

#include <signal.h>

int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);

signum: specify the signal
act: if not NULL, the action specified by act is installed.
oldact: if not NULL, the previous action is returned.

And the data structure sigaction is defined as below,

struct sigaction {

      void     (*sa_handler)(int);

      void     (*sa_sigaction)(int, siginfo_t *, void *);

      sigset_t   sa_mask;

      int        sa_flags;

      void     (*sa_restorer)(void);

};

The parameters have the following meanings,

sa_handler: specifies the action taken to the signal, SIG_DFL (default action), SIG_IGN (ignore the signal) or a function pointer to a user defined signal handler. The defined signal handler should accepts the signal number as the only input argument.

sa_sigaction: if SA_SIGINFO flag is set in sa_flags, the handler pointed by sa_sigaction should be used.

The handler function should accept three arguments, the signal number, a pointer to an instance of siginfo_t structure (contains information about the signal) and a pointer to an object of type ucontext_t (contains the receiving process context which has been interrupted by the delivered signal. It is a void pointer can be casted to ucontext_t pointer).

The siginfo_t data structure has the following elements,

siginfo_t {

   int      si_signo;    /* Signal number */

   int      si_errno;    /* An errno value */

   int      si_code;     /* Signal code */

   int      si_trapno;   /* Trap number that caused

                            hardware-generated signal

                            (unused on most architectures) */

   pid_t    si_pid;      /* Sending process ID */

   uid_t    si_uid;      /* Real user ID of sending process */

   int      si_status;   /* Exit value or signal */

   clock_t  si_utime;    /* User time consumed */

   clock_t  si_stime;    /* System time consumed */

   sigval_t si_value;    /* Signal value */

   int      si_int;      /* POSIX.1b signal */

   void    *si_ptr;      /* POSIX.1b signal */

   int      si_overrun;  /* Timer overrun count; POSIX.1b timers */

   int      si_timerid;  /* Timer ID; POSIX.1b timers */

   void    *si_addr;     /* Memory location which caused fault */

   int      si_band;     /* Band event */

   int      si_fd;       /* File descriptor */

}

sa_mask: specify the signals should be blocked when the handler is in execution. In other words, the sa_mask adds signals to the signal mask of the process before signal handler is called. And when the signal handler function returns, the signal mask of the process is reset to its previous value. In this way, we can block certain signals when the signal handler is running. In addition, the signal that caused the signal handler to execute is also blocked, unless the SA_NODEFER flag is set.

sa_flags: specifies various options of handling the signal. The actions can be aggregated by the bitwise OR operation. The details of the flags can be referred from Linux man page.

sa_restorer: This is obsolete and should not be used.

An Example

Below is an example of installing a naive signal handler for SIGINT.

#include <stdio.h>

#include <signal.h>

 

void check_block(int signum) {

    sigset_t set;

    if (sigprocmask(0, NULL, &set)==-1) {

        perror("error sigprocmask:");

    } else {

        if (sigismember(&set, signum)) {

            printf("%d is blockedn", signum);

        } else {

            printf("%d is not blockedn", signum);

        }

    }

}

 

void my_handler(int signum) {

    check_block(signum);

    printf("inside signal handler: %dn", signum);

}

 

void my_handler2(int signum, siginfo_t *info, void *context) {

    check_block(signum);

    printf("inside signal handler: %dn", info->si_signo);

}

 

int main() {

    struct sigaction action;

//    action.sa_handler = my_handler;

//    action.sa_flags = SA_RESTART;

    action.sa_sigaction = my_handler2;

    action.sa_flags = SA_RESTART | SA_SIGINFO;

    sigaction(SIGINT, &action, NULL);

    printf("press cntl + cn");

    pause();

    printf("after signal handler:n");

    check_block(SIGINT);

    return 0;

}

The code first installs the signal handler for SIGINT with sigaction system call. Inside the signal handler, it checks if the SIGINT signal is blocked. As described in sa_mask of sigaction system call, the SIGINT  should be blocked inside the signal handler.

After installing the signal handler, the program calls pause() system call to wait for signal to occur. And it checks to see if the SIGINT is blocked again after the execution returns from the signal handler. This time, SIGINT should not be blocked.

Compile the code with command below,

gcc -o sigaction sigaction.c

And a sample execution is as below,

Untitled

Figure 1. Sample Execution of Sigaction Program

SA_RESTART and Interrupted System Call

You may notice that the we set the sa_flags with SA_RESTART in the example above. This has something to do with system calls.

When a signal is caught while a system call is blocked (e.g. waiting for IO), then two results can happen after the signal handler:

  • the call is restarted automatically after the signal handler returns
  • the call fails with errno set to EINTR

With SA_RESTART flag set, some system calls are restarted automatically after the signal handler, including open, wait, waitpid etc. Those system calls return failure if SA_RESTART is not set.

There’re some system calls return failure regardless SA_RESTART is set or not. A special case is the sleep function, which is never restarted but the number of seconds remaining is returned instead of failure.

The detailed list of system calls that are restarted can found with “man 7 signal” command.

The Async-signal-safe Functions

When a signal is caught by a signal handler, the normal execution is temporarily interrupted by the signal handler. If  the signal handler returns instead of terminating the process, the execution continues at where it is interrupted. Special attention needs to be paid for functions inside signal handler because we don’t want signal handler code to interfere with other code.

A bad example of this is the signal handler changes the global static variable which is used by the code after the handler.

POSIX standard defines a list of safe functions which can be called inside the signal handler, the detailed list can be obtained with “man 7 signal”.

Note that in the example above, we called printf() function inside the signal handler, which is actually not safe. If the signal is caught when we are calling printf inside the main function, the results may be unexpected.

References:

1. The Linux Signals Handling Model: http://www.linuxjournal.com/article/3985

2. Linux Signals for the Application Programmer: http://www.linuxjournal.com/article/6483

3. Linux sigaction man page.

4. Linux signal man page (man 7 signal).

5. Advaned Programming in the UNIX environment

Linux Signal–Part 1. The Basics

Linux Signals Overview

Linux supports both POSIX reliable/standard signals and real-time signals. The first 31 signals are standard signals. Real time signals ranges from 34 to 64. The Linux command “kill -l” lists all signals numbers and names.

This post discusses reliable signals only.

A signal can be synchronous or asynchronous to the process, depending what caused the signal. Synchronous signals are also referred as traps, because they cause a trap into a kernel trap handler. An example is signal caused due to illegal instruction. Asynchronous signals are also referred as interrupts, they’re external to the current execution context and often used to send asynchronous events to a process.

If a process is in interruptible sleep state, the kernel can deliver the signal to the process and wake it up to handle it. For example, the process is waiting for terminal IO. If a process is in uninterruptible sleep, the kernel will hold the signal until the process wakes up. For instance, the process is waiting for disk IO.

Linux kernel maintains signal information for each process. The information includes an array of signal handlers and related information. Once a signal is generated, the kernel sets a bit in corresponding to the signal. Since it’s a single bit, multiple occurrences and a single occurrence are equivalent.

Signals names always start with SIG, they are defined by positive integer constants called signal numbers in the header file signal.h. signal.h is normally found in /usr/include/ directory of Linux. The actual signal numbers are usually defined in /usr/include/bits/signum.h.

Signals can happens at random times and the process can tell kernel to do one of the three things when a signal occurs,

1. ignore the signal. All except two signals (SIGSTOP and SIGKILL) can be ignored. SIGKILL always terminate a process, while SIGSTOP always clears any pending/queued SIGCONT signals and stop the process (a stopped is process may be resumed later, which is different from a terminated process). In addition, some hardware generated signals may cause the process behavior undefined if ignored (e.g. SIGSEGV).

2. catch the signal. All except two signals (SIGSTOP and SIGKILL) can be caught. We tell the kernel to execute a handler function whenever the signal occurs.

3. apply default action. Every signal has a default action. There’re four possible actions if the signal is not ignored or caught.

  • ignore: nothing happens
  • terminate: the process is terminated
  • terminate + coredump: create a core dump file and then terminate the process
  • stop: stop all threads in the process. The process goes to TASK_STOPPED state.

Linux Signals One by One

SIGHUP(1): Hangup. The signal is sent to the controlling process when a disconnect is detected by its controlling terminal interface. By default, the process is terminated.

SIGINT(2): Interrupt. The signal is sent to all foreground processes when the interrupt key (often DELETE or Control – C) is pressed. By default, the foreground processes are terminated.

SIGQUIT(3): Quit. The signal is sent to all foreground processes when the quit key (often Control – ) is pressed. In addition to terminates the foreground processes, it creates a core dump file.

SIGILL(4): Illegal instruction. It is generated when a process has executed an illegal (malformed, privileged, or unknown) hardware instruction. By default, the process is terminated and a core dump is created.

Below is a program that generates SIGILL,

typedef void(*FUNC)(void);

int main(void) {

   const static unsigned char insn[4] = {0xff, 0xff, 0xff, 0xff};

   FUNC function = (FUNC) insn;

   function();

   return 0;

}

SIGTRAP(5): Trace trap. It is used as a mechanism to notify a debugger when the process execution hits a breakpoint. By default, the process is killed and a core dump is created.

SIGABRT(6): Abort. It is generated by calling the abort function. The process terminates abnormally and a core dump file will be created by default.

SIGBUS(7): Bus error, BUS refers to the address bus in the context of a bus error. It indicates implementation defined hardware fault. It is usually caused by improper memory handling. By default, the process is terminated and a core dump is created.

SIGFPE(8): Floating point exception. It is sent to a process when it performs an illegal arithmetic operation. Note that although it is named so mainly for backward compatibility. Some common cases are dividing by 0 or floating point overflow.By default, the process is terminated and a core dump is created.

int main(void)

{

      /* "volatile" needed to eliminate compile-time optimizations */

      volatile int x = 42; 

      volatile int y = 0;

      x=x/y;

      return 0; /* Never reached */

}

SIGKILL(9): Kill. This signal always cause the process to terminate. It cannot be ignored or caught. It provides a sure way to kill a process for users with proper privilege.

SIGUSR1(10): User defined signal 1. It is sent to a process to indicate user-defined conditions. By default, the process is terminated.

SIGSEGV(11): Segmentation fault. Invalid memory segment access. By default, the process is terminated and a core dump is created. Some common cases of SIGSEGV are buffer overflow, using uninitialized pointers, dereferencing NULL pointers, exceeding the allowable stack size, attempting to access the memory the program doesn’t own, etc.

#include <stdio.h>

#include <stdlib.h>

int main(void) {

   int* a;

   a[1] = 0;

   return 0;

}

SIGUSR2(12): User defined signal 2. It is sent to a process to indicate user-defined conditions. By default, the process is terminated.

SIGPIPE(13): Broken pipe. By default, the process is terminated. It is sent to a process when it tries writing to a pipe without a process connecting to the other end.

SIGALRM(14): Alarm clock. By default, the process is terminated. It is sent to a process when a time limit has exceeded. Programs usually use SIGALRM to make a long running operation time out, or to perform an action at regular intervals.

SIGTERM(15): Termination signal. It is the default signal sent by kill and killall command. By default, the process is terminated.

SIGSTKFLT(16): Stack fault. By default, the process is terminated.

SIGCHLD/SIGCLD(17): Child process has stopped or exited, changed. By default, the signal is ignored by the process. A process can create a child process using fork. The signal is sent to the parent process when the child process terminates.

SIGCONT(18): Continue executing, if stopped. It resumes the process from TASK_STOPPED state and also clears any pending/queued stop signals. This happens no matter the process blocks, catches, or ignores the SIGCONT signal. By default, the signal is ignored.

SIGSTOP(19): Stop executing. The signal cannot be caught or ignored. It clears any pending/queued SIGCONT signals and stops the process.

SIGTSTP(20): Terminal stop signal. It clears any pending/queued SIGCONT signals no matter the signal is blocked, ignored or caught. Though it may or may not stop the process later on. By default, it stops the process. It is the signal sent to the process by its controlling terminal when user presses the SUSP key combination (Ctrl + Z normally).

SIGTTIN(21): Background process trying to read, from TTY. It clears any pending/queued SIGCONT signals no matter the signal is blocked, ignored or caught. Though it may or may not stop the process later on. By default, it stops the process.

SIGTTOU(22): Background process trying to write, to TTY. It clears any pending/queued SIGCONT signals no matter the signal is blocked, ignored or caught. Though it may or may not stop the process later on. By default, it stops the process.

SIGURG(23): Urgent condition on socket. By default, the signal is ignored by the process. It is sent to a process with async IO configured by fcntl system call on Linux when out-of-band data is available on a file descriptor connected to a socket.

SIGXCPU(24): CPU limit exceeded. By default, the process is terminated and a core dump is created. It is sent to a process when it has used the CPU for a duration that exceeds a certain predetermined user set value.

SIGXFSZ(25): File size limit exceeded. By default, the process is terminated and a core dump is created. It is sent to a process when it has created a file that exceeded the maximum allowed size.

SIGVTALRM(26): Virtual alarm clock. By default, the process is terminated. It is sent to a process when a time limit has reached. It counts only the time spent executing the process itself.

SIGPROF(27): Profiling alarm clock. By default, the process is terminated. It is sent to a process when a time limited has reached. It counts the time spent by the process and the system executing on behalf of the process.

SIGWINCH(28): Window size change. By default, the process is ignored. It is sent to a process when its controlling terminal size changes. It gives the process an opportunity to adjust its display.

SIGIO/SIGPOLL(29): I/O now possible. By default, the process is terminated. It is sent to a process when an async IO event occurs.

SIGPWR(30): Power failure restart. By default, the process is terminated. It is sent to a process when the system experiences power failure. This gives the process an opportunity to save its state.

SIGSYS/SIGUNUSED(31): Bad system call. By default, the process is terminated and a core dump is created. It is sent to a process when a bad argument is passed to a system call.

References:
1. Linux signal overview man page: http://linux.die.net/man/7/signal
2. Advanced Programming in the UNIX Environment, 2nd edition.
3. Linux signal.h and some other header source files.
4. SIGKILL wikipedia page: http://en.wikipedia.org/wiki/SIGILL
5. SIGFPE wikipedia page: http://en.wikipedia.org/wiki/SIGFPE
6. SIGBUS wikipedia page: http://en.wikipedia.org/wiki/SIGBUS