После вызова fork() не всегда можно предугадать, кто первый получит ресурсы центрального процессора (или процессоров) — родитель или потомок. Программы, которые в такой ситуации зависят от порядка выполнения, подвержены ошибке, известной под названием «состояние гонки». Поскольку возникновение таких ошибок зависит от внешних факторов (например, от загрузки системы), иногда их бывает сложно искать и отлаживать.

Дополнительная информация

Подробности реализации вызовов fork(), execve(), wait() и exit() в системах UNIX можно найти в книгах [Bach, 1986] и [Goodheart & Cox, 1994]. В книгах [Bovet & Cesati, 2005] и [Love, 2010] описаны особенности создания и завершения процессов в системе Linux.

24.7. Упражнения

24.1. Сколько новых процессов появится в результате выполнения программой следующей последовательности вызовов fork() (учитывая, что все они выполнятся успешно)?

fork();

fork();

fork();

24.2. Напишите программу, которая наглядно показывает, что после вызова vfork() дочерний процесс может закрыть файловый дескриптор (например, дескриптор под номером 0), без влияния на соответствующий дескриптор родителя.

24.3. Каким образом можно получить дамп памяти процесса в заданном месте программы, позволив исходному процессу продолжать выполнение (предполагается, что исходный код программы можно изменять)?

24.4. Поэкспериментируйте с программой из листинга 24.5 (fork_whos_on_first.c) на других реализациях UNIX, чтобы определить, как в них планируется выполнение родительского и дочернего процессов после вызова fork().

24.5. Представьте, что дочерний процесс в программе из листинга 24.6 тоже должен ждать, пока родитель не завершит какие-то действия. Какие изменения нужно будет внести в программу, чтобы этого добиться?

<p>25. Завершение работы процесса</p>

В этой главе рассматривается процедура завершения процесса. Мы начнем с описания вызовов exit() и _exit(), специально для этого предназначенных. Затем обсудим использование обработчиков выхода для автоматического освобождения ресурсов при вызове exit(). В заключение будут продемонстрированы некоторые способы взаимодействия между буферами stdio (стандартного ввода/вывода) и вызовами fork() и exit().

25.1. Завершение процесса: вызовы _exit() и exit()

Обычно процесс можно завершить двумя способами. Во-первых, это аварийное завершение, вызванное передачей сигнала, чьим действием по умолчанию является остановка работы процесса (со сбросом дампа памяти или без); этот вариант описан в разделе 20.1. Возможно также нормальное завершение с помощью системного вызова _exit().

#include

void _exit(int status);

Аргумент status, передаваемый вызову _exit(), определяет код завершения процесса, который доступен его родителю посредством вызова wait(). Этот аргумент имеет тип int, однако родительскому процессу доступны только последние 8 бит. Код 0 принято считать признаком успешного завершения, а любые другие значения сигнализируют о том, что процесс окончил работу с проблемами. Четких правил по интерпретации ненулевых значений не существует; разные приложения следуют своим собственным соглашениям, которые должны быть описаны в их документации. Стандарт SUSv3 определяет две константы, EXIT_SUCCESS (0) и EXIT_FAILURE (1), которые используются в большинстве примеров из этой книги.

Процесс всегда завершается успешно, если для этого применяется вызов _exit() (то есть _exit() никогда не возвращает значения).

Через аргумент status вызова _exit() родителю можно передать любое значение от 0 до 255, однако номера больше 128 могут вызвать неправильную работу скриптов командной строки. Дело в том, что при завершении программы с помощью сигнала командная оболочка сигнализирует об этом, присваивая переменной $? значение 128 плюс номер самого сигнала; это значение невозможно отличить от аналогичного, переданного в результате вызова _exit().

Программы обычно не вызывают _exit() напрямую, используя вместо этого библиотечную функцию exit(), которая выполняет различные предварительные действия.

#include

void exit(int status);

Функция exit() выполняет следующие приготовления перед вызовом _exit().

• Вызываются обработчики выхода (функции, зарегистрированные с помощью вызовов atexit() и on_exit()); в порядке, обратном их регистрации (см. раздел 25.3).

• Сбрасываются буферы потоков stdio.

• Выполняется системный вызов _exit() со значением, переданным в аргументе status.

В отличие от вызова _exit(), характерного для UNIX-систем, функция exit() является частью стандартной библиотеки С; это означает, что она доступна в любой реализации данного языка.

Другим способом завершения работы процесса является возвращение из функции main(), явное или неявное (когда достигается конец функции). Явный возврат значения (return n) обычно является эквивалентом вызова exit(n), поскольку среда выполнения, которая вызывает функцию main(), использует возвращаемое из нее значение для вызова exit().

Перейти на страницу:

Похожие книги