exec, exit и wait: зомби и сироты
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, …) — аварийно.
Что делает ядро при завершении:
- закрывает все файловые дескрипторы, освобождает адресное пространство, снимает отображения;
- всех живых потомков передаёт новому родителю — init (или subreaper);
- переводит процесс в состояние зомби: от него остаётся только
task_structс кодом завершения и статистикой; - посылает родителю сигнал
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
Что напечатает программа, если execl выполнится успешно?
printf("A\n");
fflush(stdout);
execl("/bin/echo", "echo", "B", (char *)NULL);
printf("C\n");
Введите строки через пробел.
Что сохраняется у процесса после успешного exec?
Дождитесь завершения потомка pid через waitpid и, если он завершился нормально (WIFEXITED),
напечатайте его код: printf("код %d\n", WEXITSTATUS(status));.
int status;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) {
printf("код %d\n", WEXITSTATUS(status));
}
Зомби или сирота?
| Зомби | Сирота | Ни то, ни другое | |
|---|---|---|---|
| Потомок завершился, родитель жив, но ещё не вызвал wait | |||
| Родитель завершился, потомок продолжает работать | |||
| Потомок завершился, родитель вызвал wait | |||
| Потомок работает, родитель жив |
В задаче 8 корневой процесс вызывает wait(NULL) один раз. Завершения какого процесса он ждёт?
Процесс C1 вызвал getppid() уже после того, как его родитель C0 завершился. Что он получит в Linux?
Войдите, чтобы отмечать пройденные уроки.