136. fork, exec, wait и сигналы
Новый процесс в Unix не «создаётся» — он раздваивается: fork делает
копию вызвавшего процесса, и дальше два одинаковых процесса идут каждый
своим путём. Чтобы в копии запустить другую программу, её образ заменяют
через exec. Родитель дожидается потомка через wait. Так работает
оболочка, так запускается всё в системе.
Вторая половина раздела — сигналы: единственный способ, которым ядро и
другие процессы прерывают программу посреди работы. С ними связано
больше тонких ошибок, чем с чем-либо ещё в этой части. Все прогоны —
_verify/sys136/ в образе stm-sys:1.
Как это называется в документации
Оригинал Перевод fork раздвоение процесса copy-on-write копирование при записи exec замена образа процесса другой программой wait, reap дождаться потомка и забрать его статус zombie зомби: завершившийся потомок, чей статус ещё не забрали orphan сирота: потомок пережил родителя — его усыновляет initsignal, handler сигнал, обработчик signal mask, pending маска заблокированных сигналов, ожидающие сигналы async-signal-safe безопасна для вызова из обработчика EINTR системный вызов прерван сигналом race condition гонка
136.1 fork: один процесс становится двумя
fork возвращает дважды: 0 — в потомке, номер потомка — в родителе, −1 —
если не удалось. Потомок получает копии данных, кучи и стека; код общий.
Программа меняет переменные в потомке и показывает, что у родителя они
прежние:
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
«Копия» — ленивая: ядро не копирует память, а помечает страницы общими
и копирует страницу только тогда, когда одна из сторон в неё пишет
(copy-on-write). Поэтому fork небольшого процесса дёшев:
1000 пар fork + wait в контейнере — 116 мс, по 116 мкс каждая.
Но таблицы страниц копируются честно: у процесса, занявшего гигабайт
памяти, одна пара fork + wait в том же контейнере — около 15 мс,
примерно в сто раз дольше, и потом каждая первая запись в страницу ещё
и копирует её. Чтобы из большого процесса запустить программу, берут
posix_spawn: он таблиц не копирует, и запуск /bin/true из того же
гигабайтного процесса занял 0,3 мс.
Потомок наследует открытые файлы вместе с позицией в них, uid, текущий
каталог, umask, маску сигналов, окружение, пределы. Не наследует:
pid, счётчики времени, блокировки файлов, таймеры и ожидающие сигналы.
136.2 Ловушка буфера
Буфер stdio — тоже часть данных, и fork копирует его вместе со всем
остальным. Если вывод идёт в канал, stdout буферизован целиком, и
строка, «напечатанная» до fork, лежит в буфере в обоих процессах:
printf("перед fork\n"); /* в канал stdout полностью буферизован */
if (fork() == 0) { printf("потомок\n"); return 0; }
wait(NULL);
printf("родитель\n");
./buffered_fork | cat
перед fork
потомок
перед fork
родитель
В терминале строка вышла бы один раз — там буфер строчный. Правило:
fflush(NULL) перед fork, или setbuf(stdout, NULL), как в первой
программе.
136.3 exec и wait
exec не создаёт процесс — он заменяет программу внутри существующего:
тот же pid, те же открытые файлы (кроме помеченных O_CLOEXEC), новый
код, данные и стек. Успешный exec не возвращается; вернулся — значит,
не удалось, и потомок должен выйти через _exit:
if (p == 0) { execlp("sh", "sh", "-c", "exit 5", (char *)0); perror("exec"); _exit(127); }
Шесть вариантов различаются только формой аргументов: l — списком,
v — массивом, p — искать по PATH, e — своё окружение. Внутри все —
execve.
wait возвращает статус, и разбирать его нужно макросами:
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)), "");
Четыре потомка — четыре исхода:
sh -c 'exit 5': завершился, код 5
exec несуществующей программы: завершился, код 127
raise(SIGKILL): убит сигналом 9 (Killed)
запись по нулевому указателю: убит сигналом 11 (Segmentation fault)
Код 127 — соглашение оболочки: «команда не найдена». Между exit и
гибелью от сигнала разница принципиальная: у убитого нет кода возврата.
waitpid с WNOHANG не блокируется, system(cmd) — это fork + exec sh -c
+ wait в одном вызове.
136.4 Зомби и сироты
Завершившийся потомок остаётся в таблице процессов, пока родитель не
заберёт его статус: это зомби. Памяти он не занимает, но номер
процесса держит. Программа завершает потомка и читает его состояние из
/proc:
State: Z (zombie)
после wait: /proc/55/status нет
Сервер, который порождает потомков и никогда не вызывает wait, рано или
поздно исчерпает pid_max (в контейнере прогона — 4 194 304; предел задаёт
система, на других он бывает намного меньше). Лечение — waitpid(-1, …,
WNOHANG) в цикле по SIGCHLD, либо signal(SIGCHLD, SIG_IGN): тогда ядро
убирает потомков само.
Если раньше умирает родитель, потомок становится сиротой, и его усыновляет
init (процесс 1), который и заберёт статус. На этом основана
демонизация (раздел 138).
136.5 Сигналы: что это
Сигнал — номер, который ядро доставляет процессу асинхронно: между любыми
двумя командами программы. Ядро передаёт управление обработчику, когда процесс
возвращается из ядра в пользовательский режим, а потом возвращает его обратно.
Источники три: терминал (SIGINT по Ctrl-C, SIGHUP при закрытии),
аппаратные ошибки (SIGSEGV, SIGBUS, SIGFPE, SIGILL) и программы —
kill, alarm, SIGPIPE, SIGCHLD.
| Сигнал | № | По умолчанию | Когда |
|---|---|---|---|
SIGHUP |
1 | завершить | терминал закрыт; демоны перечитывают настройки |
SIGINT |
2 | завершить | Ctrl-C |
SIGQUIT |
3 | core | Ctrl-\ |
SIGKILL |
9 | убить | нельзя перехватить и заблокировать |
SIGSEGV |
11 | core | обращение по плохому адресу |
SIGPIPE |
13 | завершить | запись в канал без читателя |
SIGALRM |
14 | завершить | таймер alarm |
SIGTERM |
15 | завершить | вежливая просьба завершиться; так шлёт kill без ключей |
SIGCHLD |
17 | игнорировать | потомок завершился |
SIGSTOP, SIGCONT |
19, 18 | остановить, продолжить | SIGSTOP тоже нельзя перехватить |
SIGUSR1, SIGUSR2 |
10, 12 | завершить | для программы |
Действие по умолчанию — одно из пяти: завершить, завершить с core,
игнорировать, остановить, продолжить. kill -l печатает список; номера
выше — для Linux на x86-64, на других архитектурах часть отличается.
136.6 sigaction, маска, EINTR
signal(2) устарел: его поведение при повторном сигнале и при
прерванных вызовах отличается между системами. Правильный вызов —
sigaction, и обработчик в нём — минимальный: выставить флаг типа
volatile sig_atomic_t и выйти.
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);
Четыре опыта в одной программе:
после 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)— обработчик отработал ещё до возврата изkill.sigprocmask(SIG_BLOCK, …)— сигнал не потерян, а ждёт:sigpendingего видит, и он доставляется в момент снятия маски. Так защищают критические участки от сигнала.- Блокирующий
readиз пустого канала прерван сигналомalarm: безSA_RESTARTон возвращает −1 сerrno == EINTR. Это касаетсяread,write,accept,wait,nanosleepи других медленных вызовов — их оборачивают в цикл «повторить, еслиEINTR».sleep, прерванный сигналом, возвращает недоспанные секунды, аpthread_cond_waitEINTRне возвращает вовсе — его и так ждут в циклеwhile(139.4). - С
SA_RESTARTтот жеreadпосле сигнала продолжается и получает байт, который потомок записал через полторы секунды.
Что можно делать в обработчике — короткий список: писать в
sig_atomic_t, вызывать write, _exit, kill, sigaction и другие
функции из списка async-signal-safe. Что нельзя: printf, malloc,
free, всё, что берёт блокировку или трогает глобальное состояние.
Обработчик, вызванный посреди malloc, войдёт в malloc повторно и
разрушит кучу. errno внутри обработчика сохраняют и восстанавливают.
136.7 Приёмы
- Флаг и главный цикл. Обработчик выставляет флаг, цикл его проверяет.
Чтобы не потерять сигнал между проверкой и ожиданием, ждут через
sigsuspendс временно снятой маской или черезpselect/ppoll. - Сигнал как дескриптор.
signalfdпревращает сигналы в читаемый дескриптор — их ждут вместе с сокетами вpoll(раздел 141). - Таймер.
alarm(секунды)— одинSIGALRM; точнее —setitimer,timer_createилиtimerfd. - Своё убийство.
raise(sig)— сигнал себе;abort()—SIGABRTиcore. - Игнорировать
SIGPIPEв сетевых программах: запись в закрытое соединение тогда возвращаетEPIPE, а не убивает процесс (раздел 78).
136.8 Грабли
| Симптом | Причина |
|---|---|
строка до fork напечаталась дважды |
буфер stdout скопирован; fflush(NULL) перед fork |
после exec выполнился код «после exec» |
exec не удался и вернулся; после него — perror и _exit |
потомки-зомби копятся, fork даёт EAGAIN |
родитель не вызывает wait; SIGCHLD или SIG_IGN |
read вернул −1 без видимой причины |
EINTR: сигнал прервал вызов — повторить или SA_RESTART |
| программа виснет или падает в обработчике сигнала | в обработчике printf/malloc — только async-signal-safe |
| сигнал «потерялся» | он не ставится в очередь: два одинаковых до доставки — один; блокировать маской, а не терять; сигналы реального времени SIGRTMIN…SIGRTMAX ставятся в очередь и несут число (sigqueue) |
SIGKILL не перехватывается |
и не должен: SIGKILL и SIGSTOP нельзя ни перехватить, ни заблокировать |
kill без ключа не завершил процесс |
SIGTERM перехвачен или игнорируется; kill -9 — SIGKILL |
136.9 Итоги
forkвозвращает дважды; копия ленивая (copy-on-write): 116 мкс на паруfork+waitу небольшого процесса, около 15 мс у гигабайтного; из большого процесса программу запускают черезposix_spawn.- Перед
fork—fflush; после неудачногоexec—_exit. waitразбирают макросами: код 0–255 или номер сигнала; 127 — «не найдено».- Зомби — потомок без
wait; сироту усыновляетinit. sigaction, а неsignal; обработчик — флагsig_atomic_t; маска задерживает сигнал,EINTRтребует повтора,SA_RESTARTперезапускает.SIGKILLиSIGSTOPне перехватить;SIGPIPEв сетевых программах игнорировать.
Полные исходники
fork_cow.c — fork копирует данные, переменные родителя и потомка расходятся.
/* Раздел 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 — буфер stdout копируется вместе с процессом.
/* Раздел 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 и wait со статусом потомка по битам.
/* Раздел 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, маска, EINTR и 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 — что возвращают read, sleep и pthread_cond_wait, прерванные сигналом.
/* 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;
}
Дальше: раздел 137 — обмен между процессами: каналы, сокеты, общая память.