registering callback1 /* Запуск главной программы */
registering callback2
registering callback3
exiting now
callback3 called /* Функции обратного вызова запускаются в обратном
порядке */
callback2 called
callback1 called
Как показывает пример, функции, зарегистрированные с помощью
atexit
, запускаются в порядке, обратном порядку их регистрации: последние первыми. (Это обозначается также LIFO — last-in-first-out — вошедший последним выходит первым).
POSIX определяет функцию
_exit
. В отличие от
exit
, которая вызывает функции обратного вызова и выполняет
<stdio.h>
– очистку,
_exit
является «сразу заканчивающейся» функцией:
#include <unistd.h> /* POSIX */
void _exit(int status);
Системе передается
status
, как и для
exit
, но процесс завершается немедленно. Ядро все еще делает обычную очистку: все открытые файлы закрываются, использованная адресным пространством память освобождается, любые другие ресурсы, использованные процессом, также освобождаются.
На практике функция
_Exit
ISO С идентична
_exit
. Стандарт С говорит, что от реализации функции зависит, вызывает ли
_Exit
зарегистрированные
atexit
функции и закрывает ли открытые файлы. Для систем GLIBC это не так, и функция ведет себя подобно
_exit
.
Время использовать
_exit
наступает, когда
exec
в порожденном процессе завершается неудачей. В этом случае вам не нужно использовать обычный
exit
, поскольку это сбрасывает на диск данные буферов, хранящиеся в потоках
FILE*
. Когда позже родительский процесс сбрасывает на диск свои копии буферов, данные буфера оказываются записанными дважды; это очевидно нехорошо.
Например, предположим, что вы хотите запустить команду оболочки и хотите сами выполнить
fork
и
exec
. Такой код выглядел бы следующим образом:
char *shellcommand = "...";
pid_t child;
if ((child = fork) == 0) { /* порожденный процесс */
execl("/bin/sh", "sh", "-c", shellcommand, NULL);
_exit(errno == ENOENT ? 127 : 126);
}
/* родитель продолжает */
Проверка значения
errno
и завершающего значения следуют соглашениям, используемым оболочкой POSIX.
Если запрошенная программа не существует (
ENOENT
— нет для неё элемента в каталоге), завершающее значение равно 127. В противном случае, файл существует, но
exec
не могла быть выполнена по какой-то другой причине, поэтому статус завершения равен 126. Хорошая мысль следовать этим соглашениям также и в ваших программах. Вкратце, чтобы хорошо использовать
exit
и
atexit
, следует делать следующее:
• Определить небольшой набор значений статуса завершения, которые ваша программа будет использовать для сообщения этой информации вызывающему. Используйте для них в своем коде константы
#define
или
enum
.
• Решить, имеет ли смысл наличие функций обратного вызова для использования с
atexit
. Если имеет, зарегистрировать их в
main
в соответствующий момент; например, после анализа опций и инициализации всех структур данных, которые функция обратного вызова должна очищать. Помните, что функции должны вызываться в порядке LIFO (последняя вызывается первой).
• Использовать
exit
для выхода из программы во всех местах, когда что-то идет не так и когда выход является правильным действием. Используйте коды ошибок, которые определили.
• Исключением является
main
, для которой можно использовать при желании
return
. Наш собственный стиль заключается обычно в использовании
exit
при наличии проблем и '
return 0
' в конце
main
, если все прошло хорошо.
• Использовать
_exit
или
_Exit
в порожденном процессе, если exec завершается неудачей.
9.1.6. Использование статуса завершения порожденного процесса
Когда процесс заканчивается, нормальным ходом событий для ядра является освобождение всех его ресурсов. Ядро сохраняет статус завершения законченного процесса, также, как сведения о ресурсах, которые он использовал в своей работе, a PID продолжает считаться используемым. Такой завершившийся процесс называется зомби.
Родительский процесс, будь то первоначальный родитель или
init
, может получить статус завершения порожденного процесса. Или, посредством использования функций BDS, которые не стандартизованы POSIX, можно получить статус завершения вместе со сведениями об использовании ресурсов. Использование статуса осуществляется ожиданием окончания процесса: это известно также как пожинание (reaping) процесса [91] .
91
Мы это не придумываем. Терминология, конечно, не совсем правильна, но таким было чувство юмора разработчиков оригинальной Unix — Примеч. автора.
Между механизмами, которые ожидают завершения потомков, и сигнальными механизмами, которые мы еще не обсуждали, есть значительное взаимодействие. Что из них описать вначале представляет собой нечто вроде проблемы курицы и яйца; мы решили сначала поговорить сначала о механизмах ожидания порожденного процесса, а глава 10 «Сигналы» дает полный рассказ о сигналах.
Пока достаточно понять, что сигнал является способом уведомления процесса о том, что произошло некоторое событие. Процессы могут генерировать сигналы, которые посылаются самим себе, или сигналы могут посылаться извне другими процессами или пользователем за терминалом. Например, CTRL-C посылает сигнал «прерывания», a CTRL-Z посылает сигнал управления работой «стоп».