Ошибки и обработчики
Обращение к обработчику ошибок
Раскрутка
Конфликтующие раскрутки

Листинг 1. Регистрация и удаление обработчика ошибок

Листинг 2. Процедура для обработки сбоев в памяти


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

Ошибки и обработчики

Если в процессе работы программы происходит какое-то событие, из-за которого дальнейшее нормальное выполнение становится невозможным, OS/2 передает управление специальной подсистеме обработки ошибок, или, более точно, нештатных ситуаций (exceptions; в русскоязычной литературе используются термины "исключительная ситуация", а также просто "исключение"). Причиной возникновения ошибки может стать сбой ввода/вывода, нарушение защиты, арифметическая ошибка (например, попытка деления на 0). Особую категорию ошибок составляют асинхронные, т. е. вызванные внешним вмешательством. Это сигналы (signal exceptions), вырабатываемые в результате нажатия клавиш + или +C либо при вызове для выполняющегося процесса функции DosKillProcess; асинхронной ошибкой считается также сообщение о завершении работы. Ошибки, вызванные событиями внутри выполняющейся нити, называются синхронными.

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

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

Если программа регистрирует собственный обработчик ошибок, перед возвратом управления она должна убрать его из цепочки. Для этой цели служат две функции. Функция DosUnsetExceptionHandler просто удаляет обработчик из цепочки, функция DosUnwindException предварительно запускает его на выполнение. Выполнение с последующим удалением - так называемую раскрутку (unwinding) - мы подробнее разберем в конце статьи.

В пределах одной процедуры не следует смешивать стандартные API-функции OS/2 (DosSetExceptionHandler, DosUnsetExceptionHandler, DosUnwindException) с аналогичными функциями, специфическими для языка программирования: это может привести к непредсказуемым последствиям.

