Глобальное и локальное разворачивание стека

Исключения и аварийные завершения вызывают глобальное разворачивание стека (global stack unwind) в поиске обработчика, как было показано на рис. 4.1. Предположим, например, что в отслеживаемом блоке примера, приведенного в конце предыдущего раздела, исключение возникает прежде, чем активизируются FP-исключения. Тогда перед обработчиком исключения в стеке могут находиться многочисленные обработчики завершения.

Вспомните, что структура стека является динамической, как показано на рис. 4.1, и что в стеке, наряду с другими данными, хранятся данные обработчиков исключений и завершения. Фактическое содержимое стека в любой момент времени зависит от следующих факторов:

• Статической структуры программных блоков.

• Динамической структуры программы, отражаемой в последовательности открытых вызовов функций.

Обработчики завершения: завершение процессов и потоков

Обработчики завершения не выполняются, если выполнение процесса или потока было прекращено независимо от того, было ли это инициировано самим процессом путем использования функций ExitProcess или ExitThread, или вызвано извне, например, инициировано вызовом функций TerminateProcess или TerminateThread из другого места в программе. Поэтому ни одна из этих функций не должна вызываться процессом или потоком внутри блоков try…except или try…finally.

Обратите также внимание, что выполнение функции библиотеки С exit или возврат из функции main приводят к выходу из процесса.

SEH и обработка исключений в C++

При обработке исключений в C++ используются ключевые слова catch и throw, а сам механизм исключений реализован с использованием SEH. Тем не менее, обработка исключений в C++ и SEH — это разные вещи. Их совместное применение требует внимательного обращения, поскольку обработчики исключений, написанные пользователем и сгенерированные C++, могут взаимодействовать между собой и приводить к нежелательным последствиям. Например, находящийся в стеке обработчик __except может перехватить исключение C++, в результате чего данное исключение так и не дойдет до обработчика C++.

Возможно и обратное, когда, например, обработчик C++ перехватит SEH-исключение, сгенерированное функцией RaiseException. Документация Microsoft рекомендует полностью отказаться от использования обработчиков Windows в программах на C++ и ограничиться применением в них только обработчиков исключений C++.

Кроме того, обработчики исключений или завершения Windows не осуществляют вызов деструкторов, что в ряде случаев необходимо для уничтожения экземпляров объектов C++.

Пример: использование обработчиков завершения для повышения качества программ

Обработчики исключений и завершения позволяют повысить надежность программ как за счет упрощения процедуры восстановления программы после возникновения ошибок и исключений, так и за счет гарантированного освобождения ресурсов и отмены блокирования файлов в критических ситуациях.

В программе toupper (программа 4.2) эти моменты иллюстрируются с привлечением идей, почерпнутых в программном коде предшествующих примеров. toupper обрабатывает несколько файлов, имена которых указываются в командной строке, переписывая их с преобразованием всех букв в верхний регистр. Имена преобразованных файлов получаются путем добавления префикса UC_ к исходным именам, и согласно "спецификации" программы запись поверх существующих файлов не производится. Преобразование файлов осуществляется в памяти машины, поэтому для каждого файла выделяется большая буферная область (достаточная для размещения всего файла). Кроме того, чтобы исключить любую возможность изменения файлов другими процессами, а также для того, чтобы вновь создаваемые выходные файлы строго соответствовали преобразованным входным файлам, оба вида файлов блокируются во время обработки. Понятно, что на каждой стадии обработки существует вероятность возникновения самых различных сбойных ситуаций, но в программе должна быть предусмотрена защита от подобных ошибок, и она должна располагать средствами, позволяющими ей восстановить свое нормальное состояние и попытаться обработать все остальные файлы, имена которых были указаны в командной строке. Программа 4.2 решает все эти задачи, обеспечивая разблокирование файлов во всех необходимых случаях без применения громоздкой логики операторов ветвления, к которым пришлось бы прибегнуть, если бы не были использованы средства SEH. Более подробные комментарии к программе содержатся в программном коде, находящемся на Web-сайте книги.

Программа 4.2. toupper: обработка файлов с восстановлением нормального состояния программы после сбоев

/* Глава 4. Команда toupper. */

/* Преобразование содержимое одного и более файлов с заменой всех букв на прописные. Имя выходного файла получается из имени входного файла добавлением к нему префикса UC_. */

#include "EvryThng.h"

int _tmain(DWORD argc, LPTSTR argv[]) {

HANDLE hIn = INVALID_HANDLE_VALUE, hOut = INVALID_HANDLE_VALUE;

DWORD FileSize, nXfer, iFile, j;

CHAR OutFileName [256] = "", *pBuffer = NULL;

OVERLAPPED ov = {0, 0, 0, 0, NULL}; /* Используется для блокирования файлов. */

if (argc <= 1) ReportError(_T("Использование: toupper файлы"), 1, FALSE);

/* Обработать все файлы, указанные в командной строке. */

for (iFile = 1; iFile < argc; iFile++) __try { /* Блок исключений. */

/* Все дескрипторы файлов недействительны, pBuffer == NULL, а файл OutFileName пуст. Выполнение этих условий обеспечивается обработчиками. */

_stprintf(OutFileName, "UC_%s", argv[iFile]);

__try { /* Внутренний блок try-finally. */

/* Ошибка на любом шаге сгенерирует исключение, и следующий */

/* файл будет обрабатываться только после "уборки мусора". */

/* Объем работы по очистке зависит от того, в каком месте */

/* программы возникла ошибка. */

/* Создать выходной файл (завершается с ошибкой, если файл уже существует). */

hIn = CreateFile(argv[iFile], GENERIC_READ, 0, NULL, OPEN_EXISTING, 0, NULL);

if (hIn == INVALID_HANDLE_VALUE) ReportException(argv[iFile], 1);

FileSize = GetFileSize(hIn, NULL);

hOut = CreateFile(OutFileName, GENERIC_READ | GENERIC_WRITE, 0, NULL, CREATE_NEW, 0, NULL);

if (hOut == INVALID_HANDLE_VALUE) ReportException(OutFileName, 1);

/* Распределить память под содержимое файла. */

pBuffer = malloc(FileSize);

if (pBuffer == NULL) ReportException(_T("Ошибка при распределении памяти"), 1);

/* Блокировать оба файла для обеспечения целостности копии. */

if (!LockFileEx(hIn, LOCKFILE_FAIL_IMMEDIATELY, 0, FileSize, 0, &ov) ReportException(_T("Ошибка при блокировании входного файла"), 1);

if (!LockFileEx(hOut, LOCKFILE_EXCLUSIVE_LOCK | LOCKFILE_FAIL_IMMEDIATELY, 0, FileSize, 0, &ov) ReportException(_T("Ошибка при блокировании выходного файла "), 1);

/* Считать данные, преобразовать их и записать в выходной файл. */

/* Освободить ресурсы при завершении обработки или возникновении */

/* ошибки; обработать следующий файл. */

if (!ReadFile(hIn, pBuffer, FileSize, &nXfer, NULL)) ReportException(_T("Ошибка при чтении файла"), 1);

for (j = 0; j < FileSize; j++) /* Преобразовать данные. */

if (isalpha(pBuffer [j])) pBuffer[j] = toupper(pBuffer [j]);

if(WriteFile(hOut, pBuffer, FileSize, &nXfer, NULL)) ReportException(T("Ошибка при записи в файл"), 1);

} __finally {

/*Освобождение блокировок, закрытие дескрипторов файлов,*/

/*освобождение памяти и повторная инициализация */

/*дескрипторов и указателя. */

if (pBuffer != NULL) free (pBuffer);

pBuffer = NULL;

if (hIn != INVALID_HANDLE_VALUE) {

UnlockFileEx(hIn, 0, FileSize, 0, &ov);

CloseHandle(hIn);

hIn = INVALID_HANDLE_VALUE;

}

if (hOut != INVALID_HANDLE_VALUE) {

UnlockFileEx(hOut, 0, FileSize, 0, &ov);

CloseHandle(hOut);

hOut = INVALID_HANDLE_VALUE;

}

_tcscpy(OutFileName, _T(""));

}

}

/* Конец основного цикла обработки файлов и блока try. */

/* Обработчик исключений для тела цикла. */

__except(EXCEPTION_EXECUTE_HANDLER) {

_tprintf(_T("Ошибка при обработке файла %s\n"), argv[iFile]);

DeleteFile (OutFileName);

}

_tprintf(_T("Обработаны все файлы, кроме указанных выше \n"));

return 0;

}

Пример: использование функции фильтра

Программа 4.3 представляет собой каркас программы, иллюстрирующей обработку исключений и завершения выполнения, в которой используется функция фильтра. Программа предлагает пользователю указать тип исключения, после чего продолжает работу для генерации исключения. Функция фильтра обрабатывает различные типы исключений по-разному; выбор вариантов, предусмотренных в программе, был совершенно произвольным и определялся исключительно целями демонстрации. В частности, программа обнаруживает попытки обращения к недоступным областям памяти, предоставляя адреса виртуальной памяти, по которым производилось такое обращение.

Блок __finally восстанавливает состояние маски FP-исключений. Совершенно очевидно, что восстановление состояния маски в данном случае, когда процесс уже должен завершаться, особого значения не имеет, но эта методика пригодится нам впоследствии, когда мы будем использовать ее на стадии завершения выполнения потока. Вообще говоря, процесс должен восстанавливать и системные ресурсы, например, удалять временные файлы, освобождать ресурсы синхронизации (глава 8) и отменять блокирование файлов (главы 3 и 6). Функция фильтра представлена в программе 4.4.

Данный пример не иллюстрирует обработку исключений, которые могут возникать при распределении памяти; эти исключения мы начнем интенсивно использовать в главе 5.

Программа 4.3. Exception: обработка исключений и завершения выполнения

#include "EvryThng.h"

#include <float.h>

DWORD Filter(LPEXCEPTION_POINTERS, LPDWORD);

double x = 1.0, у = 0.0;

int _tmain(int argc, LPTSTR argv[]) {

DWORD ECatgry, i = 0, ix, iy = 0;

LPDWORD pNull = NULL;

BOOL Done = FALSE;

DWORD FPOld, FPNew;

FPOld = _controlfp(0, 0); /* Сохранить старую управляющую маску. */

/* Разрешить FP-исключения. */

FPNew = FPOld & ~(EM_OVERFLOW | EM_UNDERFLOW | EM_INEXACT | EM_ZERODIVIDE | EM_DENORMAL | EM_INVALID);

_controlfp(FPNew, MCW_EM);

while (!Done) _try { /* Блок try-finally. */

_tprintf(_T("Введите тип исключения: "));

_tprintf(_T(" 1: Mem, 2: Int, 3: Flt 4: User 5: __leave "));

_tscanf(_T("%d"), &i);

__try { /* Блок try-except. */

switch (i) {

case 1: /* Исключение при обращении к памяти. */

ix = *pNull;

*pNull = 5;

break;

case 2: /* Исключение при выполнении арифметических операций над целыми числами. */

ix = ix / iy;

break;

case 3: /* FP-исключение. */

x = x / у;

_tprintf(_T("x = %20e\n"), x);

break;

case 4: /* Пользовательское исключение. */

ReportException(_T("Пользовательское исключение"), 1);

break;

case 5: /* Использовать оператор _leave для завершения выполнения.*/

__leave;

default:

Done = TRUE;

}

} /* Конец внутреннего блока __try. */

__except(Filter(GetExceptionInformation(), &ECatgry)) {

switch(ECatgry) {

case 0:

_tprintf(_T("Неизвестное исключение\n"));

break;

case 1:

_tprintf(_T("Исключение при обращении к памяти\n"));

continue;

case 2:

_tprintf(_T("Исключение при выполнении арифметических операций над целыми числами \n"));

break;

case 3:

_tprintf(_Т("FР-исключение\n"));

_clearfp();

break;

case 10:

_tprintf(_T("Пользовательское исключение\n"));

break;

default:

_tprintf(_T("Неизвестное исключение\n"));

break;

} /* Конец оператора switch. */

_tprintf(_Т("Конец обработчика\n"));

}

/* Конец блока try-except. */

} /* Конец цикла while – ниже находится обработчик завершения. */

__finally { /* Это часть цикла while. */

_tprintf(_T("Аварийное завершение?: %d\n"),

AbnormalTermination());

}

_controlfp(FPOld, 0xFFFFFFFF); /* Восстановить старую FP-маску.*/

return 0;

}

Программа 4.4 представляет функцию фильтра, используемую в программе 4.3. Эта функция просто проверяет и классифицирует различные возможные значения кодов исключений. В программном коде, размещенном на Web-сайте книги, проверяется каждое из возможных значений, в то время как приведенная ниже функция осуществляет проверку лишь тех из них, которые нужны для тестовой программы.

Наши рекомендации