Верификация программного обеспечения

Методы разработки устойчивого кода

Разбить на страницы
Показывать лекцию целиком

25.1. Классификация проблем, возникающих при работе программных систем

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

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

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

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

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

25.1.1. Сбои

Можно выделить следующие три вида сбоев, вызывающих отказные ситуации.

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

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

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

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

    В табл. 25.1 приведена классификация типов сбоев по указанным выше признакам.

    Классификация типов сбоев системы
    Параметры сбоя Тип сбоя
    Сбой в системном ПО Сбой в прикладном ПО Сбой из-за неверной технологии
    Точка возникновения сбояСистемные библиотеки или Приложение Приложение Неприменимо
    Информационное окружениеНенормальное Ненормальное Нормальное
    Сообщение о сбоеПользовательское или автоматическое Автоматическое Пользовательское

    25.1.2. Отказы и аварии

    Отказы вызывают длительное нарушение функционирования системы, или, по ГОСТ 27.002-89 [21], приводят ее в предельное состояние. Предельное состояние - это состояние, при котором дальнейшая эксплуатация системы недопустима или нецелесообразна либо восстановление ее работоспособного состояния невозможно или нецелесообразно. Тем самым в ГОСТ 27.002-89 не делается разницы между отказом и аварией. Будем называть отказом состояние системы, при котором восстановление ее работоспособного состояния возможно.

    Отказы классифицируются согласно ГОСТ 27.002-89 следующим образом.

    По временным характеристикам

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

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

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

    По причинам

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

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

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

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

    По способу обнаружения

    Явный отказ - отказ, который обнаруживается сразу после его возникновения штатными средствами контроля состояния системы.

    Скрытый отказ - отказ, который не обнаруживается штатными средствами контроля состояния системы либо обнаруживается ими спустя некоторое время после возникновения отказа. Такой отказ может послужить причиной для одного или нескольких зависимых отказов.

    По связи с другими отказами

    Независимый отказ - отказ, возникновение которого не обусловлено другими отказами.

    Зависимый отказ - отказ, возникновение которого вызвано другими отказами.

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

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

    25.2. Методы разработки устойчивого кода

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

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

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

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

    25.2.1. Критические точки и допущения (assertions)

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

    string_vector surname;
    string_vector phone;
    …
    
    int index = surname.find("Петров");
    foundPhone = phone.at(index);

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

    Например, в C существует функция assert(), определенная в заголовочном файле <assert.h>. Аргументом этой функции может выступать любое булево выражение. В случае, если оно равно false, функция прерывает работу программы. Таким образом, при помощи булевых выражений могут быть описаны допущения в критических точках программы. Предыдущий пример при использовании функции assert() будет выглядеть как

    #include <assert.h>
    …
    
    string_vector surname;
    string_vector phone;
    …
    
    int index = surname.find("Петров");
    assert( (index > 0)  (index < phone.size()) );
    foundPhone = phone.at(index);

    Часто программисты определяют свою собственную функцию assert(), например, следующим образом:

    #ifdef NODEBUG
    #define assert(ignore) 0
    #else
    #define assert(ex) \
    ((ex) ? 1 : \
    ( printf("Assertion failed "), \
    abort(), 0))
    #endif // NODEBUG

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

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

    Существует три причины использовать допущения в критических точках:

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

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

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

    25.2.2. Обработка исключений

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

    Рассмотрим класс, реализующий тип данных "вектор". Для получения значения элемента вектора в нем перегружается оператор [], в котором делается проверка допустимости значения индекса элемента. В качестве второго параметра функции Assert указывается метод класса, обрабатывающего исключительную ситуацию в случае, если значение индекса элемента недопустимо.

    class safeVector : public vector {
    public:
      class outOfRangeException {
        int l;
      public:
        outOfRangeException (const char *,
          const char *, int line)
          : l (line) {}
        int line (void) { return l; }
      };
      int safeVector::operator[] (int index) {
        Assert ((index >= 0)  (index < size),
        safeVector::outOfRangeException);
    ...
    };
    
    int process(safeVector v, int index) {
      int elem;
      try {
        elem = v[index];
      }
    
      catch (safeVector::outOfRangeException e) {
        cerr   << "Safe Vector range exception:\n";
        exit (1);
      }
      return elem;
    }

    Для того, чтобы обработчик исключения сработал, обращение к элементу массива помещается внутрь структурного блока try { }, где содержатся команды, результат выполнения которых потенциально может вызвать исключительную ситуацию. Затем внутри синтаксического блока catch { } определяется реакция на возникшую исключительную ситуацию outOfRangeException. Подобным образом можно определить реакцию на любую исключительную ситуацию.

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

    25.2.3. Сбор и обработка информации о сбоях и отказах

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

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

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

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

    Страницы:

    25.1. Классификация проблем, возникающих при работе программных систем

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

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

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

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

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

    25.1.1. Сбои

    Можно выделить следующие три вида сбоев, вызывающих отказные ситуации.

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

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

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

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

    В табл. 25.1 приведена классификация типов сбоев по указанным выше признакам.

    Классификация типов сбоев системы
    Параметры сбоя Тип сбоя
    Сбой в системном ПО Сбой в прикладном ПО Сбой из-за неверной технологии
    Точка возникновения сбояСистемные библиотеки или Приложение Приложение Неприменимо
    Информационное окружениеНенормальное Ненормальное Нормальное
    Сообщение о сбоеПользовательское или автоматическое Автоматическое Пользовательское

    25.1.2. Отказы и аварии

    Отказы вызывают длительное нарушение функционирования системы, или, по ГОСТ 27.002-89 [21], приводят ее в предельное состояние. Предельное состояние - это состояние, при котором дальнейшая эксплуатация системы недопустима или нецелесообразна либо восстановление ее работоспособного состояния невозможно или нецелесообразно. Тем самым в ГОСТ 27.002-89 не делается разницы между отказом и аварией. Будем называть отказом состояние системы, при котором восстановление ее работоспособного состояния возможно.

    Отказы классифицируются согласно ГОСТ 27.002-89 следующим образом.

    По временным характеристикам

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

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

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

    По причинам

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

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

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

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

    По способу обнаружения

    Явный отказ - отказ, который обнаруживается сразу после его возникновения штатными средствами контроля состояния системы.

    Скрытый отказ - отказ, который не обнаруживается штатными средствами контроля состояния системы либо обнаруживается ими спустя некоторое время после возникновения отказа. Такой отказ может послужить причиной для одного или нескольких зависимых отказов.

    По связи с другими отказами

    Независимый отказ - отказ, возникновение которого не обусловлено другими отказами.

    Зависимый отказ - отказ, возникновение которого вызвано другими отказами.

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

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

    25.2. Методы разработки устойчивого кода

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

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

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

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

    25.2.1. Критические точки и допущения (assertions)

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

    string_vector surname;
    string_vector phone;
    …
    
    int index = surname.find("Петров");
    foundPhone = phone.at(index);

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

    Например, в C существует функция assert(), определенная в заголовочном файле <assert.h>. Аргументом этой функции может выступать любое булево выражение. В случае, если оно равно false, функция прерывает работу программы. Таким образом, при помощи булевых выражений могут быть описаны допущения в критических точках программы. Предыдущий пример при использовании функции assert() будет выглядеть как

    #include <assert.h>
    …
    
    string_vector surname;
    string_vector phone;
    …
    
    int index = surname.find("Петров");
    assert( (index > 0)  (index < phone.size()) );
    foundPhone = phone.at(index);

    Часто программисты определяют свою собственную функцию assert(), например, следующим образом:

    #ifdef NODEBUG
    #define assert(ignore) 0
    #else
    #define assert(ex) \
    ((ex) ? 1 : \
    ( printf("Assertion failed "), \
    abort(), 0))
    #endif // NODEBUG

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

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

    Существует три причины использовать допущения в критических точках:

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

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

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

    25.2.2. Обработка исключений

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

    Рассмотрим класс, реализующий тип данных "вектор". Для получения значения элемента вектора в нем перегружается оператор [], в котором делается проверка допустимости значения индекса элемента. В качестве второго параметра функции Assert указывается метод класса, обрабатывающего исключительную ситуацию в случае, если значение индекса элемента недопустимо.

    class safeVector : public vector {
    public:
      class outOfRangeException {
        int l;
      public:
        outOfRangeException (const char *,
          const char *, int line)
          : l (line) {}
        int line (void) { return l; }
      };
      int safeVector::operator[] (int index) {
        Assert ((index >= 0)  (index < size),
        safeVector::outOfRangeException);
    ...
    };
    
    int process(safeVector v, int index) {
      int elem;
      try {
        elem = v[index];
      }
    
      catch (safeVector::outOfRangeException e) {
        cerr   << "Safe Vector range exception:\n";
        exit (1);
      }
      return elem;
    }

    Для того, чтобы обработчик исключения сработал, обращение к элементу массива помещается внутрь структурного блока try { }, где содержатся команды, результат выполнения которых потенциально может вызвать исключительную ситуацию. Затем внутри синтаксического блока catch { } определяется реакция на возникшую исключительную ситуацию outOfRangeException. Подобным образом можно определить реакцию на любую исключительную ситуацию.

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

    25.2.3. Сбор и обработка информации о сбоях и отказах

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

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

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

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

    Вернуться к учебному плану