Manage Signal in Unix

post hero image

Introduction

Before getting deep into the signal management in Unix systems, let’s talk about some important concepts.

You should know that the race condition is a situation where more than a process modify the same shared data. The outcome depends on relative order with which operations are performed. Lots of race conditions can be solved by synchronizing the processes using signals, pipes or other ways.

Sync between processes

Signals are software interrupts that report both synchronous and asynchronous events.
When a process receives a signal can:

  • ignore it: SIG_IGN
  • execute a user-defined action: void (*func)()
  • execute a default action: SIG_DFL

Note that all signals have the same priority.

List of signals

The list of signals is defined by <signal.h> header file.
You can find it at /usr/include/signal.h on a UNIX-based OS.

The full list of signals is:

SignalDefault actionDescription
SIGABRTAProcess abort signal.
SIGALRMTAlarm clock.
SIGBUSAAccess to an undefined portion of a memory object.
SIGCHLDIChild process terminated, stopped or continued.
SIGCONTCContinue executing, if stopped.
SIGFPEAErroneous arithmetic operation.
SIGHUPTHangup.
SIGILLAIllegal instruction.
SIGINTTTerminal interrupt signal.
SIGKILLTKill (cannot be caught or ignored).
SIGPIPETWrite on a pipe with no one to read it.
SIGQUITATerminal quit signal.
SIGSEGVAInvalid memory reference.
SIGSTOPSStop executing (cannot be caught or ignored).
SIGTERMTTermination signal.
SIGTSTPSTerminal stop signal.
SIGTTINSBackground process attempting read.
SIGTTOUSBackground process attempting write.
SIGUSR1TUser-defined signal 1.
SIGUSR2TUser-defined signal 2.
SIGPOLLTPollable event.
SIGPROFTProfiling timer expired.
SIGSYSABad system call.
SIGTRAPATrace/breakpoint trap.
SIGURGIHigh bandwidth data is available at a socket.
SIGVTALRMTVirtual timer expired.
SIGXCPUACPU time limit exceeded.
SIGXFSZAFile size limit exceeded.

The default actions are as follows:

  • T: Abnormal termination of the process
  • A: Abnormal termination of the process with additional actions
  • I: Ignore the signal
  • S: Stop the process
  • C: Continue the process, if it is stopped; otherwise, ignore the signal

Signals and System Calls

A signal in C language is implemented in this way:

#include <signal.h>

/**
 * Defines the behavior of a process when it receives a signal
 * @param sig signal integer value
 * @param func who to treat the signal
 * @return
 */
void (*signal (int sig, void (*func)(int))) (int);

Please Note: the following system calls usually need <unistd.h> library to be included.

alarm

unsigned int alarm(unsigned int seconds);

Start a timer. After the number of seconds specified in seconds, the process receive a SIGALRM signal. Note that there’s only a timer foreach precess: if two subsequent calls are made, the value of the second call overrides the value of the first.

fork

pid_t fork(void);

A child process inherits all parent’s signals behaviours:

  1. Ignores the same signals ignored by the parent
  2. Manage in the same way the signals managed by the parent
  3. Has the same default actions of the parent

exec

The exec() family of functions replaces the current process image with a new process image.
exec() changes the signal arrangement. The signals managed:

  • before exec, come back to default action
  • after exec, ignore the same signals ignored before

kill

int kill(pid_t pid, int sig);

kill() system call sends any signal to any process or process group.

If pid > 0, sig signal is sent to the process with the ID specified by pid.
If pid == 0, sig is sent to every process in the process group of the calling process.
If pid == -1, sig is sent to every process for which the calling process has permission to send signals, except for process 1 (init).
If pid <= -1, sig is sent to every process in the process group whose ID is -pid.
If sig == 0, then no signal is sent, but existence and permission checks are still performed. This can be used to check for the existence of a process ID or process group ID that the caller is permitted to signal.

pause

int pause(void);

pause() causes the calling process to sleep until a signal is delivered. It returns only when a signal was caught and the signal-catching function returned. In this case, pause() returns -1, and errno is set to EINTR.

sleep

unsigned int sleep(unsigned int seconds);

Temporary suspend the process until the number of seconds specified in seconds have elapsed or until a signal arrives and is not ignored.

sigaction

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

The sigaction() system call is used to change the action taken by a process on receipt of a specific signal.

The Unix signal model is unreliable. Here is a list of the major problems:

  • signals can be lost (the process does not receive them)
  • low control over signal managements
  • chance to start the signal management routine
  • chance for system calls to be interrupted

signal() primitive is not portable since there are different implementations for different Unix versions. POSIX.1 introduce the sigaction() primitive, which tries to overcome the issues related to Unix signal model.

sigaction() allow you to examine and/or modify the action associated with a given signal. Moreover, you can automatically restart system calls interrupted by a signal. Note that POSIX.1 asks you to guarantee that the signal remains installed (until the behaviour changes explicitly).

The sigaction structure is defined as follows:

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

sa_handler specifies the action to be associated with signum and can be:

  • SIG_DFL for the default action
  • SIG_IGN to ignore this signal
  • A pointer to a signal handling function that takes in the signal number

Examples

Change Signal Arrangement

Ignore a signal:

// ... some code
signal(SIGUSR1, SIG_IGN);
// from now on, the process will ignore SIGUSR1
// ... some other code

Define a signal handler:

// ... some code
void handler(int signal) {
    printf("Received signal: %d\n", signal);
    // you can terminate execution with exit(0);
}

int main() {
    // ... some code
    signal(SIGINT, handler);
    // from now on, the process executes 'handler' function
    // everytime Ctrl + C is pressed on the keyboard

    // ... some other code
}

Restore default action for the signal:

// ...
void handler(int signal) { /* ... */ }

int main() {
    // ...
    signal(SIGINT, handler);
    // from now on, the process does not execute 'handler' function
    // everytime Ctrl + C is pressed on the keyboard

    // ...
    signal(SIGINT, SIG_DFL);
    // from now on, the process restore default function of Ctrl + C
}

Manage child termination

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>

void print_status(int status) {
    if (WIFEXITED(status)) {
        printf("normally terminated with status %d\n", WEXITSTATUS(status));
    } else if (WIFSIGNALED(status)) {
        printf("terminated from signal %d\n", WTERMSIG(status));
    } else if (WIFSTOPPED(status)) {
        printf("stopped from signal %d\n", WSTOPSIG(status));
    }
}

void handler(int signal) {
    int status;
    wait(&status);

    print_status(status);
    printf("status = %d\tsignal = %d\n", status, signal);

    exit(0);
}

int main() {
    int pid;
    pid = fork();

    if (pid > 0) {
        printf("#PARENT: start\n");
        signal(SIGCHLD, handler);
        // ERROR! SIGKILL not manageable
        signal(SIGKILL, handler);

        sleep(10);
        printf("#PARENT: end\n");

        exit(0);
    } else {
        printf("#CHILD: start\n");
        sleep(5);
        printf("#CHILD: end\n");

        exit(1);
    }
}

Sigaction basic example

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

void sigint_handler(int sig) {
    printf("\t#received SIGINT: signal number = %d\n", sig);
}

/**
 * Press Ctrl + C to finish before the sleep().
 */
int main(void) {
    // prototype
    void sigint_handler(int sig);
    // set the sigaction structure
    struct sigaction sa;

    sa.sa_handler = sigint_handler;
    sa.sa_flags = 0;
    sigemptyset(&sa.sa_mask);

    if (sigaction(SIGINT, &sa, NULL) == -1) {
        perror("sigaction");
        exit(1);
    }

    sleep(20);

    return 0;
}

Conclusion

The signal handlers code shall never contain calls to I/O primitives: they are very slow and could be blocking. It’s very discouraged the usage of write(), read() but also printf and scanf.

If you need to define global variables, used by main and signal handlers, their data type should be sig_atomic_t since it can be accessed without interruption. Moreover, you should also declare global variables as volatile in order to avoid that any changes to the state of a variable inside the code of a signal handler is not noticed from the main application code.

Useful links: