Part 5 · 1 chapters · ~8 min

Processes and Signals

Process creation with fork and exec, copy-on-write, waitpid, exit statuses and zombies, environment and arguments, signals and handlers, async-signal safety, the self-pipe trick, SIGTERM and graceful shutdown, process groups and job control, and posix_spawn.

6

fork, exec, wait

code
pid_t pid = fork();
if (pid < 0) { perror("fork"); exit(1); }
if (pid == 0) { execvp(argv[0], argv); perror("execvp"); _exit(127); }     // only returns on failure
int status; waitpid(pid, &status, 0);
if (WIFEXITED(status)) printf("exit %d\n", WEXITSTATUS(status));

// signals: a handler may only call async-signal-safe functions (no printf, no malloc)
static volatile sig_atomic_t stop = 0;
static void on_term(int sig) { (void)sig; stop = 1; }
struct sigaction sa = { .sa_handler = on_term }; sigemptyset(&sa.sa_mask);
sigaction(SIGTERM, &sa, NULL);
while (!stop) serve_one();       // main loop checks the flag; drain and exit cleanly

posix_spawn creates and execs in one call and avoids copying page tables for large parents; runtimes such as Node's libuv and Go use spawn-style paths when starting children. On the same machine, the OS course measured Node's spawnSync('/usr/bin/true') at 1.53 ms per process, against 486 µs for a bare fork, exit and wait in C: the difference is exec, loading a program, and Node's own bookkeeping.

HOW A SHELL RUNS A COMMAND
fork, exec, wait, and a signal
shellkernelchildfork()copy of the shell (copy-on-write)
swipe the figure sideways, or tap expand for full screen
1/4
fork
fork creates a near-identical child process; memory is shared copy-on-write, so the copy is cheap until either side writes. Measured here: fork, exit and wait took 486 µs each.
clone the process486 µs measured, COW