Embedded HandbookApps
All sections › XVII. Systems programming in C
· level intermediate · 15 min read
Open in the interactive handbook

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 init
signal, 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.

fork makes a copy, exec replaces the program, wait collects the status; without wait the child remains a zombie


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.

The stdio buffer is copied together with the process: into a pipe the line goes out twice, into a terminal once


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 (сигнал был, вызов перезапущен)
  1. kill(getpid(), SIGUSR1) — the handler has run even before kill returns.
  2. sigprocmask(SIG_BLOCK, …) — the signal is not lost but waits: sigpending sees it, and it is delivered at the moment the mask is removed. This is how critical sections are protected from signals.
  3. A blocking read from an empty pipe is interrupted by the alarm signal: without SA_RESTART it returns −1 with errno == EINTR. This applies to read, write, accept, wait, nanosleep and other slow calls — they are wrapped in a "retry on EINTR" loop. sleep interrupted by a signal returns the seconds it did not sleep, and pthread_cond_wait never returns EINTR at all — it is waited on in a while loop anyway (139.4).
  4. With SA_RESTART the same read continues 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


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

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.

Open in the interactive handbook