136. fork, exec, wait and signals
In Unix a new process is not "created" — it splits in two: fork makes
a copy of the calling process, and from then on two identical processes each go
their own way. To run a different program in the copy, its image is replaced
with exec. The parent waits for the child with wait. This is how
the shell works, and how everything in the system is started.
The second half of the section is about signals: the only way the kernel and
other processes interrupt a program in the middle of its work. They cause
more subtle bugs than anything else in this part. All the runs are in
_verify/sys136/ in the stm-sys:1 image.
Terms used in the documentation
Term Meaning fork splitting a process in two copy-on-write copying a page only when it is written exec replacing the process image with another program wait, reap wait for a child and collect its status zombie a child that has terminated but whose status has not been collected yet orphan a child that outlived its parent — it is adopted by initsignal, handler an asynchronous notification, the function that handles it signal mask, pending the set of blocked signals, the signals waiting for delivery async-signal-safe safe to call from a signal handler EINTR a system call was interrupted by a signal race condition the result depends on the timing of concurrent actions
136.1 fork: one process becomes two
fork returns twice: 0 in the child, the child's number in the parent, −1
if it failed. The child gets copies of the data, heap and stack; the code is shared.
The program changes variables in the child and shows that in the parent they
stay the same:
printf("перед fork: pid %ld\n", (long)getpid());
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) { /* потомок */
globvar++; var++;
printf("потомок: pid %ld ppid %ld globvar %d var %d\n",
(long)getpid(), (long)getppid(), globvar, var);
_exit(42);
}
int status;
waitpid(pid, &status, 0); /* родитель ждёт — потомок не станет зомби */
printf("родитель: pid %ld globvar %d var %d; потомок вернул %d\n",
(long)getpid(), globvar, var, WEXITSTATUS(status));
перед fork: pid 54
потомок: pid 55 ppid 54 globvar 7 var 89
родитель: pid 54 globvar 6 var 88; потомок вернул 42
The "copy" is lazy: the kernel does not copy the memory but marks the pages as shared
and copies a page only when one of the sides writes to it
(copy-on-write). That is why fork of a small process is cheap:
1000 pairs of fork + wait in the container take 116 ms, 116 µs each.
But the page tables are copied in full: for a process that has taken a gigabyte
of memory, one pair of fork + wait in the same container takes about 15 ms,
roughly a hundred times longer, and after that every first write to a page also
copies it. To start a program from a large process, you use
posix_spawn: it does not copy the tables, and starting /bin/true from the same
gigabyte process took 0.3 ms.
The child inherits open files together with their positions, uid, the current
directory, umask, the signal mask, the environment, the limits. It does not inherit:
the pid, the time counters, file locks, timers and pending signals.
136.2 The buffer trap
The stdio buffer is also part of the data, and fork copies it along with everything
else. If the output goes to a pipe, stdout is fully buffered, and
a line "printed" before fork sits in the buffer in both processes:
printf("перед fork\n"); /* в канал stdout полностью буферизован */
if (fork() == 0) { printf("потомок\n"); return 0; }
wait(NULL);
printf("родитель\n");
./buffered_fork | cat
перед fork
потомок
перед fork
родитель
In a terminal the line would come out once — there the buffer is line-buffered. The rule:
fflush(NULL) before fork, or setbuf(stdout, NULL), as in the first
program.
136.3 exec and wait
exec does not create a process — it replaces the program inside an existing one:
the same pid, the same open files (except those marked O_CLOEXEC), new
code, data and stack. A successful exec does not return; if it returned, it
failed, and the child must exit through _exit:
if (p == 0) { execlp("sh", "sh", "-c", "exit 5", (char *)0); perror("exec"); _exit(127); }
The six variants differ only in the form of the arguments: l — as a list,
v — as an array, p — search PATH, e — your own environment. Underneath, all of them are
execve.
wait returns a status, and it has to be decoded with macros:
if (WIFEXITED(status))
printf("%s: завершился, код %d\n", what, WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("%s: убит сигналом %d (%s)%s\n", what, WTERMSIG(status),
strsignal(WTERMSIG(status)), "");
Four children — four outcomes:
sh -c 'exit 5': завершился, код 5
exec несуществующей программы: завершился, код 127
raise(SIGKILL): убит сигналом 9 (Killed)
запись по нулевому указателю: убит сигналом 11 (Segmentation fault)
Code 127 is a shell convention: "command not found". There is a fundamental difference between exit and
death by a signal: a killed process has no exit code.
waitpid with WNOHANG does not block; system(cmd) is fork + exec sh -c
+ wait in one call.
136.4 Zombies and orphans
A terminated child stays in the process table until the parent
collects its status: this is a zombie. It takes no memory, but it holds on to its
process number. The program terminates a child and reads its state from
/proc:
State: Z (zombie)
после wait: /proc/55/status нет
A server that spawns children and never calls wait will sooner or
later exhaust pid_max (in the container of the run it is 4194304; the limit is set by
the system, and on others it can be much smaller). The cure is waitpid(-1, …,
WNOHANG) in a loop on SIGCHLD, or signal(SIGCHLD, SIG_IGN): then the kernel
removes the children itself.
If the parent dies first, the child becomes an orphan and is adopted by
init (process 1), which will collect its status. Daemonization
is based on this (section 138).
136.5 Signals: what they are
A signal is a number that the kernel delivers to a process asynchronously: between any
two instructions of the program. The kernel passes control to the handler when the process
returns from the kernel to user mode, and then gives control back.
There are three sources: the terminal (SIGINT on Ctrl-C, SIGHUP when it is closed),
hardware errors (SIGSEGV, SIGBUS, SIGFPE, SIGILL) and programs —
kill, alarm, SIGPIPE, SIGCHLD.
| Signal | No. | Default | When |
|---|---|---|---|
SIGHUP |
1 | terminate | the terminal was closed; daemons reread their settings |
SIGINT |
2 | terminate | Ctrl-C |
SIGQUIT |
3 | core | Ctrl-\ |
SIGKILL |
9 | kill | cannot be caught or blocked |
SIGSEGV |
11 | core | access to a bad address |
SIGPIPE |
13 | terminate | writing to a pipe with no reader |
SIGALRM |
14 | terminate | the alarm timer |
SIGTERM |
15 | terminate | a polite request to terminate; this is what kill sends without options |
SIGCHLD |
17 | ignore | a child terminated |
SIGSTOP, SIGCONT |
19, 18 | stop, continue | SIGSTOP cannot be caught either |
SIGUSR1, SIGUSR2 |
10, 12 | terminate | for the program's own use |
The default action is one of five: terminate, terminate with core,
ignore, stop, continue. kill -l prints the list; the numbers
above are for Linux on x86-64, on other architectures some of them differ.
136.6 sigaction, the mask, EINTR
signal(2) is obsolete: its behavior on a repeated signal and on
interrupted calls differs between systems. The right call is
sigaction, and the handler in it is minimal: set a flag of type
volatile sig_atomic_t and return.
static volatile sig_atomic_t got_usr1, got_alarm;
static void on_sig(int signo)
{
if (signo == SIGUSR1) got_usr1 = 1;
if (signo == SIGALRM) got_alarm = 1;
}
struct sigaction sa = { .sa_handler = on_sig };
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; /* без SA_RESTART: read вернёт EINTR */
sigaction(SIGUSR1, &sa, NULL);
sigaction(SIGALRM, &sa, NULL);
Four experiments in one program:
после kill: got_usr1 = 1
заблокирован: got_usr1 = 0, в ожидании = 1
маска снята: got_usr1 = 1
read после alarm: n = -1, errno = Interrupted system call, got_alarm = 1
read с SA_RESTART: n = 1, символ x, got_alarm = 1 (сигнал был, вызов перезапущен)
kill(getpid(), SIGUSR1)— the handler has run even beforekillreturns.sigprocmask(SIG_BLOCK, …)— the signal is not lost but waits:sigpendingsees it, and it is delivered at the moment the mask is removed. This is how critical sections are protected from signals.- A blocking
readfrom an empty pipe is interrupted by thealarmsignal: withoutSA_RESTARTit returns −1 witherrno == EINTR. This applies toread,write,accept,wait,nanosleepand other slow calls — they are wrapped in a "retry onEINTR" loop.sleepinterrupted by a signal returns the seconds it did not sleep, andpthread_cond_waitnever returnsEINTRat all — it is waited on in awhileloop anyway (139.4). - With
SA_RESTARTthe samereadcontinues after the signal and receives the byte that the child wrote a second and a half later.
What you may do in a handler is a short list: write to a
sig_atomic_t, call write, _exit, kill, sigaction and other
functions from the async-signal-safe list. What you must not do: printf, malloc,
free, anything that takes a lock or touches global state.
A handler called in the middle of malloc will enter malloc again and
corrupt the heap. errno is saved and restored inside the handler.
136.7 Techniques
- A flag and the main loop. The handler sets a flag, the loop checks it.
To avoid losing a signal between the check and the wait, you wait with
sigsuspendwith the mask temporarily removed, or withpselect/ppoll. - A signal as a descriptor.
signalfdturns signals into a readable descriptor — they are waited for together with sockets inpoll(section 141). - A timer.
alarm(seconds)gives oneSIGALRM; more precise aresetitimer,timer_createortimerfd. - Killing yourself.
raise(sig)sends a signal to yourself;abort()givesSIGABRTandcore. - Ignore
SIGPIPEin network programs: a write to a closed connection then returnsEPIPEinstead of killing the process (section 78).
136.8 Pitfalls
| Symptom | Cause |
|---|---|
a line from before fork was printed twice |
the stdout buffer was copied; fflush(NULL) before fork |
after exec, the code "after exec" ran |
exec failed and returned; after it — perror and _exit |
zombie children pile up, fork gives EAGAIN |
the parent does not call wait; SIGCHLD or SIG_IGN |
read returned −1 for no visible reason |
EINTR: a signal interrupted the call — retry or use SA_RESTART |
| the program hangs or crashes in a signal handler | printf/malloc in the handler — only async-signal-safe calls |
| a signal "got lost" | signals are not queued: two identical ones before delivery become one; block with the mask instead of losing them; real-time signals SIGRTMIN…SIGRTMAX are queued and carry a number (sigqueue) |
SIGKILL cannot be caught |
and it should not be: SIGKILL and SIGSTOP can be neither caught nor blocked |
kill without an option did not terminate the process |
SIGTERM is caught or ignored; kill -9 sends SIGKILL |
136.9 Summary
forkreturns twice; the copy is lazy (copy-on-write): 116 µs per pair offork+waitfor a small process, about 15 ms for a gigabyte one; from a large process a program is started withposix_spawn.- Before
fork—fflush; after a failedexec—_exit. waitis decoded with macros: a code 0–255 or a signal number; 127 means "not found".- A zombie is a child without
wait; an orphan is adopted byinit. sigaction, notsignal; the handler sets asig_atomic_tflag; the mask delays a signal,EINTRrequires a retry,SA_RESTARTrestarts the call.SIGKILLandSIGSTOPcannot be caught; ignoreSIGPIPEin network programs.
Full sources
fork_cow.c — fork copies the data, the parent's and the child's variables diverge.
/* Раздел 136: fork копирует данные (copy-on-write), код общий; переменные расходятся. */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int globvar = 6;
int main(void)
{
int var = 88;
setbuf(stdout, NULL); /* без буфера: иначе строка «перед fork» уедет дважды */
printf("перед fork: pid %ld\n", (long)getpid());
pid_t pid = fork();
if (pid < 0) { perror("fork"); return 1; }
if (pid == 0) { /* потомок */
globvar++; var++;
printf("потомок: pid %ld ppid %ld globvar %d var %d\n",
(long)getpid(), (long)getppid(), globvar, var);
_exit(42);
}
int status;
waitpid(pid, &status, 0); /* родитель ждёт — потомок не станет зомби */
printf("родитель: pid %ld globvar %d var %d; потомок вернул %d\n",
(long)getpid(), globvar, var, WEXITSTATUS(status));
return 0;
}
buffered_fork.c — the stdout buffer is copied together with the process.
/* Раздел 136: буфер stdout копируется вместе с процессом — «перед fork» печатается дважды. */
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(void)
{
printf("перед fork\n"); /* в канал stdout полностью буферизован */
if (fork() == 0) { printf("потомок\n"); return 0; }
wait(NULL);
printf("родитель\n");
return 0;
}
exec_wait.c — fork, exec and wait with the child's status decoded bit by bit.
/* Раздел 136: fork + exec + wait — так работает любая оболочка; статус потомка по битам. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <sys/wait.h>
static void report(const char *what, int status)
{
if (WIFEXITED(status))
printf("%s: завершился, код %d\n", what, WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("%s: убит сигналом %d (%s)%s\n", what, WTERMSIG(status),
strsignal(WTERMSIG(status)), "");
}
int main(void)
{
pid_t p = fork();
if (p == 0) { execlp("sh", "sh", "-c", "exit 5", (char *)0); perror("exec"); _exit(127); }
int st; waitpid(p, &st, 0); report("sh -c 'exit 5'", st);
p = fork();
if (p == 0) { execlp("no_such_program_xyz", "x", (char *)0); _exit(127); }
waitpid(p, &st, 0); report("exec несуществующей программы", st);
p = fork();
if (p == 0) { raise(SIGKILL); }
waitpid(p, &st, 0); report("raise(SIGKILL)", st);
p = fork();
if (p == 0) { int *z = 0; *z = 1; _exit(0); }
waitpid(p, &st, 0); report("запись по нулевому указателю", st);
return 0;
}
signals.c — sigaction, the mask, EINTR and alarm.
/* Раздел 136: sigaction, маска, EINTR, alarm; обработчик только выставляет флаг. */
#define _DEFAULT_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <unistd.h>
#include <errno.h>
static volatile sig_atomic_t got_usr1, got_alarm;
static void on_sig(int signo)
{
if (signo == SIGUSR1) got_usr1 = 1;
if (signo == SIGALRM) got_alarm = 1;
}
int main(void)
{
struct sigaction sa = { .sa_handler = on_sig };
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; /* без SA_RESTART: read вернёт EINTR */
sigaction(SIGUSR1, &sa, NULL);
sigaction(SIGALRM, &sa, NULL);
/* 1. Сигнал самому себе через kill: обработчик отработал до возврата из kill. */
kill(getpid(), SIGUSR1);
printf("после kill: got_usr1 = %d\n", got_usr1);
/* 2. Заблокировать SIGUSR1: сигнал ждёт, пока маску не снимут. */
sigset_t set, old;
sigemptyset(&set); sigaddset(&set, SIGUSR1);
sigprocmask(SIG_BLOCK, &set, &old);
got_usr1 = 0;
kill(getpid(), SIGUSR1);
sigset_t pend; sigpending(&pend);
printf("заблокирован: got_usr1 = %d, в ожидании = %d\n", got_usr1, sigismember(&pend, SIGUSR1));
sigprocmask(SIG_SETMASK, &old, NULL);
printf("маска снята: got_usr1 = %d\n", got_usr1);
/* 3. alarm прерывает блокирующий read: без SA_RESTART — EINTR. */
int fd[2]; pipe(fd);
char c;
alarm(1);
ssize_t n = read(fd[0], &c, 1);
printf("read после alarm: n = %zd, errno = %s, got_alarm = %d\n", n, strerror(errno), got_alarm);
/* 4. Тот же read с SA_RESTART — перезапустится, поэтому пишем в канал из обработчика нельзя;
показываем только, что флаг SA_RESTART меняет поведение: read не вернётся, пока нет данных. */
sa.sa_flags = SA_RESTART; sigaction(SIGALRM, &sa, NULL);
if (fork() == 0) { usleep(1500000); write(fd[1], "x", 1); _exit(0); }
alarm(1); got_alarm = 0;
n = read(fd[0], &c, 1);
printf("read с SA_RESTART: n = %zd, символ %c, got_alarm = %d (сигнал был, вызов перезапущен)\n", n, c, got_alarm);
return 0;
}
eintr136.c — what read, sleep and pthread_cond_wait return when interrupted by a signal.
/* 136.6: что возвращают вызовы, прерванные сигналом без SA_RESTART:
* read — −1 и EINTR, sleep — недоспанные секунды, pthread_cond_wait — не EINTR никогда. */
#define _GNU_SOURCE
#include <errno.h>
#include <pthread.h>
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <time.h>
#include <unistd.h>
static volatile sig_atomic_t usr1;
static void on_sig(int s) { if (s == SIGUSR1) usr1++; }
static pthread_mutex_t mu = PTHREAD_MUTEX_INITIALIZER;
static pthread_cond_t cv = PTHREAD_COND_INITIALIZER;
static int ready, rc_wait = -1;
static double t_wait;
static double now(void) { struct timespec t; clock_gettime(CLOCK_MONOTONIC, &t); return t.tv_sec + t.tv_nsec / 1e9; }
static void *waiter(void *arg)
{
double t0 = now();
(void)arg;
pthread_mutex_lock(&mu);
while (!ready) {
rc_wait = pthread_cond_wait(&cv, &mu);
if (rc_wait != 0) break; /* код ошибки, если бы он был */
}
t_wait = now() - t0;
pthread_mutex_unlock(&mu);
return NULL;
}
int main(void)
{
struct sigaction sa = { .sa_handler = on_sig };
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0; /* без SA_RESTART */
sigaction(SIGALRM, &sa, NULL);
sigaction(SIGUSR1, &sa, NULL);
int p[2]; char c;
pipe(p);
alarm(1);
ssize_t n = read(p[0], &c, 1);
printf("read: n = %zd, errno = %s\n", n, n < 0 ? strerror(errno) : "-");
alarm(1);
unsigned left = sleep(5);
printf("sleep(5): вернул %u\n", left);
pthread_t th;
pthread_create(&th, NULL, waiter, NULL);
usleep(200000);
for (int k = 0; k < 5; k++) { pthread_kill(th, SIGUSR1); usleep(100000); }
int before = usr1;
usleep(500000);
pthread_mutex_lock(&mu); ready = 1; pthread_cond_signal(&cv); pthread_mutex_unlock(&mu);
pthread_join(th, NULL);
printf("pthread_cond_wait: сигналов SIGUSR1 %d, код %d (%s), ждал %.1f с\n", before, rc_wait,
rc_wait == EINTR ? "EINTR" : rc_wait == 0 ? "0" : "другой", t_wait);
return 0;
}
Next: section 137 — communication between processes: pipes, sockets, shared memory.