exec, exit и wait: зомби и сироты

⏱ 30 мин · 🧩 заданий: 6

exec: заменить программу

exec загружает в текущий процесс новую программу. PID остаётся прежним, а код, данные, стек и куча заменяются.

execl("/bin/ls", "ls", "-l", (char *)NULL);          /* список аргументов */
execlp("ls", "ls", "-l", (char *)NULL);              /* p — искать в PATH */
char *args[] = {"ls", "-l", NULL};
execv("/bin/ls", args);                               /* v — массив */
execvp("ls", args);
execve("/bin/ls", args, envp);                        /* e — своё окружение; это и есть системный вызов */
perror("exec");                                       /* сюда попадаем только при ошибке */
Сохраняется Сбрасывается
PID, PPID, сеанс, группа адресное пространство (код, данные, стек, куча, отображения)
открытые дескрипторы (без FD_CLOEXEC) обработчики сигналов → SIG_DFL
маска сигналов, SIG_IGN, ожидающие сигналы разделяемая память mmap
текущий каталог, UID/GID, лимиты потоки (остаётся один)

fork + exec

Так shell запускает команды:

pid_t pid = fork();
if (pid == 0) {
    execvp(argv[0], argv);
    perror("exec");
    _exit(127);            /* exec не удался */
}
waitpid(pid, &status, 0);

Между fork и exec потомок может настроить окружение: перенаправить дескрипторы (dup2), сменить каталог, сигналы — так работают >, |, & в shell.

Завершение процесса

Процесс завершается:

  • return из main или вызов exit(code) — нормально;
  • _exit(code) — нормально, без сброса буферов stdio;
  • по сигналу (SIGKILL, SIGSEGV, …) — аварийно.

Что делает ядро при завершении:

  1. закрывает все файловые дескрипторы, освобождает адресное пространство, снимает отображения;
  2. всех живых потомков передаёт новому родителю — init (или subreaper);
  3. переводит процесс в состояние зомби: от него остаётся только task_struct с кодом завершения и статистикой;
  4. посылает родителю сигнал SIGCHLD.

wait и waitpid

#include <sys/wait.h>

int status;
pid_t done = wait(&status);              /* ждать любого потомка */
pid_t p = waitpid(pid, &status, 0);      /* ждать конкретного */
waitpid(-1, &status, WNOHANG);           /* проверить без ожидания */

if (WIFEXITED(status))   printf("код %d\n", WEXITSTATUS(status));
if (WIFSIGNALED(status)) printf("убит сигналом %d\n", WTERMSIG(status));
if (WIFSTOPPED(status))  printf("остановлен сигналом %d\n", WSTOPSIG(status));

wait(NULL) — дождаться любого потомка, статус не нужен. Если потомков нет, wait сразу вернёт −1 с errno = ECHILD.

wait ждёт только детей

Не внуков и не «всех процессов, созданных программой». Если нужно дождаться внуков, придётся синхронизироваться иначе: каналом, семафором, сигналом.

Зомби

Зомби — процесс, который завершился, но родитель ещё не забрал его статус через wait. Память освобождена, но запись в таблице процессов (PID, код завершения) занята.

$ ps -eo pid,ppid,stat,comm | grep Z
 4242  4241 Z+   prog <defunct>

Много зомби — исчерпание PID. Лечится: родитель вызывает wait (часто — в обработчике SIGCHLD) или игнорирует SIGCHLD явно (signal(SIGCHLD, SIG_IGN) — тогда в Linux зомби не создаются). Если завершится сам родитель, зомби усыновит init и тут же соберёт.

Сироты

Сирота — процесс, родитель которого завершился. Ядро «переподвешивает» его к init (PID 1) или к ближайшему предку, объявившему себя subreaper (prctl(PR_SET_CHILD_SUBREAPER); так делает systemd --user). Поэтому getppid() у сироты возвращает уже не PID умершего родителя.

Задача 8: что может быть в ppid

Процесс Ck выполняет mem->ppid = getppid(); в какой-то момент. Его родитель C(k−1) к этому моменту мог уже завершиться (он сразу после записи вызывает exit(0)). Тогда getppid() вернёт 1 (init) или PID subreaper-а. Значит, среди напечатанных пар могут встретиться пары с ppid = 1.

Итог

  • exec заменяет программу, сохраняя PID и дескрипторы; при успехе не возвращается.
  • При завершении ядро освобождает ресурсы, усыновляет потомков init-ом, оставляет зомби и шлёт родителю SIGCHLD.
  • wait/waitpid забирают статус только своих детей; макросы WIFEXITED, WEXITSTATUS, WIFSIGNALED, WTERMSIG.
  • Зомби — завершился, но не «похоронен»; сирота — родитель умер, родителем стал init.

Практика решено 0 из 6

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Ввод ответа

Что напечатает программа, если execl выполнится успешно?

printf("A\n");
fflush(stdout);
execl("/bin/echo", "echo", "B", (char *)NULL);
printf("C\n");

Введите строки через пробел.

Задание 2 Несколько вариантов

Что сохраняется у процесса после успешного exec?

Задание 3 Написать код

Дождитесь завершения потомка pid через waitpid и, если он завершился нормально (WIFEXITED), напечатайте его код: printf("код %d\n", WEXITSTATUS(status));.

Задание 4 Заполнить таблицу

Зомби или сирота?

ЗомбиСиротаНи то, ни другое
Потомок завершился, родитель жив, но ещё не вызвал wait
Родитель завершился, потомок продолжает работать
Потомок завершился, родитель вызвал wait
Потомок работает, родитель жив
Отметьте ячейку — «+», пустая — «−».
Задание 5 Один вариант ответа

В задаче 8 корневой процесс вызывает wait(NULL) один раз. Завершения какого процесса он ждёт?

Задание 6 Один вариант ответа

Процесс C1 вызвал getppid() уже после того, как его родитель C0 завершился. Что он получит в Linux?

Войдите, чтобы отмечать пройденные уроки.