Точно так же он способен регистрировать множество обработчиков выхода. Функции, зарегистрированные с помощью вызовов atexit() и on_exit(), попадают в один и тот же список. Если оба этих метода используются в одной и той же программе, зарегистрированные ими обработчики вызываются в порядке, обратном регистрации, независимо от способа регистрации.
Вызов on_exit() отличается большей гибкостью по сравнению с atexit(), но он не входит ни в один стандарт и лишь всего несколькими альтернативными реализациями системы UNIX, поэтому при написании переносимых приложений его следует избегать.
Применение вызовов atexit() и on_exit() для регистрации обработчиков выхода демонстрируется в листинге 25.1. При запуске этой программы мы увидим следующий вывод:
$ ./exit_handlers
on_exit function called: status=2, arg=20
atexit function 2 called
atexit function 1 called
on_exit function called: status=2, arg=10
Листинг 25.1. Использование обработчиков выхода
procexec/exit_handlers.c
#define _BSD_SOURCE /* Получаем объявление вызова on_exit() из
#include
#include "tlpi_hdr.h"
static void
atexitFunc1(void)
{
printf("atexit function 1 called\n");
}
static void
atexitFunc2(void)
{
printf("atexit function 2 called\n");
}
static void
onexitFunc(int exitStatus, void *arg) {
printf("on_exit function called: status=%d, arg=%ld\n",
exitStatus, (long) arg);
}
int
main(int argc, char *argv[])
{
if (on_exit(onexitFunc, (void *) 10)!= 0)
fatal("on_exit 1");
if (atexit(atexitFunc1)!= 0)
fatal("atexit 1");
if (atexit(atexitFunc2)!= 0)
fatal("atexit 2");
if (on_exit(onexitFunc, (void *) 20)!= 0)
fatal("on_exit 2");
exit(2);
}
procexec/exit_handlers.c
Вывод, сгенерированный программой из листинга 25.2, демонстрирует феномен, который на первый взгляд может показаться обескураживающим. Если запустить эту программу со стандартным выводом, направленным в терминал, получится вполне предсказуемый результат:
$ ./fork_stdio_buf
Hello world
Ciao
Но если перенаправить вывод в файл, мы увидим следующее:
$ ./fork_stdio_buf > a
$ cat a
Ciao
Hello world
Hello world
В выводе выше можно заметить два странных момента: строка, напечатанная функцией printf(), продублирована и ей почему-то предшествует вывод вызова write().
Листинг 25.2. Взаимодействие вызова fork() и буферизации stdio
procexec/fork_stdio_buf.c
#include "tlpi_hdr.h"
int
main(int argc, char *argv[])
{
printf("Hello world\n");
write(STDOUT_FILENO, "Ciao\n", 5);
if (fork() == –1)
errExit("fork");
/* Здесь продолжают выполнение как потомок, так и родитель */
exit(EXIT_SUCCESS);
}
procexec/fork_stdio_buf.c
Для того чтобы понять, почему сообщение, выводимое вызовом printf(), печатается два раза, вспомните, что буферы stdio находятся в пользовательском пространстве памяти процесса (см. раздел 13.2). Следовательно, эти буферы дублируются в дочернем процессе вызовом fork(). Когда стандартный вывод направлен в терминал, буферизация в нем по умолчанию происходит построчно, благодаря чему строки, разделенные разрывами, сразу же выводятся вызовом printf(). Однако стандартный вывод, направленный в файл, по умолчанию буферизируется по блокам. Таким образом, на момент вызова fork() строка, выводимая вызовом printf(), все еще находится в родительском буфере stdio и, следовательно, дублируется в потомке. Позже, когда родительский и дочерний процессы вызывают exit(), они оба сбрасывают свои копии буферов stdio, что приводит к дублированию результата.
Избежать этого можно одним из двух способов.
• В качестве решения, устраняющего непосредственно проблему буферизации stdio, перед вызовом fork() можно использовать функцию fflush(), которая сбрасывает соответствующий буфер. Или, как вариант, мы могли бы отключить буферизацию потока stdio с помощью вызовов setvbuf() или setbuf().
• Потомок может выполнять вызов _exit() вместо exit(), чтобы не сбрасывать буферы стандартного ввода/вывода. Этот метод иллюстрирует более общее правило: в приложении, создающем потомка, который не выполняет новой программы, обычно только один из процессов (чаще всего родитель) должен завершаться с помощью функции exit(), тогда как для всех остальных следует использовать вызов _exit(). Это дает гарантию того, что только один процесс вызывает обработчики выхода и сбрасывает буферы stdio, что обычно и требуется.