Программирование для Linux. Профессиональный подход.
Глава 3. Процессы.
Выполняющийся экземпляр программы называется процессом. Если на экране отображаются два терминальных окна, то, скорее всего, одна и та же терминальная программа запущена дважды — ей просто соответствуют два процесса. В каждом окне, очевидно, работает интерпретатор команд — это еще один процесс. Когда пользователь вводит команду в интерпретаторе, соответствующая ей программа запускается в виде процесса. По завершении работы программы управление вновь передается процессу интерпретатора.
Опытные программисты часто создают несколько взаимодействующих процессов в рамках одного приложения, чтобы оно могло выполнять группу действий одновременно. Это повышает надежность приложения и позволяет ему использовать уже написанные программы.
Большинство описанных в данной главе функций управления процессами доступно и в других UNIX-системах. В основном они объявлены в файле <unistd.h>, но не помешает проверить это в документации.
3.1. Знакомство с процессами.
Пользователю достаточно войти в систему, чтобы в ней начали выполняться процессы. Даже если пользователь ничего не запускает, а просто сидит перед экраном и пьет кофе. в системе все равно "теплится жизнь". Любой выполняющейся программе соответствует один или несколько процессов. Давайте для начала познакомимся с теми из них, которые присутствуют по умолчанию.
3.1.1. Идентификаторы процессов.
Каждый процесс в Linux помечается уникальным идентификатором (PID, process identifier). Идентификаторы — это 16-разрядные числа, назначаемые последовательно по мере создания процессов.
У всякого процесса имеется также родительский процесс (за исключением специального демона init, о котором рассказывается в разделе 3.4.3, "Процессы-зомби"). Таким образом, все процессы Linux организованы в виде древовидной иерархии, на вершине которой находится процесс init. К атрибутам процесса относится идентификатор его предка (PPID, parent process identifier).
Работая с идентификаторами процессов в программах, написанных на языках С и C++, следует объявлять соответствующие переменные как имеющие тип pid_t (определен в файле <sys/types.h>). Программа может узнать идентификатор своего собственного процесса с помощью системного вызова getpid(), а идентификатор своего родительского процесса — с помощью вызова getppid(). В листинге 3.1 показано, как это сделать.
Листинг 3.1. (print-pid.c) Вывод идентификатора процесса.
#include <stdio.h>
#include <unistd.h>
Int main() {
printf("The process ID is %d\n", (int)getpid());
printf("The parent process ID is %d\n", (int)getppid());
return 0;
}
Обратите внимание на важную особенность: при каждом вызове программа сообщает о разных идентификаторах, поскольку всякий раз запускается новый процесс. Тем не менее, если программа вызывается из одного и того же интерпретатора команд, то родительский идентификатор оказывается одинаковым.
3.1.2. Получение списка активных процессов.
Команда ps отображает список процессов, работающих в данный момент в системе. Версия этой команды в GNU/Linux имеет множество опций, так как пытается быть совместимой со своими "родственниками" в других UNIX-системах. С помощью опций можно указывать, о каких процессах и какую именно требуется получить информацию.
Будучи вызванной без аргументов, команда ps выводит список тех процессов, управляющим терминалом которых является ее собственный терминал:
% ps.
PID TTY TIME CMD.
21693 pts/8 00:00:00 bash.
21694 pts/8 00:00:00 ps.
В данном случае мы видим два процесса. Первый из них. bash, — это интерпретатор команд, запущенный на данном терминале. Второй — выполняющийся экземпляр самой команды ps. В самом левом столбце, PID, отображаются идентификаторы процессов.
Более полный список можно получить с помощью следующей команды:
% ps -е -о pid,ppid,command.
Опция -е заставляет команду ps отображать все процессы, выполняющиеся в системе. Опция -о pid,ppid,command сообщает о том, какая информация о процессе нас интересует. В данном случае это идентификатор самого процесса, идентификатор его родительского процесса и команда, посредством которой был запущен процесс.
Форматы вывода команды ps. В опции -o через запятую указываются столбцы которые должны быть включены в вывод команды ps. Например, команда ps -о pid,user,start_time,command отображает идентификатор процесса, имя его владельца, время запуска а также команду, соответствующую процессу. Полный список опций и столбцов можно узнать на man-странице команды ps. Имеются три предопределенных формата вывода: -f (полный листинг), -l (длинный листинг) и -j (вывод заданий).Ниже приведено несколько первых и последних строк, выдаваемых этой командой в нашей системе:
% ps -e -о pid,ppid,command.
PID PPID COMMAND.
1 0 init [5].
2 1 [kflushd].
3 1 [kupdate].
...
21725 21693 xterm.
21727 21725 bash.
21728 21727 ps -e -o pid,ppid,command.
Заметьте: родительский идентификатор команды ps, 21727, соответствует интерпретатору bash, из которого была вызвана команда. В свою очередь, родительский идентификатор интерпретатора, 21725, принадлежит программе xterm — эмулятору терминала, в котором выполняется интерпретатор.
3.1.3. Уничтожение процесса.
Для уничтожения процесса предназначена команда kill. Ей достаточно указать идентификатор требуемого процесса.
Команда kill посылает процессу сигнал SIGTERM, являющийся запросом на завершение.[10] По умолчанию, если в программе отсутствует обработчик данного сигнала, процесс просто завершает свою работу. О сигналах речь пойдет в разделе 3.3, "Сигналы".
3.2. Создание процессов.
Существуют два способа создания процессов. Первый из них относительно прост, но применяется редко, поскольку неэффективен и связан со значительным риском для безопасности системы. Второй способ сложнее, но избавлен от недостатков первого.
3.2.1. Функция system().
Функция system() определена в стандартной библиотеке языка С и позволяет вызывать из программы системную команду, как если бы она была набрана в командной строке. По сути, эта функция запускает стандартный интерпретатор Bourne shell ( /bin/sh) и передает ему команду на выполнение. Например, программа, представленная в листинге 3.2, вызывает команду ls -l /, отображающую содержимое корневого каталога.
Листинг 3.2. (system.c) Использование функции system().
#include <stdlib.h>
Int main() {
int return_value;
return_value = system("ls -l /");
return return_value;
}
Функция system() возвращает код завершения указанной команды. Если интерпретатор не может быть запущен, возвращается значение 127, а в случае возникновения других ошибок — -1.
Поскольку функция system() запускает интерпретатор команд, она подвержена всем тем ограничениям безопасности, что и системный интерпретатор. Рассчитывать на наличие какой-то конкретной версии Bourne shell не приходится. В большинстве UNIX-систем программа /bin/sh представляет собой символическую ссылку на другой интерпретатор. В Linux — это bash (Bourne-Again SHell), причем в разных дистрибутивах присутствуют разные его версии. Вызов из функции system() программы с привилегиями пользователя root также может иметь неодинаковые последствия в разных системах. Таким образом, лучше создавать процессы с помощью функций fork() и exec().
3.2.2. Функции fork() и exec().
В DOS и Windows API имеется семейство функций spawn(). Они принимают в качестве аргумента имя программы, создают новый экземпляр ее процесса и запускают его. В Linux нет функции, которая делала бы все это за один заход. Вместо этого имеется функция fork(), создающая дочерний процесс, который является точной копией родительского процесса, и семейство функций exec(), заставляющих требуемый процесс перестать быть экземпляром одной программы и превратиться в экземпляр другой программы. Чтобы создать новый процесс, нужно сначала с помощью функции fork() создать копню текущего процесса, а затем с помощью функции exec() преобразовать одну из копий в экземпляр запускаемой программы.
Вызов функции fork().
Вызывая функцию fork(), программа создает свой дубликат, называемый дочерним процессом. Родительский процесс продолжает выполнять программу с той точки, где была вызвана функция fork(). То же самое делает и дочерний процесс.
Как же различить между собой оба процесса? Во-первых, дочерний процесс — это новый, только что появившийся в системе процесс, поэтому его идентификатор отличается от идентификатора родительского процесса. Таким образом, программа может вызвать функцию getpid() и узнать, где именно она находится. Но сама функция fork() реализует другой способ: она возвращает разные значения в родительском и дочернем процессах. Родительский процесс получает идентификатор своего потомка, а дочернему процессу возвращается 0. В системе нет процессов с нулевым идентификатором, так что программа легко разбирается в ситуации.
В листинге 3.3 приведен пример ветвления программы с помощью функции fork(). Учтите, что первая часть инструкции if выполняется только в родительском процессе, тогда как ветвь else — только в дочернем.
Листинг 3.3. (fork.c) Ветвление программы с помощью функции fork().
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
Int main() {
pid_t child_pid;
printf("The main program process ID is %d\n",
(int)getpid());
child_pid = fork();
if (child_pid != 0) {
printf("This is the parent process, with ID %d\n",
(int)getpid());
printf("The child's process ID is %d\n", (int)child_pid);
} else.
printf("This is the child process, with ID %d\n",
(int)getpid());
return 0;
}
Семейство функций exec().
Функции семейства exec() заменяют программу, выполняющуюся в текущем процессе, другой программой. Когда программа вызывает функцию exec(), ее выполнение немедленно прекращается и начинает работу новая программа.
Функции, входящие в семейство exec(), немного отличаются друг от друга по своим возможностям и способу вызова.
■ Функции, в названии которых присутствует суффикс 'p' ( execvp() и execlp()), принимают в качестве аргумента имя программы и ищут эту программу в каталогах, определяемых переменном среды PATH. Всем остальным функциям нужно передавать полное путевое имя программы.
■ Функции, в названии которых присутствует суффикс 'v' ( execv(), execvp() и execve()), принимают список аргументов программы в виде массива строковых указателей, оканчивающегося NULL-указателем. Функции с суффиксом 'l' ( execl(), execlp() и execle()) принимают список аргументов переменного размера.
■ Функции, в названии которых присутствует суффикс 'e' ( execve() и execle()), в качестве дополнительного аргумента принимают массив переменных среды. Этот массив содержит строковые указатели и оканчивается пустым указателем. Каждая строка должна иметь вид "ПЕРЕМЕННАЯ=значение".
Поскольку функция exec() заменяет одну программу другой, она никогда не возвращает значение — только если вызов программы оказался невозможен в случае ошибки.
Список аргументов, передаваемых программе, аналогичен аргументам командной строки, указываемым при запуске программы в интерактивном режиме. Их тоже можно получить с помощью параметров argc и argv функции main(). Не забывайте, когда программу запускает интерпретатор команд, первый элемент массива argv будет содержать имя программы, а далее будут находиться переданные программе аргументы. Аналогичным образом следует поступить, формируя список аргументов для функции exec().
Совместное использование функций fork() и exec().
Стандартная методика запуска одной программы из другой такова: сначала с помощью функции fork() создается дочерний процесс, затем в нем вызывается функция exec(). Это позволяет главной программе продолжать выполнение в родительском процессе.
Программа, показанная в листинге 3.4, отображает содержимое корневого каталога с помощью команды ls, как и программа в листинге 3.2. Но на этот раз команда ls вызывается не из интерпретатора, а напрямую; ей передаются аргументы -l и /.
Листинг 3.4. (fork-exec.с) Совместное использование функций fork() и exec().
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
/* Запуск дочернего процесса в виде новой программы. Параметр.
PROGRAM — это имя вызываемой программы; ее поиск будет.
осуществляться в каталогах, определяемых переменной среды PATH.
Параметр ARG_LIST -- это список строковых аргументов,
передаваемых программе (должен оканчиваться указателем NULL).
Функция возвращает идентификатор порожденного процесса. */
Int spawn(char* program, char** arg_list) {
pid_t child_pid;
/* Создание копии текущего процесса. */
child_pid = fork();
if (child_pid != 0).
/* Это родительский процесс. */
return child_pid;
else {
/* Выполнение указанной программы. */
execvp(program, arg_list);
/* Функция execvp() возвращает значение только в случае.
ошибки. */
fprintf(stderr, "an error occurred in execvp\n");
abort();
}
}
Int main() {
/* Список аргументов, передаваемых команде ls. */
char* arg_list[] = {
"ls", /* argv[0] -- имя программы. */
"-l",
NULL /* Список аргументов должен оканчиваться указателем.
NULL. */
};
/* Порождаем дочерний процесс, который выполняет команду ls.
Игнорируем возвращаемый идентификатор дочернего процесса. */
spawn("ls", arg_list);
printf("done with main program\n");
return 0;
}
3.2.3. Планирование процессов.
Операционная система Linux планирует работу родительских и дочерних процессов независимо друг от друга. Нет гарантии, что один процесс будет запущен раньше другого. и неизвестно, как долго один процесс будет выполняться, прежде чем Linux прервет его работу и передаст управление другому процессу. В частности, к моменту завершения родительского процесса может оказаться, что команда ls еще не выполнена, выполнена частично или уже закончила свою работу.[11] Linux лишь гарантирует, что любой процесс когда-нибудь получит свой "кусочек пирога": ни один процесс не окажется полностью лишенным доступа к процессору.
Можно сообщить системе о том, что процесс не очень важен и должен выполняться с пониженным приоритетом. Это делается путем повышения фактора уступчивости процесса. По умолчанию у каждого процесса нулевой фактор уступчивости. Повышение этого значения свидетельствует о снижении приоритета процесса, и наоборот: процессы с низким (т.е. отрицательным) фактором уступчивости получают больше времени на выполнение.
Для запуска программы с ненулевым фактором уступчивости необходимо воспользоваться командой nice -n. Рассмотрим следующий пример:
% nice -n 10 sort input.txt > output.txt.
Здесь активизируется длительная операция сортировки, которая, благодаря пониженному приоритету, не приведет к сильному снижению производительности системы. Изменить фактор уступчивости выполняющегося процесса позволяет команда renice.
Если требуется менять фактор уступчивости программным путем, воспользуйтесь функцией nice(). Ее аргумент — это величина приращения, добавляемая к фактору уступчивости вызывающего процесса. В результате приоритет процесса снижается.
Только программа с привилегиями пользователя root может запускать процессы с отрицательным фактором уступчивости или понижать это значение у выполняющегося процесса. Это означает, что вызывать команды nice и renice с отрицательными аргументами можно, лишь войдя в систему как пользователь root, и только процесс, выполняемый от имени суперпользователя, может передавать функции nice() отрицательное значение. Таким образом, обычные пользователи не могут помешать работать процессам других пользователей и монополизировать системные ресурсы.
3.3. Сигналы.
Сигналы — это механизм связи между процессами в Linux. Данная тема очень обширна, поэтому здесь мы рассмотрим лишь наиболее важные сигналы и методики управления процессами.
Сигнал представляет собой специальное сообщение, посылаемое процессу. Сигналы являются асинхронными: когда процесс принимает сигнал, он немедленно обрабатывает его, прерывая выполнение текущей функции и даже текущей строки программы. Есть несколько десятков различных сигналов, каждый из которых имеет свое функциональное назначение. Все они распознаются по номерам, но в программах для ссылки на сигналы пользуются символическими константами. В Linux эти константы определены в файле /usr/include/bits/signum.h (его не нужно включать в программы, для этого есть файл <signal.h>).
В ответ на полученный сигнал процесс выполняет ряд действий в зависимости от типа сигнала. У каждого сигнала есть стандартный обработчик, определяющий, что произойдет с процессом, если он попытается проигнорировать сигнал. Для большинства сигналов можно также задавать явную функцию обработки. В этом случае при поступлении сигнала выполнение программы приостанавливается, выполняется обработчик, а потом программа возобновляет свою работу.
Операционная система Linux посылает процессам сигналы в случае возникновения определенных ситуаций. Например, сигналы SIGBUS (ошибка на шине), SIGSEGV (нарушение сегментации) и SIGFPE (ошибка операции с плавающей запятой) могут быть посланы процессу, пытающемуся выполнить неправильную операцию. По умолчанию эти сигналы приводят к завершению процесса и созданию дампа оперативной памяти.
Процесс может сам послать сигнал другому процессу. Чаще всего возникает необходимость завершить требуемый процесс с помощью сигнала SIGTERM или SIGKILL.[12] С помощью сигналов можно также передавать команды выполняющимся программам. Для этого существуют "пользовательские" сигналы SIGUSR1 и SIGUSR2. Иногда в аналогичных целях применяется сигнал SIGHUP, с помощью которого можно заставить программу повторно прочитать свои файлы конфигурации.
Функция sigaction() определяет правила обработки указанного сигнала. Первый ее аргумент — это номер сигнала. Следующие два аргумента представляют собой указатели на структуру sigaction; первый из них регистрирует новый обработчик сигнала, а второй содержит описание предыдущего обработчика. Наиболее важным полем структуры sigaction является sa_handler. Оно может содержать одно из трех значений:
■ SIG_DFL — выбор стандартного обработчика сигнала;
■ SIG_IGN — игнорирование сигнала,
■ указатель на функцию обработки сигнала; эта функция должна принимать один параметр (номер сигнала) и возвращать значение типа void.
Поскольку сигнал может прийти в любой момент, он способен застать программу "врасплох" за выполнением критической операции, не подразумевающей прерывание. Такой операцией, к примеру, является обработка предыдущего сигнала. Отсюда правило: следует избегать операций ввода-вывода и вызовов большинства библиотечных и системных функций в обработчиках сигналов.
Обработчик должен выполнять минимум действий в ответ на получение сигнала и как можно быстрее возвращать управление в программу (или просто завершать ее работу). В большинстве случаев обработчик просто фиксирует факт поступления сигнала, а основная программа периодически проверяет, был ли сигнал, и реагирует должным образом.
Тем не менее возможность прерывания обработчика никогда нельзя исключать. Это очень сложная ситуация для диагностирования и отладки (и наглядный пример состояния гонки, о котором пойдет речь в разделе 4.4. "Синхронизация потоков и критические секции"). Необходимо внимательно следить за тем, что именно делается в обработчике.
Даже присвоение значения глобальной переменной несет потенциальную опасность, так как данная операция может занять два или три такта процессора, а за это время успеет прийти следующий сигнал, вследствие чего переменная окажется поврежденной. Если обработчик использует какую-то переменную в качестве флага поступления сигнала, она должна иметь специальный тип sig_atomic_t. Linux гарантирует, что операция присваивания значения такой переменной займет ровно один такт и не будет прервана. На самом деле тип sig_atomic_t в Linux эквивалентен типу int; более того, операции присваивания целочисленных переменных (32- и 16-разрядных) и указателей всегда атомарны. Использовать тип sig_atomic_t необходимо для того, чтобы программу можно было перенести в любую стандартную UNIX-систему.
В листинге 3.5 представлен шаблон программы, в которой функция-обработчик подсчитывает, сколько раз программа получает сигнал SIGUSR1.
Листинг 3.5. (sigusr1.c) Корректное применение обработчика сигнала.
#include <signal.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
Sig_atomic_t sigusr1_count = 0;
Void handler(int signal_number) {
++sigusr1_count;
}
Int main() {
struct sigaction sa;
memset(&sa, 0, sizeof(sa));
sa.sa_handler = &handler;
sigaction(SIGUSR1, &sa, NULL);
/* далее идет основной текст. */
/* ... */
printf("SIGUSR1 was raised %d times\n", sigusr1_count);
return 0;
}
3.4. Завершение процесса.
Обычно процесс завершается одним из двух способов: либо выполняющаяся программа вызывает функцию exit(), либо функция main() заканчивается. У каждого процесса есть код завершения — число, возвращаемое родительскому процессу. Этот код передается в качестве аргумента функции exit() или возвращается функцией main().
Возможно также аварийное завершение процесса, в ответ на получение сигнала. Таковыми могут быть, например, упоминавшиеся выше сигналы SIGBUS, SIGSEGV и SIGFPE. Есть сигналы, явно запрашивающие прекращение работы процесса. В частности, сигнал SIGINT посылается, когда пользователь нажимает <Ctrl+C>. Сигнал SIGTERM посылается процессу командной kill по умолчанию. Если программа вызывает функцию abort(), она посылает сама себе сигнал SIGABRT. Самый "могучий" из всех сигналов — SIGKILL: он приводит к безусловному уничтожению процесса и не может быть ни блокирован, ни обработан.
Любой сигнал можно послать с помощью команды kill, указав дополнительный флаг. Например, чтобы уничтожить процесс, послав ему сигнал SIGKILL, воспользуйтесь следующей командой:
% kill -KILL идентификатор_процесса.
Для отправки сигнала из программы предназначена функция kill(). Ее первым аргументом является идентификатор процесса. Второй аргумент — номер сигнала (стандартному поведению команды kill соответствует сигнал SIGTERM). Например, если переменная child_pid содержит идентификатор дочернего процесса, то следующая функция, вызываемая из родительского процесса, вызывает завершение работы потомка:
Kill(child_pid, SIGTERM);
Для использования функции kill() необходимо включить в программу файлы <sys/types.h> и <signal.h>.
По существующему соглашению код завершения указывает на то, успешно ли выполнилась программа. Нулевой код говорит о том, что все в порядке, ненулевой код свидетельствует об ошибке. В последнем случае конкретное значение кода может подсказать природу ошибки. Подобным образом функционируют все компоненты GNU/Linux. Например, на это рассчитывает интерпретатор команд, когда в командных сценариях вызовы программ объединяются с помощью операторов && (логическое умножение) и || (логическое сложение) Таким образом, функция main() должна явно возвращать 0 при отсутствии ошибок.
Помните о следующем ограничении: несмотря на то что тип параметра функции exit(), как и тип возвращаемого значения функции main(), равен int, операционная система Linux записывает код завершения лишь в младший из четырех байтов. Это означает, что значение кода должно находиться в диапазоне от 0 до 127. Коды, значение которых больше 128, интерпретируются особым образом: когда процесс уничтожается вследствие получения сигнала, его код завершения равен 128 плюс номер сигнала.
3.4.1. Ожидание завершения процесса.
Читатели, запускавшие программу fork-exec (см. листинг 3.4), должно быть, обратили внимание на то, что вывод команды ls часто появляется после того, как основная программа уже завершила свою работу. Это связано с тем, что дочерний процесс, в котором выполняется команда ls, планируется независимо от родительского процесса. Linux — многозадачная операционная система, процессы в ней выполняются одновременно, поэтому нельзя заранее предсказать, кто — предок или потомок — завершится раньше.
Но бывают ситуации, когда родительский процесс должен дождаться завершения одного или нескольких своих потомков. Это можно сделать с помощью функций семейства wait(). Они позволяют родительскому процессу получать информацию о завершении дочернего процесса. В семейство входят четыре функции, различающиеся объемом возвращаемой информации, а также способом задания дочернего процесса.
3.4.2. Системные вызовы wait().
Самая простая функция в семействе называется wait(). Она блокирует вызывающий процесс до тех пор, пока один из его дочерних процессов не завершится (или не произойдет ошибка). Код состояния потомка возвращается через аргумент, являющийся указателем на целое число. В этом коде зашифрована различная информация о потомке. Например, макрос WEXITSTATUS() возвращает код завершения дочернего процесса. Макрос WIFEXITED() позволяет узнать, как именно завершился процесс: обычным образом (с помощью функции exit() или оператора return функции main()) либо аварийно вследствие получения сигнала. В последнем случае макрос WTERMSIG() извлекает из кода завершения номер сигнала.
Ниже приведена доработанная версия функции main() из файла fork-exec.c. На этот раз программа вызывает функцию wait(), чтобы дождаться завершения дочернего процесса, в котором выполняется команда ls.
Int main() {
int child_status;
/* Список аргументов, передаваемых команде ls. */
char* arg_list[] = {
"ls", /* argv[0] — имя программы. */
"-l",
"/",
NULL /* Список аргументов должен оканчиваться указателем.
NULL. */
};
/* Порождаем дочерний процесс, который выполняет команду ls.
Игнорируем возвращаемый идентификатор дочернего процесса. */
spawn("ls*, arg_list);
/* Дожидаемся завершения дочернего процесса. */
wait(&child_status);
if (WTFEXITED(child_status));
printf("the child process exited normally, with exit code %d\n",
WEXITSTATUS(child_status));
else.
printf("the child process exited abnormally\n");
return 0;
}
Расскажем о других функциях семейства. Функция waitpid() позволяет дождаться завершения конкретного дочернего процесса, а не просто любого. Функция wait3() возвращает информацию о статистике использования центрального процессора завершившимся дочерним процессом. Функция wait4() позволяет задать дополнительную информацию о том, завершения каких процессов следует дождаться.
3.4.3. Процессы-зомби.
Если дочерний процесс завершается в то время, когда родительский процесс заблокирован функцией wait(), он успешно удаляется и его код завершения передается предку через функцию wait(). Но что произойдет, если потомок завершился, а родительский процесс так и не вызвал функцию wait()? Дочерний процесс просто исчезнет? Нет, ведь в этом случае информация о его завершении (было ли оно аварийным или нет и каков код завершения) пропадет. Вместо этого дочерний процесс становится процессом-зомби.
Зомби — это процесс, который завершился, но не был удален. Удаление зомби возлагается на родительский процесс. Функция wait() тоже это делает, поэтому перед ее вызовом не нужно проверять, продолжает ли выполняться требуемый дочерний процесс. Предположим, к примеру, что программа создает дочерний процесс, выполняет нужные вычисления и затем вызывает функцию wait(). Если к тому времени дочерний процесс еще не завершился, функция wait() заблокирует программу. В противном случае процесс на некоторое время превратится в зомби. Тогда функция wait() извлечет код его завершения, система удалит процесс и функция немедленно завершится.
Что же всё-таки случится, если родительский процесс не удалит своих потомков? Они останутся в системе в виде зомби. Программа, показанная в листинге 3.6, порождает дочерний процесс, который немедленно завершается, тогда как родительский процесс берет минутную паузу, после чего тоже заканчивает работу, так и не позаботившись об удалении потомка.
Листинг 3.6. (zombie.c) Создание процесса-зомби.
#include «stdlib.h>
#include <sys/types.h>
#include <unistd.h>
Int main() {
pid_t child_pid;
/* Создание дочернего процесса. */
child_pid = fork();
if (child_pid > 0) {
/* Это родительский процесс — делаем минутную паузу. */
sleep(60);
} else {
/* Это дочерний процесс — немедленно завершаем работу. */
exit(0);
}
return 0;
}
Скомпилируйте этот файл и запустите программу. Пока программа работает, перейдите в другое окно и просмотрите список процессов с помощью следующей команды:
% ps -е -o pid,ppid,stat,cmd.
Эта команда отображает идентификатор самого процесса и его предка, а также статус процесса и его командную строку. Обратите внимание на присутствие двух процессов с именем zombie. Один из них — предок, другой — потомок. У последнего идентификатор родительского процесса равен идентификатору основного процесса zombie, при этом потомок обозначен как <defunct> (несуществующий), а его код состояния равен Z (т.е. zombie — зомби).
Итак, мы хотим узнать, что будет, когда программа zombie завершится, не вызвав функцию wait(). Останется ли процесс-зомби? Нет — выполните команду ps и убедитесь в этом: оба процесса zombie исчезли. Дело в том, что после завершения программы управление ее дочерними процессами принимает на себя специальный процесс — демон init, который всегда работает, имея идентификатор 1 (это первый процесс, запускаемый при загрузке Linux). Демон init автоматически удаляет все унаследованные им дочерние процессы-зомби.
3.4.4. Асинхронное удаление дочерних процессов.
Если дочерний процесс просто вызывает другую программу с помощью функции exec(), то в родительском процессе можно сразу же вызвать функцию wait() и пассивно дожидаться завершения потомка. Но очень часто нужно, чтобы родительский процесс продолжал выполняться одновременно с одним или несколькими своими потомками. Как в этом случае получать сигналы об их завершении?
Один подход заключается в периодическом вызове функции wait3() или wait4(). Функция wait() в данной ситуации не подходит, так как в случае отсутствия завершившегося дочернего процесса она заблокирует основную программу. А вот упомянутые две функции принимают дополнительный флаг WNOHANG, переводящий их в неблокируемый режим, в котором функция либо удаляет дочерний процесс, если он есть, либо просто завершается. В первом случае возвращается идентификатор процесса, во втором — 0.
Более элегантный подход состоит в асинхронном уведомлении родительского процесса о завершении потомка. Существуют разные способы сделать это, но проще всего воспользоваться сигналом SIGCHLD, посылаемым как раз тогда, когда завершается дочерний процесс. По умолчанию программа никак не реагирует на этот сигнал, поэтому раньше вы могли и не знать о его существовании.
Таким образом, нужно организовать удаление дочерних процессов в обработчике сигнала SIGCHLD. Естественно, код состояния удаляемого процесса следует сохранять в глобальной переменной, если эта информация необходима основной программе. В листинге 3.7 показана программа, в которой реализована данная методика.
Листинг 3.7. (sigchld.c) Удаление дочерних процессов в обработчике сигнала SIGCHLD.
#include <signal.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
Sig_atomic_t child_exit_status;
Void clean_up_child_process(int signal_number) {
/* Удаление дочернего процесса. */
int status;
wait(&status);
/* Сохраняем статус потомка в глобальной переменной. */
child_exit_status = status;
}
Int main() {
/* Обрабатываем сигнал SIGCHLD, вызывая функцию.
clean_up_child_process(). */
struct sigaction sigchld_action;
memset(&sigchld_action, 0, sizeof(sigchld_action));
sigchld_action.sa_handler = &clean_up_child_process;
sigaction(SIGCHLD, &sigchld_action, NULL);
/* Далее выполняются основные действия, включая порождение.
дочернего процесса. */
/* ... */
return 0;
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
- 28.
- 29.
- 30.
- 31.
- 32.
- 33.
- 34.
- 35.
- 36.
- 37.
- 38.
- 39.
- 40.
- 41.
- 42.
- 43.
- 44.
- 45.
- 46.
- 47.
- 48.
- 49.
- 50.
- 51.
- 52.
- 53.
- 54.
- 55.
- 56.
- 57.
- 58.
- 59.
- 60.
- 61.
- 62.
- 63.
- 64.
- 65.
- 66.
- 67.
- 68.
- 69.
- 70.
- 71.
- 72.
- 73.
- 74.
- 75.
- 76.
- 77.
- 78.
- 79.
- 80.
- 81.
- 82.
- 83.
- 84.
- 85.
- 86.
- 87.
- 88.
- 89.
- 90.
- 91.
- 92.
- 93.
- 94.
- 95.
- 96.
- 97.
- 98.
- 99.
- 100.
- 101.
- 102.
- 103.
- 104.
- 105.
- 106.
- 107.
- 108.
- 109.
- 110.
- 111.
- 112.
- 113.
- 114.
- 115.
- 116.
- 117.
- 118.
- 119.
- 120.
- 121.
- 122.
- 123.
- 124.
- 125.
- 126.
- 127.
- 128.
- 129.
- 130.
- 131.
- 132.
- 133.
- 134.
- 135.
- 136.
- 137.
- 138.
- 139.
- 140.
- 141.
- 142.
- 143.
- 144.
- 145.
- 146.
- 147.
- 148.
- 149.
- 150.
- 151.
- 152.
- 153.
- 154.
- 155.
- 156.
- 157.
- 158.
- 159.
- 160.
- 161.
- 162.
- 163.
- 164.
- 165.
- 166.
- 167.
- 168.
- 169.
- 170.
- 171.
- 172.
- 173.
- 174.
- 175.
- 176.
- 177.
- 178.
- 179.
- 180.
- 181.
- 182.
- 183.
- 184.
- 185.
- 186.
- 187.
- 188.
- 189.
- 190.
- 191.
- 192.
- 193.
- 194.
- 195.
- 196.
- 197.
- 198.
- 199.
- 200.
- 201.
- 202.
- 203.
- 204.
- 205.
- 206.
- 207.
- 208.
- 209.
- 210.
- 211.
- 212.
- 213.
- 214.
- 215.
- 216.
- 217.
- 218.
- 219.
- 220.
- 221.
- 222.
- 223.
- 224.
- 225.
- 226.
- 227.
- 228.
- 229.
- 230.
- 231.
- 232.
- 233.
- 234.
- 235.
- 236.
- 237.
- 238.
- 239.
- 240.
- 241.
- 242.
- 243.
- 244.
- 245.
- 246.
- 247.
- 248.
- 249.
- 250.
- 251.
- 252.
- 253.
- 254.
- 255.
- 256.
- 257.
- 258.
- 259.
- 260.
- 261.
- 262.
- 263.
- 264.
- 265.
- 266.
- 267.