Жизненные циклы родительского и дочернего процессов обычно не совпадают — один из них живет дольше, чем другой. Из этого вытекает два вопроса.
• Кто становится родителем
Применение операции PR_SET_PDEATHSIG системного вызова prctl() (доступного только в Linux) позволяет сделать так, чтобы в случае потери родителя процесс получал выбранный сигнал.
• Что происходит с потомком, который завершается до того, как его родитель имел возможность выполнить wait()? Дело в том, что, хоть потомок и закончил свою работу, родителю все равно должно быть позволено сделать вызов wait(), чтобы определить причину завершения. Ядро решает эту проблему путем превращения потомка в
Что касается «зомби», системы Unix следуют канонам, принятым в художественных фильмах, — процесс-«зомби» нельзя убить сигналом, даже если это SIGKILL (своеобразная серебряная пуля). Благодаря этому родитель всегда может выполнить вызов wait().
Когда родитель наконец делает вызов wait(), ядро удаляет процесс-«зомби», поскольку информация о нем больше не требуется. С другой стороны, если родитель завершает работу, так и не выполнив вызов wait(), процесс init удочеряет потомка и автоматически делает этот вызов самостоятельно, удаляя таким образом «зомби» из системы.
Если родитель создает потомка, но не успевает вызвать wait(), запись о дочернем процессе-«зомби» все равно продолжит храниться в таблице процессов ядра. Если будет создано слишком большое количество «зомби», они в какой-то момент переполнят эту таблицу, мешая созданию новых процессов. Поскольку «зомби» нельзя убить по сигналу, единственный способ удалить их из системы заключается в принудительном (или штатном) завершении работы их родителя; в этом случае они удочеряются процессом init, который начинает их ожидать, что приводит к их удалению из системы.
Последствия такой семантики имеют большое значение при проектировании долгоживущих родительских процессов, таких как сетевые серверы и командные оболочки, создающих большое количество потомков. Иначе говоря, родительский процесс в таких приложениях должен выполнять вызовы wait(), чтобы гарантировать удаление отработанных потомков из системы и не дать им превратиться в неубиваемых «зомби». Такие вызовы можно делать синхронно или асинхронно в ответ на сигнал SIGCHLD, как описывается в разделе 26.3.1.
В листинге 26.4 показан процесс создания «зомби» и тот факт, что их нельзя принудительно завершить с помощью сигнала SIGKILL. При запуске этой программы мы увидим следующий вывод:
$ ./make_zombie
Parent PID=1013
Child (PID=1014) exiting
1013 pts/4 00:00:00 make_zombie
1014 pts/4 00:00:00 make_zombie
After sending SIGKILL to make_zombie (PID=1014):
1013 pts/4 00:00:00 make_zombie
1014 pts/4 00:00:00 make_zombie
В выводе, представленном выше, видно, что команда ps(1) выводит строку
Программа из листинга 26.4 использует функцию system() для выполнения консольной команды, передаваемой ей в виде символьно-строкового аргумента. Подробно эта функция описывается в разделе 27.6.
Листинг 26.4. Создание дочерних процессов-«зомби»
procexec/make_zombie.c
#include
#include
#include "tlpi_hdr.h"
#define CMD_SIZE 200
int
main(int argc, char *argv[])
{
char cmd[CMD_SIZE];
pid_t childPid;
setbuf(stdout, NULL); /* Отключаем буферизацию стандартного вывода */
printf("Parent PID=%ld\n", (long) getpid());
switch (childPid = fork()) {
case –1:
errExit("fork");
case 0: /* Потомок немедленно завершается, чтобы стать «зомби» */
printf("Child (PID=%ld) exiting\n", (long) getpid());
_exit(EXIT_SUCCESS);
default: /* Родитель */
sleep(3); /* Даем потомку шанс начать выполнение и завершиться */
snprintf(cmd, CMD_SIZE, "ps | grep %s", basename(argv[0]));
system(cmd); /* Видим потомка-«зомби» */
/* Теперь отправляем «зомби» сигнал о безусловном завершении */
if (kill(childPid, SIGKILL) == –1)
errMsg("kill");
sleep(3); /* Даем потомку шанс отреагировать на сигнал */
printf("After sending SIGKILL to zombie (PID=%ld):\n",
(long) childPid);
system(cmd); /* Опять видим потомка-«зомби» */
exit(EXIT_SUCCESS);
}
}
procexec/make_zombie.c