Process Control: fork, exec, wait, signals¶
A process is a running program. On Linux it’s a task_struct in the kernel with an address space, a file descriptor table, a signal disposition table, credentials, and a scheduling entity. In this file you learn how processes are created, replaced, reaped, and signaled — the four verbs that define Unix process management. Get these right and you can build a shell, a daemon, a supervisor, a container runtime, or an inference-worker pool.
fork() — the copy-on-write clone¶
fork() creates an almost-identical copy of the calling process. Same code, same data, same fds, same everything — except:
The child gets a new PID;
fork()returns0in the child and the child’s PID in the parent.On failure
fork()returns-1in the parent, no child is created.Modern Linux uses copy-on-write: the child’s pages point at the parent’s until either writes, then the kernel copies. This makes
forkcheap enough for shells and expensive for huge-memory processes (a 30GB inference server forking is not free — you get COW overhead and possibly overcommit issues).
pid_t pid = fork();
if (pid < 0) { perror("fork"); exit(1); }
else if (pid == 0) {
/* child */
execvp("ls", (char*[]){"ls", "-l", NULL});
_exit(127); // exec failed; use _exit not exit in child after fork
} else {
/* parent */
int status;
waitpid(pid, &status, 0);
}
Two things to internalize.
Use
_exit, notexit, in a child that has forked but not yet exec’d.exit()runsatexithandlers and flushes stdio buffers — which may double-flush the parent’s buffered output (both processes now have a copy of the buffer)._exitskips all that and goes straight to the kernel.forkin a multi-threaded program is a minefield. Only the calling thread continues in the child. Any mutex held by another thread at fork time is now locked forever in the child. This is whyposix_spawnexists, and why in a threaded server you fork-then-exec immediately with no work between them.
The exec family — replacing yourself¶
exec* replaces the current process image with a new program. Same PID, same fd table (unless O_CLOEXEC), same PPID, but everything about the running code is discarded and reloaded.
Variant |
Argv |
Env |
Path search |
|---|---|---|---|
|
list |
inherited |
no |
|
list |
inherited |
yes ( |
|
list |
explicit |
no |
|
vector |
inherited |
no |
|
vector |
inherited |
yes |
|
vector |
explicit |
yes |
Mnemonic: l=list, v=vector, p=path search, e=env.
If exec* returns, it failed. Otherwise it never returns.
wait, waitpid, and zombies¶
When a child exits, the kernel keeps a tiny record of it (exit status, resource usage) until the parent reaps it. That record is a zombie. A process that exits and has children left running becomes those children’s PPID = 1 (the init process, e.g., systemd), and init reaps them — those are orphans, not zombies.
int status;
pid_t child = waitpid(pid, &status, 0); // block until this specific child
pid_t any = waitpid(-1, &status, WNOHANG); // any child, don't block
if (WIFEXITED(status)) printf("exit code %d\n", WEXITSTATUS(status));
if (WIFSIGNALED(status)) printf("killed by signal %d\n", WTERMSIG(status));
Zombie prevention patterns:
If you don’t care about exit status:
signal(SIGCHLD, SIG_IGN)on Linux/BSD tells the kernel to auto-reap. But this is signal-based magic and disableswaitfor you.Better: install a
SIGCHLDhandler that loopswaitpid(-1, ..., WNOHANG)until it returns 0. That handles the case where multiple children exit simultaneously (signals coalesce).Best for daemons:
daemon(3)+prctl(PR_SET_CHILD_SUBREAPER, 1)to become a subreaper for orphans.
Signals — the async part of Unix¶
A signal is a software interrupt delivered to a process (or a specific thread). The kernel or another process sends it; the receiver’s signal handler runs at some arbitrary point between two normal instructions.
Signals you must know: SIGINT (Ctrl-C), SIGTERM (polite kill), SIGKILL (uncatchable kill), SIGSTOP/SIGCONT (pause/resume, uncatchable), SIGCHLD (child died), SIGPIPE (wrote to a closed pipe/socket — default is terminate, almost always wrong), SIGSEGV (invalid memory), SIGBUS (misaligned or mmap I/O error), SIGHUP (terminal hangup or config-reload convention), SIGUSR1/SIGUSR2 (application-defined).
sigaction, not signal¶
The signal(3) function’s semantics are underspecified across Unix versions. On some, the handler is reset to default after being called once (SysV semantics). On others it’s persistent (BSD semantics). It doesn’t let you specify which signals to block during handler execution. It doesn’t reliably give you SA_RESTART. Do not use signal() in new code except for SIG_IGN / SIG_DFL restore.
Use sigaction:
struct sigaction sa = {0};
sa.sa_handler = handle_sigint;
sigemptyset(&sa.sa_mask);
sigaddset(&sa.sa_mask, SIGTERM); // block SIGTERM while SIGINT is being handled
sa.sa_flags = SA_RESTART; // auto-restart slow syscalls
sigaction(SIGINT, &sa, NULL);
The three specific things sigaction gives you that signal doesn’t:
Guaranteed persistent handler (no re-installation dance).
sa_mask— signals to block during handler execution, preventing nested-signal races.Flags:
SA_RESTART(auto-restart EINTR’d syscalls),SA_SIGINFO(extended handler withsiginfo_tgiving sender PID, faulting address, etc.),SA_NOCLDWAIT/SA_NOCLDSTOPforSIGCHLDtuning.
What you can do inside a signal handler¶
Almost nothing. The handler runs at an arbitrary point in your program — possibly inside malloc, inside printf, inside anything holding a lock. Only async-signal-safe functions are legal to call. man 7 signal-safety lists them; the short list you’ll actually use: write, _exit, sig_atomic_t reads/writes, sem_post.
Never call printf, malloc, fprintf, exit, or any stdio inside a signal handler. It compiles. It seems to work. It will deadlock in production at 3 AM. The correct pattern is:
volatile sig_atomic_t got_sigint = 0;
void handler(int sig) { got_sigint = 1; }
/* main loop */
while (!got_sigint) { /* work */ }
Or the self-pipe / signalfd patterns below.
signalfd — Linux’s escape hatch¶
Signal handlers are miserable to work with. signalfd() turns a signal into a readable file descriptor, so you can wait for it in your main event loop alongside network and file fds.
sigset_t mask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGCHLD);
sigprocmask(SIG_BLOCK, &mask, NULL); // block; signalfd will consume
int sfd = signalfd(-1, &mask, SFD_CLOEXEC | SFD_NONBLOCK);
// now add sfd to epoll/poll and read struct signalfd_siginfo from it
This is a huge quality-of-life upgrade for servers. Same principle: timerfd for timers, eventfd for cross-thread wakeups. In Phase 5 you’ll wire all three into an epoll loop.
The self-pipe trick — the pre-signalfd standard¶
Before signalfd, the portable way to poll for signals was:
Create a pipe.
Signal handler
writes one byte to the pipe (write is async-signal-safe).Main loop’s
select/pollwakes up when the pipe becomes readable, then processes the signal.
int pfd[2];
pipe2(pfd, O_CLOEXEC | O_NONBLOCK);
void handler(int sig) { char c = (char)sig; write(pfd[1], &c, 1); }
/* select on pfd[0] as one of your read fds */
Still useful on non-Linux (macOS, BSDs) where signalfd doesn’t exist. On Linux, prefer signalfd.
What most people get wrong about this¶
They install a signal handler that calls printf “just for debugging” and never remove it. Then in production the process hangs and no one knows why. The stack trace shows printf → flockfile → stuck. This is the single most common signal-handling bug in the wild. Rule: signal handler sets a flag, main loop does the work. Or use signalfd. Nothing else.
Practice this week¶
Write a program that forks 3 children, each sleeping for a random 1-5 seconds and exiting. The parent installs a
SIGCHLDhandler that sets a flag; main loop reaps in awhile (waitpid(-1, &status, WNOHANG) > 0)loop. Verify no zombies withpsmid-run.Write a
system()replacement:int mysystem(const char *cmd)that forks, execssh -c cmd, waits, returns the status. Handle EINTR.Take your favorite blocking program and add a
SIGINThandler that lets it exit cleanly (drain buffers, close fds, print summary). Use both thevolatile sig_atomic_tpattern and (separately)signalfd. Notice the difference in ergonomics.Read
man 7 signalcover to cover. Yes, all of it. Once. You’ll come back for reference for the rest of your life.
References¶
man 2 fork,man 2 execve,man 2 waitpid,man 2 sigaction,man 2 signalfd,man 7 signal,man 7 signal-safety.APUE ch. 8 (Process Control), ch. 10 (Signals).
TLPI ch. 20-27 (comprehensive on signals; ch. 22 for
signalfd).Bryan Cantrill’s talks on signal-handling war stories — entertaining and instructive.
Return to README.md · Next: 04_ipc_pipes_shm_sockets.md