Signals are one of the most basic ways programs can receive messages from the outside world. I’ve found limited tutorial-type documentation on them, so this post covers how to set them up and some debugging techniques.
The easiest way to get a feel for signal handling is to play with a simple C program, like this:
#include #include #include void my_handler(int signum) { const char msg[] = "Signal handler got signaln"; write(STDOUT_FILENO, msg, sizeof msg); } int main(int argc, char *argv[]) { printf("PID: %dn", getpid()); // Set up signal handler struct sigaction action = {}; action.sa_handler = &my_handler; sigaction(SIGINT, &action, NULL); while (1) { pause(); } return 0; }
Compile and run and try hitting Ctrl-C a few times:
$ gcc signals.c -o signals $ ./signals PID: 11152 ^CSignal handler got signal 2 ^CSignal handler got signal 2 ^CSignal handler got signal 2
Each signal calls the signal handler we set up.
If you attach strace
(system call tracer) and then hit Ctrl-C in the terminal running ./signals again, you can see each signal coming in:
$ $ strace -p 11152 -e trace=none -e signal=all Process 11152 attached - interrupt to quit --- SIGINT (Interrupt) @ 0 (0) --- --- SIGINT (Interrupt) @ 0 (0) --- --- SIGINT (Interrupt) @ 0 (0) ---
As we can’t kill it with Ctrl-C, we can use kill to shutdown ./signals:
$ kill 11152
kill
defaults to sending a SIGTERM, which we’re not handling (yet). You could add a handler for it by adding the line sigaction(SIGTERM, &action, NULL);
but then we’d have to kill -9
the process to kill it (which is two extra characters of typing) so I’m leaving SIGTERM unhandled.
Ignoring Signals
There are also ways to make your program not even receive signals: ignoring and blocking them (which are subtly different). To ignore a signal, change sa_action
to SIG_IGN
:
#include #include #include void my_handler(int signum) { const char msg[] = "Signal handler got signaln"; write(STDOUT_FILENO, msg, sizeof msg); } int main(int argc, char *argv[]) { printf("PID: %dn", getpid()); // Set up signal handler struct sigaction action = {}; action.sa_handler = SIG_IGN; sigaction(SIGINT, &action, NULL); while (1) { pause(); } return 0; }
Now recompile and run and hit Ctrl-C. You’ll get something like this:
$ ./signals PID: 86579 ^C^C^C^C^C
If you attach strace, you’ll see that ./signals isn’t even receiving the SIGINTs.
You can see the signals a program is ignoring by looking at /proc/PID/status:
$ cat /proc/86579/status Name: signals State: S (sleeping) Tgid: 86579 Pid: 86579 PPid: 30493 TracerPid: 0 Uid: 197420 197420 197420 197420 Gid: 5000 5000 5000 5000 FDSize: 256 Groups: 4 20 24 25 44 46 104 128 499 5000 5001 5762 74990 75209 77056 78700 79910 79982 VmPeak: 4280 kB VmSize: 4160 kB VmLck: 0 kB VmPin: 0 kB VmHWM: 352 kB VmRSS: 352 kB VmData: 48 kB VmStk: 136 kB VmExe: 4 kB VmLib: 1884 kB VmPTE: 28 kB VmSwap: 0 kB Threads: 1 SigQ: 0/192723 SigPnd: 0000000000000000 ShdPnd: 0000000000000000 SigBlk: 0000000000000000 SigIgn: 0000000000000002 SigCgt: 0000000000000000 CapInh: 0000000000000000 CapPrm: 0000000000000000 CapEff: 0000000000000000 CapBnd: ffffffffffffffff Cpus_allowed: ffffffff Cpus_allowed_list: 0-31 Mems_allowed: 00000000,00000001 Mems_allowed_list: 0 voluntary_ctxt_switches: 2 nonvoluntary_ctxt_switches: 3
SigIgn is a hexidecimal number and it has kind of a weird format: the ignored signal number’s bit is set. So, for 2, the second bit is set. For SIGTERM (signal 15), the 15th bit is set: 0100_0000_0000_0000 in binary or 0x4000 in hexadecimal. So, if you were ignoring both SIGINT and SIGTERM, SigIgn would look like: 0000000000004002.
SigCgt is for signals that are being caught by the program and SigBlk is for signals that are being blocked.
Blocking Signals
What if you want your program to handle any signals that come in, just do it later? You might have a critical section where you don’t want to be interrupted, but afterwards you want to know what came in. That’s where blocking signals comes in handy.
You can block signals using sigprocmask
:
#include #include #include void my_handler(int signum) { const char msg[] = "Signal handler got signaln"; write(STDOUT_FILENO, msg, sizeof msg); } int main(int argc, char *argv[]) { printf("PID: %dn", getpid()); // Set up signal handler struct sigaction action = {}; action.sa_handler = &my_handler; sigaction(SIGINT, &action, NULL); printf("Blocking signals...n"); sigset_t sigset; sigemptyset(&sigset); sigaddset(&sigset, SIGINT); sigprocmask(SIG_BLOCK, &sigset, NULL); // Critical section sleep(5); printf("Unblocking signals...n"); sigprocmask(SIG_UNBLOCK, &sigset, NULL); while (1) { pause(); } return 0; }
First we create a sigset_t
which can hold a set of signals. We empty out the set with a call to sigemptyset
and add a signal element: SIGINT. (There are a bunch of other set ops you can use to modify sigset_t
, if necessary.)
If you compile and run this and try Ctrl-C-ing while signals are blocked, one signal will be “let through” when the signals are unblocked:
$ ./signals PID: 86791 Blocking signals... ^C^C^C^C^C^CUnblocking signals... Signal handler got signal 2
One common use for this is blocking signals while the signal handler is running. That way you can have your signal handler modify some non-atomic state (say, a counter of how many signals have come in) in a safe way.
However, suppose we called sigprocmask
in the signal handler. There will always be a race condition: another signal could come in before we’ve called sigprocmask
! So sigaction
takes a mask of signals it should block while the handler is executing:
#include #include #include void my_handler(int signum) { const char msg[] = "Signal handler got signaln"; write(STDOUT_FILENO, msg, sizeof msg); } int main(int argc, char *argv[]) { printf("PID: %dn", getpid()); // Set up signal handler struct sigaction action = {}; action.sa_handler = &my_handler; sigset_t mask; sigemptyset(&mask); sigaddset(&mask, SIGINT); sigaddset(&mask, SIGTERM); action.sa_mask = mask; sigaction(SIGINT, &action, NULL); while (1) { pause(); } return 0; }
Here, we’re masking both SIGINT and SIGTERM: if either of these signals comes in while my_handler
is running, they’ll be blocked until it completes.
Inheritance
Ignored and blocked signals are inherited when you fork a program. Thus, if a program isn’t responding to signals the way that you expect, it might be the fault of whoever forked it. Also, if you want your program to handle signals in certain ways you should explicitly set that rather than depending on the default.
If you want to use a signal’s default behavior (which is usually “terminate the program”), you can use SIG_DFL
as the sa_handler
.
What you can do in a signal handler
You might notice that I’m using write
in the signal handlers above, instead of the somewhat more friendly printf
. This is because there are only a small set of “async safe” functions you can call in a signal handler and printf isn’t one of them. There is a list of functions you can call on the signal(7) man page. A few examples that often come up: you cannot heap-allocate memory, buffer output, or mess with locks.
If you call any unsafe functions in a signal handler, the behavior is undefined (meaning it might work fine, or it might make your car blow up).
Edit: thanks to Vincent Bernat, who mentioned this in the comments.
References:
Good article. You should mention that the use of some functions are unsafe in the signal handler, like… printf().
LikeLike
Good point! Fixed the code and added a section about that. Thank you!
LikeLike