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:
Signal | Default action | Description |
---|---|---|
SIGABRT | A | Process abort signal. |
SIGALRM | T | Alarm clock. |
SIGBUS | A | Access to an undefined portion of a memory object. |
SIGCHLD | I | Child process terminated, stopped or continued. |
SIGCONT | C | Continue executing, if stopped. |
SIGFPE | A | Erroneous arithmetic operation. |
SIGHUP | T | Hangup. |
SIGILL | A | Illegal instruction. |
SIGINT | T | Terminal interrupt signal. |
SIGKILL | T | Kill (cannot be caught or ignored). |
SIGPIPE | T | Write on a pipe with no one to read it. |
SIGQUIT | A | Terminal quit signal. |
SIGSEGV | A | Invalid memory reference. |
SIGSTOP | S | Stop executing (cannot be caught or ignored). |
SIGTERM | T | Termination signal. |
SIGTSTP | S | Terminal stop signal. |
SIGTTIN | S | Background process attempting read. |
SIGTTOU | S | Background process attempting write. |
SIGUSR1 | T | User-defined signal 1. |
SIGUSR2 | T | User-defined signal 2. |
SIGPOLL | T | Pollable event. |
SIGPROF | T | Profiling timer expired. |
SIGSYS | A | Bad system call. |
SIGTRAP | A | Trace/breakpoint trap. |
SIGURG | I | High bandwidth data is available at a socket. |
SIGVTALRM | T | Virtual timer expired. |
SIGXCPU | A | CPU time limit exceeded. |
SIGXFSZ | A | File size limit exceeded. |
The default actions are as follows:
T
: Abnormal termination of the processA
: Abnormal termination of the process with additional actionsI
: Ignore the signalS
: Stop the processC
: 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:
- Ignores the same signals ignored by the parent
- Manage in the same way the signals managed by the parent
- 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 actionSIG_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: