Точно так же он способен регистрировать множество обработчиков выхода. Функции, зарегистрированные с помощью вызовов 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.4. Взаимодействие между буферами stdio и вызовами fork() и _exit()

Вывод, сгенерированный программой из листинга 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, что обычно и требуется.

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

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