signals, one per page
Twenty-eight lessons on POSIX signals, one signal or signal concept per page: the polite defaults SIGTERM, SIGKILL, SIGINT and SIGHUP; the keyboard signals Ctrl+C, Ctrl+backslash and Ctrl+Z plus the terminal's SIGTTIN and SIGTTOU; the violent ones, uncatchable SIGKILL, SIGSEGV, SIGPIPE and core dumps; the three dispositions default, ignore and catch, sigaction, SIG_IGN with SIG_DFL, and the user-defined SIGUSR1 and SIGUSR2; the command line, kill -l, pkill with killall, trap and the 128+N exit convention; the process lifecycle of SIGCHLD, zombies reaped by wait, SIGSTOP with SIGCONT and process groups; and survival with nohup, setsid, disown and async-signal-safety.
One signal, one page, one real capture. That's the rhythm.
signals, one per page
Twenty-eight lessons on POSIX signals, one signal or signal concept per page: the polite defaults SIGTERM, SIGKILL, SIGINT and SIGHUP; the keyboard signals Ctrl+C, Ctrl+backslash and Ctrl+Z plus the terminal's SIGTTIN and SIGTTOU; the violent ones, uncatchable SIGKILL, SIGSEGV, SIGPIPE and core dumps; the three dispositions default, ignore and catch, sigaction, SIG_IGN with SIG_DFL, and the user-defined SIGUSR1 and SIGUSR2; the command line, kill -l, pkill with killall, trap and the 128+N exit convention; the process lifecycle of SIGCHLD, zombies reaped by wait, SIGSTOP with SIGCONT and process groups; and survival with nohup, setsid, disown and async-signal-safety.
Set in Space Grotesk, Inter and JetBrains Mono (SIL Open Font License).
Every fact in this book is as the official source states it, fetched and read during this build: the Linux man-pages man7.org pages signal(7), signal(2), kill(1), kill(2), sigaction(2), wait(2) and setpgid(2) from the man-pages project; the POSIX signal.h specification from The Open Group (IEEE Std 1003.1-2017); and the Wikipedia articles Signal (IPC), Core dump, Nohup, Kill (command), Zombie process, Exit status and Job control (Unix). No facts are sourced from Reddit, Stack Overflow or vendor blogs. Every command and output pair on every page was run on this machine (bash 5.2.21, procps-ng 4.0.4, kernel 6.8.0-147-generic) and confirmed before printing; every demo ran on scratch child processes, never on system files. Unsafe demonstrations, signalling PID 1 or the login shell, were not run and are stated from the man pages instead. An independent guide, not affiliated with or endorsed by the Linux man-pages project, The Open Group, the Wikimedia Foundation, or the Linux kernel developers.
Your purchase is for personal use only. You do not have redistribution rights: please do not share, resell, or republish this book or its pages.
© 2026 Steve Hodgkiss. All rights reserved. Personal use only; no redistribution rights.
Edition 1.0 · stevehodgkiss.net
Contents
Per Linux man-pages signal(7), SIGTERM is signal 15, standard P1990, action Term, comment Termination signal. Per kill(1), if no signal is specified, the TERM signal is sent, the default action for this signal is to terminate the process, and this signal should be used in preference to the KILL signal (number 9). This machine's capture: kill -TERM to a background sleep, then wait, printed wait status=143, which is 128 plus 15.
SIGTERM asks first
SIGTERM is 15, action Term: terminate. kill with no number sends TERM, and kill(1) says it should be used in preference to KILL.
Why the courtesy? A process may install a handler for TERM: flush buffers, close files, say goodbye. Our capture: wait status=143, the shell writing 128 + 15 into $?.
TERM is a request. The next three pages cover the other fundamentals: the signal that never asks, the one from your keyboard, and the modem hangup.
Send TERM to a scratch sleep and read the wait status yourself before you reach for 9.
Per the Wikipedia article Signal (IPC), typing certain key combinations at the controlling terminal of a running process causes the system to send it certain signals, and Ctrl-C sends an INT signal. Per Linux man-pages signal(7), SIGINT's comment is Interrupt from keyboard. This machine's kernel turns the intr character into signal 2 for the foreground process group; the key mapping belongs to the tty layer, not the shell.
Ctrl+C sends SIGINT
Press Ctrl+C and the terminal driver, not the shell, turns the intr character into signal 2.
It is delivered to the whole foreground process group: editor, sort and pager in one pipeline all feel the same INT at once.
Wikipedia's Signal article lists the key combinations; stty -a shows yours. The next two pages give the same story for quit and stop.
Run stty -a and find the intr line; then try to name a key you could remap it to.
Per Linux man-pages signal(7), the signals SIGKILL and SIGSTOP cannot be caught, blocked, or ignored. Per sigaction(2), it is not possible to block SIGKILL or SIGSTOP by specifying them in sa_mask, and attempts to do so are silently ignored. Per kill(2), the only signals that can be sent to process ID 1, the init process, are those for which init has explicitly installed signal handlers. This machine's capture: a script that ignored TERM survived it, then died of KILL with final status=137.
You cannot catch 9
signal(7) says it once: SIGKILL and SIGSTOP cannot be caught, blocked, or ignored.
sigaction(2) adds the fine print: name them in sa_mask and the request is silently ignored. Our capture: a script ignoring TERM survived TERM, then fell to KILL, final status=137.
One exception: PID 1 may ignore what it likes, since kill(2) only delivers the signals init has handlers for.
Write a script that ignores TERM, prove it survives, then end it with 9.
Per the POSIX signal.h specification, SIG_DFL is a request for default signal handling, SIG_IGN is a request that signal be ignored, and SIG_ERR is the return value from signal() in case of error. Per Linux man-pages signal(7), during an execve, the dispositions of handled signals are reset to the default; the dispositions of ignored signals are left unchanged. This machine's capture: a script running trap with an empty TERM body survived TERM.
SIG_IGN and SIG_DFL
Two pointer values are not functions: SIG_DFL, default handling, and SIG_IGN, ignore. POSIX words them as requests.
The shell spelling of IGN is trap "" TERM; our capture survived TERM exactly that way. And the asymmetry that matters: across exec, caught resets, ignored persists.
That is why nohup works: it sets IGN before exec, and the child is born immune to hangups.
Prove the exec rule to yourself: exec a child from a script that ignored TERM and check its dispositions.
Per the Wikipedia article Exit status, when a command is terminated by a signal whose number is N, a shell sets the variable $? to a value greater than 128, most shells use 128+N, while ksh93 uses 256+N. Per kill(1), kill itself has exit status values 0 and beyond for its own failures. This machine's captures: HUP printed 129, INT 130, KILL 137, PIPE 141, TERM 143, USR1 138, SEGV 139.
Exit 128 plus N
A process cannot choose its own death status, so the shell invents one: 128 + the signal number.
Our table is all capture: HUP 129, INT 130, KILL 137, PIPE 141, TERM 143. Subtract 128 and you have the signal that did it.
137 is never a coincidence: it is always 9. ksh93 disagrees with 256 + N; everywhere else this arithmetic is law.
Kill a job with three different signals and verify each status against 128 plus the number.
Per Linux man-pages wait(2), if a wait is not performed, then the terminated child remains in a zombie state, and the kernel maintains a minimal set of information about the zombie process, the PID, termination status, and resource usage, to allow the parent to obtain information about the child. Per the Wikipedia article Zombie process, a zombie is a process that has completed execution but still has an entry in the process table, and the kill command has no effect on a zombie process. This machine's capture: before wait ps showed Z python3; after wait, the entry was gone.
Zombies and reaping
A zombie finished executing but keeps its table entry: pid, exit status, resource usage. Dead, not deleted.
wait(2) is the reaper: one call and the entry goes. Our capture: before wait Z, after wait gone. And kill does nothing: per Wikipedia, you cannot kill what is already dead.
SIGCHLD set to SIG_IGN stops zombies on Linux, but sigaction(2) calls a wait loop the only fully portable way.
Leave a zombie on purpose for thirty seconds, then reap it; feel how unhurried reaping is.
Per the Wikipedia article Nohup, nohup means no hang up; its purpose is to execute a command such that it ignores the HUP (hangup) signal and therefore does not stop when the user logs out, and output that would normally go to the terminal goes to a file called nohup.out, if it has not already been redirected. This machine's capture, run under a real pseudo-terminal so nohup.out would be created: nohup printed ignoring input and appending output to nohup.out, and the file held hello-from-nohup.
nohup ignores SIGHUP
nohup runs a command with SIGHUP already ignored, so the closing terminal cannot take it down.
Per Wikipedia, terminal-bound output goes to nohup.out. Our capture, under a real pseudo-terminal: nohup announced appending output to nohup.out, and the file held hello-from-nohup.
No hangup immunity, not immortality: KILL still ends the job, and stdout redirected elsewhere never meets nohup.out.
Run a short job under nohup with stdout already redirected, and confirm no nohup.out appears.