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