Регистрация и удаление обработчика ошибок показаны в листинге 1. При написании обработчиков необходимо учитывать следующее.

  • Нет необходимости писать отдельную процедуру для каждой ошибки: для ошибок, которые обрабатываются одинаково, можно (и нужно) написать общий обработчик.
  • Синхронные ошибки передаются операционной системой только в те нити управления, где они возникли, поэтому для каждой нити необходимо отдельно зарегистрировать обработчик ошибок. При завершении задачи операционная система передает сигнал завершения всем нитям управления процесса, при нажатии клавиш + она посылает асинхронный сигнал прерывания главной нити.
  • DLL-библиотеки помещают свои обработчики ошибок в цепочку перед обработчиками вызвавшей программы, а перед возвратом управления удаляют их. Таким образом, если во время работы DLL произойдет ошибка и DLL сама ее обработает, программа не получит об этом никакой информации.
  • Сигналы, вырабатываемые при нажатии клавиш + или +C, воспринимаются только программами для полноэкранной среды OS/2 и оконной среды VIO (программам для среды Presentation Manager они недоступны), причем только в том случае, если их получение заказано в явной форме путем вызова функции DosSetSignalExceptionFocus с параметром ON (разумеется, необходимо также зарегистрировать обработчик сигнала). Названные ограничения не распространяются на сигнал, вырабатываемый при вызове для программы функции DosKillProcess.
  • Обращение к обработчику ошибок

    Пример обработчика ошибок приведен в листинге 2. Всего обработчик получает четыре параметра: ExceptionReportRecord, ExceptionRegistrationRecord, ContextRecord и DispatcherContext. Опишем их.

  • ExceptionReportRecord (входной/ выходной параметр) - указатель на структуру, содержащую описание ошибки.
  • ExceptionRegistrationRecord (входной/выходной параметр) - указатель на регистрационную запись текущего обработчика, используется для микропроцессоров 386.
  • ContextRecord (входной/выходной параметр) - указатель на так называемую запись контекста, описывающую состояние машины в момент ошибки.
  • DispatcherContext (выходной параметр) - указатель на зарезервированное поле, в которое система помещает информацию о состоянии вложенных ошибок и конфликтующих раскруток. Эта информация используется диспетчером ошибок (в случае вложенных ошибок) или процедурой раскрутки (в случае конфликтующих раскруток); пользовательская программа не должна ее модифицировать.
  • Допустимые возвращаемые значения - XCPT_CONTINUE_EXECUTION (ошибка обработана, можно продолжить работу) и XCPT_CONTINUE_ SEARCH (ошибка не обработана и должна быть передана следующему обработчику в цепочке). Не распознав номер ошибки, обработчик вернет значение XCPT_CONTINUE_ SEARCH.

    Ошибки могут быть допускающими продолжение работы (continuable) и не допускающими продолжения (non-continuable). Во втором случае возвращать значение XCPT_CONTINUE_EXECUTION нельзя - возникнет ошибка XCPT_NONCONTINUABLE_EXCEPTION.

    Общих ограничений на использование функций внутри обработчиков нет. Обработчик завершения процесса не должен создавать новые нити (функция DosCreateThread), запускать программы (DosExecPgm) и сеансы (DosStartSession), а также завершать процесс (DosExit): это может привести к непредсказуемым последствиям.

    Поскольку обработка ошибки не должна быть прервана из-за возникновения ошибки в другой нити управления, соответствующую секцию кода следует сделать критической, или непрерываемой (must-complete).

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

    Раскрутка

    При выполнении нелокальной инструкции GOTO (переходе на метку, находящуюся за пределами текущей процедуры), когда программа теряет возможность корректно удалить зарегистрированные ею обработчики ошибок, применяется раскрутка. (Раскрутку можно использовать также при завершении нити управления; это хороший метод освобождения занятых нитью ресурсов.) Функция раскрутки DosUnwindException последовательно вызывает и удаляет обработчики ошибок с начала цепочки обработчиков вплоть до заданного (который не удаляется).

    Хотя функция раскрутки вызывается из программы обычным образом, состояние компьютера на момент ее вызова фиксируется в структуре ContextRecord - точно так же, как при возникновении ошибки. Число параметров DosUnwindException может быть переменным (их передача организована в соответствии с соглашением о вызовах языка Си), что можно использовать для передачи необходимой информации по адресу назначения при нелокальном переходе. Всегда передаются адрес регистрационной записи того обработчика, на котором должна быть остановлена раскрутка, и тот адрес, по которому должно быть передано управление после окончания раскрутки. Если передать в качестве адреса обработчика -1, DosUnwindException раскрутит цепочку до конца, если 0 - раскрутит цепочку до конца и завершит работу программы (полная раскрутка). При отсутствии в цепочке заданной регистрационной записи возникает ошибка XCPT_INVALID_UNWIND_TARGET.

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

    Конфликтующие раскрутки

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

    Отсутствие у второй раскрутки точки назначения приводит к ошибке XCPT_INVALID_UNWIND_TARGET (см. выше), и в результате управление вновь получает первая раскрутка. В случае, когда точка назначения второй раскрутки существует, первая раскрутка отменяется, а вторая продолжается до того обработчика, на котором должна была закончиться первая.

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

    В заключение заметим, что при работе с API-функциями OS/2 всегда следует проверять код возврата. В приводимых здесь (и в предыдущей статье - см. "Мир ПК", #7/97, с. 42) листингах эта проверка опущена ради экономии места, однако для обеспечения устойчивой работы программы она необходима.


    Николай Смирнов - руководитель направления OS/2 в российском представительстве IBM EEA, тел.: (095)940-20-00
    Евгений Лызенко - ведущий инженер компании "ТУКА", e-mail: madm@tuka.msk.ru

    Листинг 1. Регистрация и удаление обработчика ошибок

    #define INCL_BASE
    #define INCL_DOSEXCEPTIONS
    #include 
    
    ULONG _cdecl myHandler( PEXCEPTIONREPORTRECORD,
                            PEXCEPTIONREGISTRATIONRECORD,
                            PCONTEXTRECORD,
                            PVOID);
    
    VOID main(VOID)
    
    {
      EXCEPTIONREGISTRATIONRECORD xcpthand = { 0, &myHandler };
      /* Обратите внимание: структура xcpthand локальная */
      /* и хранится в стеке */
    
      DosError(FERR_DISABLEEXCEPTION | FERR_DISABLEHARDERR);
      DosSetExceptionHandler(&xcpthand);
    
      /* Здесь происходит выполнение других задач; при */
      /* возникновении ошибки управление получит myHandler. */
    
      DosUnsetExceptionHandler(&xcpthand);
    }
    
    

    Листинг 2. Процедура для обработки сбоев в памяти

    #define INCL_BASE
    #define INCL_DOSEXCEPTIONS
    #include 
    
    #define HF_STDERR 2 /* Индекс вывода стандартных ошибок */
      ULONG _cdecl myHandler(
            PEXCEPTIONREPORTRECORD pERepRec,
            PEXCEPTIONREGISTRATIONRECORD pERegRec,
            PCONTEXTRECORD pCtxRec,
            PVOID p)
    
    {
       ULONG   cbWritten, ulMemSize, flMemAttrs;
       APIRET  rc;
       /* Нарушение защиты в известном месте */
    
       if (pERepRec->ExceptionNum == XCPT_ACCESS_VIOLATION
        && pERepRec->ExceptionAddress != (PVOID) XCPT_DATA_UNKNOWN)
       { /* Сбой страницы */
         if ((pERepRec->ExceptionInfo[0] == XCPT_READ_ACCESS
           || pERepRec->ExceptionInfo[0] == XCPT_WRITE_ACCESS)
           && pERepRec->ExceptionInfo[1] != XCPT_DATA_UNKNOWN)
         {
           DosWrite(HF_STDERR, "
    Page Fault
    ", 15, &cbWritten);
           ulMemSize = 1;
           /* Опросим память, чтобы найти причину сбоя. */
           DosQueryMem((PVOID) pERepRec->pExceptionInfo[1],
                       &ulMemSize,
                       &flMemAttrs);
    
           /* Если память не свободна или не зафиксирована, зафиксируем ее. */
           if (!(flMemAttrs & (PAG_FREE | PAG_COMMIT)))
           {
             DosWrite(HF_STDERR,
                      "
    Attempt to access uncommitted memory
    ",
                      40, &cbWritten);
    
             rc = DosSetMem((PVOID) pERepRec->ExceptionInfo[1],
                            4096,
                            PAG_DEFAULT | PAG_COMMIT);
    
             if (rc)
             {
                DosWrite(HF_STDERR,
                         "
    Error committing memory
    ",
                         27, &cbWritten);
                return (XCPT_CONTINUE_SEARCH);
             }
    /* В противном случае природа ошибки другая */
             else return (XCPT_CONTINUE_EXECUTION);
       } } }
       return (XCPT_CONTINUE_SEARCH);
    }