Week 8: Signals
Signal basics
A signal is an asynchronous alert sent from one process to another (or from a process to itself). There is a fixed set of named signals, and they carry no data other than their signal number. The kernel sends signals to processes for various reasons:
SIGCHLDis sent to a parent process when a child process exits.SIGSEGVis sent to a process when it triggers a segmentation fault via invalid memory access (normally this signal is not caught).SIGILLis sent when the process attempts to execute an illegal processor instruction.
Other signals are meant to be sent between userspace processes:
SIGTERMrequests the target process to terminate.SIGKILLforcibly kills the target process.SIGWINCHalerts a terminal program that the window has been resized.
In a process, each signal has a corresponding disposition that determines what happens when the signal is received. The possibilities are:
- Ignore the signal.
- Perform the default action, which depends on the signal.
SIGTERMterminates the process, for instance, whileSIGWINCHdoes nothing. - Catch the signal and call a signal handler function.
Though the special signals SIGKILL and SIGSTOP cannot be ignored or caught.
A process can set a signal type to be blocked (also referred to as masked). If an incoming signal is blocked by the recipient process, the kernel will keep it pending until the process unblocks it (or sets the disposition to "ignore"). Once unblocked, the signal will be delivered to the process. Masking is usually temporary – otherwise you would just set the signal to be ignored.
Why would you want to mask?
- You are executing a critical section of the code and you don't want your process to be terminated in the middle of it. (Though understand that this is best-effort since you cannot mask or ignore
SIGKILL.) - You are a multi-threaded program and you want to mask signals in worker threads and have a dedicated signal thread handle all incoming signals. (You have to mask, not ignore, in the worker threads since signal masks are per-thread but signal dispositions are per-process.)
System calls
One process sends a signal to another using the kill syscall, which, despite its name, can send any signal and does not necessarily terminate the target.
int kill(pid_t pid, int sig);
pid is normally positive, in which case the signal is sent to the process with that PID. If zero or negative, the behavior differs; read the man page for details. You must either be root, or have the same real UID as the target process.
kill can send arbitrary signals; e.g., you can send SIGSEGV regardless of if the target process actually triggered a segmentation fault.
sigaction is how you change the disposition of a signal.
struct sigaction {
void (*sa_handler)(int);
sigset_t sa_mask;
int sa_flags;
// more fields
};
int sigaction(int signum, struct sigaction* act, struct sigaction* oldact);
You can use it like this:
void handler(int signo) {
// ...
}
struct sigaction act = { 0 };
act.sa_handler = handler;
int r = sigaction(SIGTERM, &act, NULL);
Set sa_handler to the special values SIG_IGN to ignore the signal or SIG_DFL to set it to the default disposition.
sa_mask in struct sigaction is the set of signals that will be temporarily masked while the signal handler is executing.
sigprocmask lets you query and set the process's signal mask:
int sigprocmask(int how, sigset_t* set, sigset_t* oset);
how can be one of:
SIG_BLOCK– block all signals insetSIG_UNBLOCK– unblock all signals insetSIG_SETMASK– set the signal mask toset(all signals in the set are blocked; all signals not in the set are unblocked)
The first two only change the signals that are in set, while SIG_SETMASK changes every signal. If oset is not NULL, then it will be set to the process's current signal mask. If you just wish to query and not set the mask, then pass NULL as the second argument.
sigprocmask takes two signal set (sigset_t) arguments. There are some helper functions for manipulating signal sets: (man 3 sigsetops)
int sigemptyset(sigset_t* set);
int sigfillset(sigset_t* set);
int sigaddset(sigset_t* set, int signo);
int sigdelset(sigset_t* set, int signo);
int sigismember(sigset_t* set, int signo);
sigfillset selects all signals in the set; sigemptyset unselects them; sigaddset and sigdelset select or unselect, respectively, individual signals; sigismember tests if a signal is selected in the set.
We can wait for any signal to arrive with pause, or a particular signal (or set of signals) with sigwait:
int pause();
int sigwait(sigset* set, int* sig);
But we'll see in the next section that these functions are tricky to use safely.
Signals are hard
Signals are one of the trickiest parts of Linux. Signals can arrive at any time, and signal handlers execute concurrently with the rest of the program, so they share many of the same difficulties as preemptive multithreading.
Because signals can arrive at random times, a program's data structures may be in an inconsistent state when a signal handler is invoked. Only a subset of C standard library functions are safe to call from a signal handler – see man 7 signal-safety. Example: It's not safe to call malloc from a signal handler, because the signal might have been received in the middle of a different call to malloc!
With that in mind, it's a good idea to do as little as possible in the signal handler. Ideally, you just set a flag and return, and test the flag in your program's main loop.
pause is perilous. Consider the following snippet:
sigaction(SIGUSR1, &(struct sigaction){.sa_handler=my_handler}, NULL);
puts("Send me SIGUSR1 to continue!");
pause();
Do you see the bug?
If SIGUSR1 is delivered after puts but before pause, then pause will block until it is sent a second time – unlikely in this toy example, perhaps, but a very easy mistake to make in real programs.
We can fix it with sigsuspend, which atomically sets the signal mask and then goes to sleep, restoring the old signal mask when woken up:
sigaction(SIGUSR1, &(struct sigaction){.sa_handler=my_handler}, NULL);
sigset_t set, oldset;
sigemptyset(&set);
sigaddset(&set, SIGUSR1);
sigprocmask(SIG_BLOCK, &set, &oldset);
// SIGUSR1 is blocked.
puts("Send me SIGUSR1 to continue!");
sigsuspend(&oldset);
// SIGUSR1 is unblocked until `sigsuspend` returns.
sigprocmask(SIG_SETMASK, &oldset, NULL);
// SIGUSR1 is unblocked.
This example was based on the blog post "The perils of pause(2)" by Julian Squires.
Another problem is that signals can interrupt system calls – if certain syscalls are pending when a signal is delivered, the syscall will be cancelled and return EINTR. Only system calls that can block forever can be interrupted by signals. For instance, reading input from a terminal device, or waiting on a lock, or listening for a network connection. But not disk I/O. This design choice is the subject of a famous essay from the 1990s.
There are two things you can do about interrupted syscalls:
- Include
SA_RESTARTinact.sa_flagswhen passed tosigaction. Syscall interrupted by the signal will be automatically restarted – though some syscalls can't be restarted (seeman 7 signal). - Wrap any potentially blocking syscalls with a loop that retries on
EINTR. This can be annoying in practice because you don't always know whether a syscall can block – for instance, callingreadon a file descriptor that may point to a file on disk (won't block) or to a terminal device (might block).
Signals and multithreading
There are some nuances to how multithreading and signal handling interact:
- Disposition of a signal is per-process, not per-thread. However, signal masks are per-thread.
- Some signals are delivered to the entire process (in which case the signal handler, if one is registered, is run on an arbitrary thread), and some are delivered to a particular thread (for example, hardware exceptions triggered by that thread).
- Multithreaded programs should use
pthread_sigmaskinstead ofsigprocmask(though I suspect thatsigprocmaskalso "just works").
What should I do?
- Simple programs might not need to handle signals at all. All signals have a default behavior which is usually sensible.
- If you just want to do graceful shutdown on
SIGTERM, then set a flag in the signal handler or use the self-pipe trick, and handle it in the main loop of your program. - If you need to do something more complicated… be careful.
Final project milestone
Depending on how you implemented entry deletion, your database could end up with wasted space for deleted entries. Write a compact() function that gets rid of unused disk space. Install a signal handler so that compact is called when SIGUSR1 is received. (But don't call it in the signal handler!) Add a command to your client program to send this signal. (You'll need a way for the client program to discover the server's PID.) Hint
One typical approach is for the server to write its PID to a file at a known location at start-up.
Homework exercises
- (★) What are the three possible dispositions for a signal?
- (★) What is the difference between "masked" and "pending" for a signal?
- (★) True or false:
SIGTERMcan be ignored. - (★★) Write a program that shows what happens when the same signal is delivered twice before the program can handle it. Is the signal handler invoked twice, or only once?
- (★★) What happens when a signal handler is executing and the same signal is delivered? What if it's a different signal?
- (★★) If multiple pending signals are unblocked, what order are they delivered in?
- (★★) What happens to signal dispositions, masks, and pending signals upon
fork? What aboutexec? - (★★) What interface does your programming language provide for signal handling? Are you allowed to call arbitrary functions from the signal handler?
- (★★★) Look up the "self-pipe trick". What is it? When would you use it?
- (★★★) Linux has an alternative interface for handling signals called
signalfd. Read the man page and write an example program. What does it make easier? What are the drawbacks?
Further reading
- "Thread-Specific Data and Signal Handling in Multi-Threaded Applications" – old (from 1997) and uses an obsolete implementation of threading on Linux, but still a valuable post
- "Convert SIGTERM to an exception by default?" – Python discussion