Родительский процесс, получивший ненулевое значение, возвращенное из вызова
fork
, применяет системный вызов
wait
для приостановки своего выполнения до тех пор, пока информация о состоянии дочернего процесса не станет доступной. Это произойдет, когда дочерний процесс вызовет функцию
exit
; мы присвоили ему код завершения 37. Далее родительский процесс продолжается,
, что дочерний процесс завершился нормально, и извлекает код завершения из информации о состоянии процесса.
Процессы-зомби
Применение вызова
fork
для создания процессов может оказаться очень полезным, но вы должны отслеживать дочерние процессы. Когда дочерний процесс завершается, связь его с родителем сохраняется до тех пор, пока родительский процесс в свою очередь не завершится нормально, или не вызовет
wait
. Следовательно, запись о дочернем процессе не исчезает из таблицы процессов немедленно. Становясь неактивным, дочерний процесс все еще остается в системе, поскольку его код завершения должен быть сохранен, на случай если родительский процесс в дальнейшем вызовет
wait
. Он становится умершим или процессом-зомби.
Вы сможете увидеть создание процесса-зомби, если измените количество сообщений в программе из примера с вызовом
fork
. Если дочерний процесс выводит меньше сообщений, чем родительский, он закончится первым и будет существовать как зомби, пока не завершится родительский процесс.
Упражнение 11.5. Зомби
Программа fork2.c такая же, как программа fork1.с, за исключением того, что количества сообщений, выводимых родительским и дочерним процессами, поменяли местами. Далее приведены соответствующие строки кода:
switch (pid) {
case -1:
perror("fork failed");
exit(1);
case 0:
message = "This is the child";
n = 3;
break;
default:
message = "This is the parent";
n = 5;
break;
}
Как это работает
Если вы выполните только что приведенную программу с помощью команды
./fork2 &
и затем вызовите программу
ps
после завершения дочернего процесса, но до окончания родительского, то увидите строку, подобную следующей. (Некоторые системы могут сказать
<zombie>
вместо
<defunct>
.)
$ ps -аl
F S UID PID PPID С PRI NI ADDR SZ WCHAN TTY TIME CMD
004 S 0 1273 1259 0 75 0 - 589 wait4 pts/2 00:00:00 su
родительский процесс завершится необычно, дочерний процесс автоматически получит в качестве родителя процесс с PID, равным 1 (init). Теперь дочерний процесс — зомби, который уже не выполняется, но унаследован процессом
init
из-за необычного окончания родительского процесса. Зомби останется в таблице процессов, пока не пойман процессом
init
. Чем больше таблица, тем медленнее эта процедура. Следует избегать процессов-зомби, поскольку они потребляют ресурсы до тех пор, пока процесс init не вычистит их.
Есть еще один системный вызов, который можно применять для ожидания дочернего процесса. Он называется
waitpid
и применяется для ожидания завершения определенного процесса.
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *stat_loc, int options);
Аргумент
pid
— конкретный дочерний процесс, окончания которого нужно ждать. Если он равен –1,
waitpid
вернет информацию о любом дочернем процессе. Как и вызов
wait
, он записывает информацию о состоянии процесса в место, указанное аргументом
stat_loc
, если последний не равен пустому указателю. Аргумент
options
позволяет изменить поведение
waitpid
. Наиболее полезная опция
WNOHANG
мешает вызову
waitpid
приостанавливать выполнение вызвавшего его процесса. Ее можно применять для выяснения, завершился ли какой-либо из дочерних процессов, и если нет, то продолжать выполнение. Остальные опции такие же, как в вызове
wait
.
Итак, если вы хотите, чтобы родительский процесс периодически проверял, завершился ли конкретный дочерний процесс, можно использовать следующий вызов:
waitpid(child_pid, (int *)0, WNOHANG);
Он вернет ноль, если дочерний процесс не завершился и не остановлен, или
child_pid
, если это произошло. Вызов waitpid вернет -1 в случае ошибки и установит переменную
errno
. Это может произойти, если нет дочерних процессов (
errno
равна
ECHILD
), если вызов прерван сигналом (
EINTR
) или аргумент
options
неверный (
EINVAL
).
Перенаправление ввода и вывода
Вы можете применить ваши знания о процессах для изменения поведения программ, используя тот факт, что открытые файловые дескрипторы сохраняются вызовами
fork
и
exec
. Следующий пример из упражнения 11.6 содержит программу-фильтр, которая читает из стандартного ввода и пишет в свой стандартный вывод, выполняя при этом некоторое полезное преобразование.
Далее приведена программа очень простой фильтрации upper.c, которая читает ввод и преобразует строчные буквы в прописные:
#include <stdio.h>
#include <ctype.h>
#include <stdlib.h>
int main {
int ch;
while ((ch = getchar) != EOF) {
putchar(toupper(ch));
}
exit(0);
}
Когда вы выполните программу, она сделает то, что и ожидалось: