Why this matters
When you press Ctrl-C, when docker stop or systemctl stop ends a service, when a program reads past the end of an array and "segfaults", when a container is killed for using too much memory: each time, a signal is delivered to a process. Signals are the operating system's way of interrupting a program asynchronously, and handling the right ones is what separates a program that loses data when stopped from one that saves its state and exits cleanly. The second half of this lesson covers the environment, the other channel (besides arguments) through which a process receives information from whoever started it: PATH, HOME, LANG, and every DATABASE_URL in the world of deployed software.
What a signal is
A signal is a small integer notification sent to a process by the kernel or by another process. When it arrives, the process's normal flow is interrupted and one of three things happens: the default action (terminate, terminate with a core dump, stop, continue or ignore, depending on the signal), the signal is ignored, or a handler function you installed runs, after which the program continues where it was interrupted.
| signal | number | sent when | default |
|---|---|---|---|
SIGINT | 2 | Ctrl-C in the terminal | terminate |
SIGQUIT | 3 | Ctrl-\ | terminate + core |
SIGKILL | 9 | kill -9, out-of-memory killer | terminate, cannot be caught or ignored |
SIGSEGV | 11 | invalid memory access | terminate + core |
SIGPIPE | 13 | write to a pipe with no reader | terminate |
SIGALRM | 14 | timer set with alarm expired | terminate |
SIGTERM | 15 | polite request to stop (kill default) | terminate |
SIGCHLD | 17 | a child process ended | ignore |
SIGUSR1, SIGUSR2 | 10, 12 | whatever your program decides | terminate |
SIGSTOP | 19 | stop (pause) the process, cannot be caught | stop |
(Numbers are those of Linux on x86-64; use the names in code.) kill -l lists them all, and in the sandbox man 7 signal describes every one.
When a signal kills a process, the parent sees it through WIFSIGNALED/WTERMSIG (previous two lessons), and shells report status 128 + N:
$ sleep 100 &
$ kill -KILL %1
$ wait %1; echo $?
137
Sending signals
From the shell: kill -TERM PID (or just kill PID), kill -USR1 PID, Ctrl-C (SIGINT to the foreground process group). From C: kill(pid, sig) from <signal.h>, and raise(sig) to signal yourself. A process may only signal processes of the same user (unless privileged).
Handling signals with sigaction
The portable way to install a handler is sigaction:
#define _POSIX_C_SOURCE 200809L
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
static volatile sig_atomic_t stop = 0;
static void on_signal(int sig)
{
(void)sig;
stop = 1; /* only set a flag: nothing else is safe here */
}
int main(void)
{
struct sigaction sa;
memset(&sa, 0, sizeof sa);
sa.sa_handler = on_signal;
sigemptyset(&sa.sa_mask);
if (sigaction(SIGINT, &sa, NULL) < 0 || sigaction(SIGTERM, &sa, NULL) < 0) {
perror("sigaction");
return 1;
}
printf("working (pid %ld); press Ctrl-C or send SIGTERM\n", (long)getpid());
fflush(stdout);
int ticks = 0;
while (!stop) {
sleep(1); /* returns early when a signal arrives */
ticks++;
}
printf("signal received after %d ticks: saving state and exiting cleanly\n", ticks);
return 0;
}
Started in the background and sent SIGTERM after about two and a half seconds:
$ ./prog > o.txt & sleep 2.5; kill -TERM $!; wait $!; echo "status $?"
status 0
$ cat o.txt
working (pid 12); press Ctrl-C or send SIGTERM
signal received after 3 ticks: saving state and exiting cleanly
(The PID and tick count vary.) Without the handler, the status would be 143 and the second line would never appear. The pieces:
sa_handleris a pointer to a function taking the signal number (function pointers from the pointers module).SIG_IGNignores the signal,SIG_DFLrestores the default.sa_masklists signals blocked while the handler runs; empty is fine here.sa_flags(zeroed by thememset) can requestSA_RESTART(automatically restart some interrupted system calls) and more.- Older code uses
signal(SIGINT, handler), which is simpler but behaves differently across systems; prefersigaction.
What a handler may do
A handler runs in the middle of whatever the program was doing: perhaps halfway through printf updating its buffer, or malloc updating its free lists. If the handler calls printf or malloc itself, it re-enters code that is in an inconsistent state, with results ranging from garbled output to deadlock. So handlers may only call async-signal-safe functions (write, _exit, kill and a list of others in signal-safety(7)), and the standard pattern is to do almost nothing:
- Set a flag of type
volatile sig_atomic_t.sig_atomic_tis an integer type that is read and written in one indivisible step;volatiletells the compiler that the value can change behind its back, so it must reload it every time the loop checks it, instead of caching it in a register and looping forever. - Let the main loop notice the flag and do the real work: save files, close connections, print.
Interrupted system calls
When a signal arrives during a blocking call like sleep, read or waitpid, the handler runs and the call returns early (sleep returns the seconds left; read returns -1 with errno == EINTR). That is useful, since it lets the loop notice the flag promptly, and it is why the write_all helper from the first lesson of this module retries on EINTR.
The environment
Every process has an environment: a list of NAME=value strings, copied from its parent at fork and passed on through exec. It is how settings flow down the process tree without being mentioned on every command line:
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main(void)
{
const char *home = getenv("HOME");
const char *editor = getenv("EDITOR");
printf("HOME=%s\n", home ? home : "(unset)");
printf("EDITOR=%s\n", editor ? editor : "(unset, using vi)");
if (setenv("GREETING", "hello", 1) != 0)
perror("setenv");
printf("GREETING=%s\n", getenv("GREETING"));
int n = 0;
for (char **e = environ; *e != NULL; e++)
n++;
printf("%d variables in the environment\n", n);
return 0;
}
In the sandbox:
HOME=/home/learner
EDITOR=(unset, using vi)
GREETING=hello
10 variables in the environment
And with EDITOR=nano ./prog, the second line becomes EDITOR=nano: the shell syntax NAME=value command sets a variable for that one command only.
getenv(name)(ISO C) returns a pointer to the value, or NULL if the variable is not set. Always check;printf("%s", NULL)is undefined behavior. Do not modify the returned string.setenv(name, value, overwrite)andunsetenv(name)(POSIX) change your own environment, which your future children inherit. A child can never change its parent's environment, which is why a script that runsexport X=1cannot change the variables of the shell that started it (unlesssourced).environis the whole list as a NULL-terminated array of"NAME=value"strings, exactly likeargv.
Treat environment values as untrusted input: validate them like anything a user typed, with strtol and range checks for numbers.
Common mistakes
- Calling
printf,mallocorexitin a handler. Use a flag; if you must print, usewrite. - A plain
intflag. Withoutvolatile, the optimizer may read it once and loop forever. - Installing handlers too late, after the program has announced it is ready.
- Expecting to catch
SIGKILLorSIGSTOP. They exist precisely so that a process cannot refuse them. Graceful shutdown must hang offSIGTERM, withSIGKILLas the fallback that orchestrators send after a grace period. - Dereferencing
getenv's result without checking for NULL. - Assuming one signal per send. Several identical signals arriving while one is pending are merged into one; signals are notifications, not a message queue.
Recap
- Signals interrupt a process asynchronously; each has a default action, and most can be caught or ignored.
SIGKILLandSIGSTOPcannot. - A process killed by signal N reports status 128 + N (137 = SIGKILL, 139 = SIGSEGV, 143 = SIGTERM).
- Install handlers with
sigaction; in the handler only set avolatile sig_atomic_tflag (or call async-signal-safe functions), and do the work in the main loop. - Blocking calls return early with
EINTRwhen a signal is handled. - The environment is a list of
NAME=valuestrings inherited across fork and exec; read it withgetenv(check for NULL), change your own withsetenv.
Going further: in the sandbox, man 7 signal and man 7 signal-safety are installed and list every signal and every async-signal-safe function. For a modern alternative to handlers, read about signalfd(2) at man7.org, which turns signals into data you read from a file descriptor.