Можно выделить шесть этапов в «жизненном цикле» программного обеспечения: анализ требований к системе; определение спецификаций; проектирование; программирование; тестирование; эксплуатация и сопровождение.
Методы повышения надежности программ можно применять на всех этапах. ПО разрабатывают на первых пяти этапах, где и возникают ошибки. Наибольшее число ошибок допускается при проектировании (до 60%) и программировании (до 40%). Однако и при тестировании и при сопровождении могут быть внесены новые ошибки.
Надежность программного обеспечения предусматривает следующие цели: предупреждение, обнаружение и исправление ошибок, обеспечение устойчивости, безопасности и недоступности.
Предупреждение ошибок – наиболее эффективный путь обеспечения надежности, так как ошибку легче предупредить, чем потом обнаружить ее и исправить. Предупреждение ошибок – это задача проектирования. Для предупреждения ошибок используют рациональную организацию труда разработчиков ПО, методы структурного программирования и автоматизацию проектирования.
Недостатки в решении административных проблем по управлению коллективом разработчиков ПО влияют на надежность ПО не меньше, чем недостатки в решении технических проблем. В результате опыта создания сложных программных продуктов сформировались некоторые общие принципы работы. Число исполнителей проекта должно быть минимально необходимым. Это упрощает и сокращает связи между участниками работы, способствует их взаимопониманию и уменьшает число ошибок при согласовании и стыковке различных частей проекта. Руководитель коллектива должен быть не только хорошим администратором, но и профессионалом, чтобы грамотно оценивать принимаемые решения и продукцию членов коллектива. Считается, что если программисты работают в официально оформленных бригадах, то это повышает их производительность, качество работы и преемственность. Управление разработкой должно осуществляться на основе последовательной реализации отдельных этапов проекта с организацией строго контроля и учета на каждом этапе. Эффективная концепция проверки состоит в реализации правила : правильность п-го этапа проекта должна проверяться разработчиками (п – 1)-го и (п + 1)-го этапов.
Концепция структурного программирования заключается в представлении (декомпозиции) сложной программы через более простые ее компоненты (модули) в виде некоторой структуры (рис. 3.37). Декомпозиция – это универсальный метод борьбы со сложностью систем. Модули программы А решают определенные функционально законченные задачи и имеют небольшую сложность. Они должны быть максимально независимы друг от друга. Программа имеет иерархическую структуру, в которой модули верхних уровней управляют работой модулей нижних уровней. Связи между модулями должны быть минимальны и по возможности сводиться только к передаче данных.
При проектировании иерархической модульной системы возможны два варианта: восходящее и нисходящее проектирование. В первом случае разработка модулей идет снизу вверх. После того как выработаны и спроектированы модули нижнего уровня (например, модули , , …, ), они объединяются с помощью подсистемы более высокого уровня , которая определяет порядок работы этих модулей, процедуру их вызова, обмена данными и др. Затем аналогично модули , , …, объединяются с помощью модуля и т.д.
При нисходящем проектировании (сверху вниз) сначала задачу А разбивают на несколько достаточно крупных подзадач , , …, . При написании текста модуля А используют имена модулей , , …, , которые еще не написаны и являются пустыми, содержащими лишь операторы входа и выхода (такие модули называются «заглушками»). В дальнейшем также поступают и с модулями , , …, . Процесс завершается тогда, когда вновь полученные модули оказываются настолько простыми, что могут быть записаны с использованием операторов языка программирования.
Программу называют структурированной, если она удовлетворяет следующим основным требованиям: имеет модульное иерархическое построение; модули являются функционально законченными, независимыми и автономными подпрограммами и имеют один вход и один выход; текст программы и любого модуля легко читается сверху вниз; логика всех модулей построена на базе ограниченного числа элементарных программных конструкций. Последнее требование основано на теореме о структурировании [3.12], которая утверждает, что любая программа может быть построена с помощью трех управляющих структур: THEN; IF–THEN–ELSE; DO–UNTIL (рис. 3.38). Структура THEN (простая вычислительная последовательность) обеспечивает преобразование или перемещение данных. Структура IF–THEN–ELSE (ЕСЛИ–ТО–ИНАЧЕ) осуществляет выбор одной из двух альтернатив преобразования данных в зависимости от истинности Т или ложности F некоторого условия. Структуру DO–UNTIL (ДЕЛАТЬ–ПОКА–НЕ) применяют, если требуется выполнить одно и то же преобразование более одного раза. Эти структуры имеют один вход и один выход и могут соединяться между собой, образуя более сложные структуры. Программы, написанные с использованием таких структур, не имеют операторов безусловного перехода GO TO и возврата RETURN и поэтому могут быть написаны и прочитаны сверху вниз.
Принцип структурного программирования позволяет систематизировать и упростить процессы проектирования ПО и сделать реальной задачу написания корректных программ с малым количеством ошибок.
В уже написанной программе до начала ее эксплуатации ошибки обнаруживают, используя тестирование и верификацию. Тестирование состоит в выполнении программы при отсутствии реальной внешней среды с целью обнаружения ошибок. Специально подбирают входные данные (тесты) и реакция ПО для этих данных сравнивается с эталонной. В структурированной программе выделяют четыре уровня тестирования: тестирование модулей, сопряжений между модулями, тестирование внешних функций, комплексное тестирование. На уровне модулей проверяют логику программы. Контроль сопряжений обнаруживает ошибки в межмодульном интерфейсе. Тестирование внешних функций определяет соответствие внешних спецификаций и функций программы. Комплексное тестирование является завершающим этапом проверки системы. Поскольку процедуры тестирования могут проводиться параллельно с разработкой ПО, то в соответствии с концепцией проектирования используют восходящее и нисходящее тестирование.
Тестирование осуществляется в три этапа: составление (генерация) тестов, выполнение программы на этих тестах и оценка полученных результатов. Выполнение программы на тестах может осуществляться вручную (на бумаге «в уме») для несложных программ (статическое тестирование) или с исполнением на ЭВМ (динамическое тестирование). Если результаты прохождения теста анализирует программист, то это трудоемкая и рутинная, но необходимая работа. Для автоматизации этого процесса требуется иметь эталонные результаты и хранить их в памяти ЭВМ для сравнения. Возможно также наличие эталонной программы, которая запускается на тех же тестах и вырабатывает эталонные выходные данные.
Тесты составляют программисты или автоматические системы генерации тестов. Это наиболее сложный и творческий процесс. Используются два подхода при решении этой задачи: функциональный и структурный. В первом случае программа рассматривается как «черный ящик». Это означает, что ее внутренняя структура никак не учитывается, и тесты составляют на основании функциональных спецификаций.
Построим, например, функциональный тест для программы LIGHT (табл. 3.7), вычисляющей функции (3.6) и (3.7): , . Задача теста состоит в проверке реализации программой именно данных функций. Простейший способ – испытать программу на всех входных наборах. Их число в данном случае равно 28 = 256. Это тривиальный тест. Он дает исчерпывающее тестирование и не требует знаний о внутренней структуре программы. Однако исчерпывающее тестирование обычно невозможно из-за большого объема тривиального теста.
Необходимо строить тест, обеспечивающий достаточную полноту проверки, но такого объема, чтобы его применение было экономически оправдано. Для оценки полноты функционального теста используют различные критерии: 1) проверку всех классов входных данных, когда тест должен содержать по одному представителю из каждого класса; 2) проверку всех классов выходных данных, когда при исполнении тестовых примеров должно быть получено по одному представителю из каждого класса; 3) проверку всех функций, когда каждая реализуемая программой функция должна быть проверена хотя бы один раз; 4) проверку всех ограничений хотя бы один раз и т.п.
Применим первые три критерия для построения теста программы LIGHT (табл. 3.7). Поскольку входные переменные х принимают только два значения (0 и 1), то в соответствии с первым критерием каждая переменная должна иметь оба эти значения. Выходные данные делятся на два класса: f = 0 и f = 1. Поэтому тест должен содержать по крайней мере один разрешенный и один запрещенный наборы для функций и . Третий критерий требует проверку каждой функции, программы хотя бы один раз. Наша программа проверяет условия включения ламп Н и Ч (если все переменные соответственно функций и равны 1) и условия невключения (если хотя бы одна из переменных не равна 1). Исходя из сказанного можно построить следующий тест (11111111, 01110111, 10111011, 11011101, 11101110).
Определим обнаруживаются ли данным тестом два отказа в программе LIGHT, рассмотренные в разделе 3.7. Ошибочная запись команды № 3 MOV C, M2 приводит к неправильному вычислению функции . Этот отказ обнаруживается на тестовом наборе 11011101. Правильная программа дает результат: = 0, = 0; программа с ошибкой – результат = 1, = 0. Второй отказ типа ANA B ® ORA B при записи команды № 5 приводит к неправильному вычислению . Эта ошибка обнаруживается на двух тестовых наборах 01110111 и 10111011.
Доказать полноту функционального теста (если он не тривиальный) достаточно сложно. То же относится и к тестам, построенным на основе структурного подхода с использованием информации о внутренней структуре программы. При этом применяют в основном два критерия: проверку каждой команды не менее 1 раза; проверку каждого пути программы не менее 1 раза. Последний критерий наиболее «сильный». Если программа содержит циклы, то ограничиваются тестированием простых, ациклических путей или число итераций ограничивается некоторой константой.
Рассмотрим, например, бинарную программу на рис. 3.9, а. Она содержит пять путей: П1 = 1, 2, 10; П2 = 1, 3, 4, 10; П3 = 1, 3, 5, 6, 10; П4 = 1, 3, 5, 7, 8, 10; П5 = 1, 3, 5, 7, 9. Эти пути тестируются соответственно наборами следующего теста: (0111, 1011, 1101, 1110, 1111). Пусть при написании программы допущена ошибка: вместо оператора условного перехода = 0? (переход по нулевому результату) записан оператор ¹ 0? (переход по единичному результату). Эта ошибка обнаруживается набором входных переменных 1101. В правильной программе на данном наборе реализуется путь П3 = 1, 3, 5, 6, 10 и выходной результат = 0. В программе с ошибкой реализуется путь П5 = 1, 3, 5, 7, 9 и ошибочный выходной результат = 1.
Даже для сравнительно несложных программ тестирование не может доказать отсутствие в них ошибок, а может только обнаружить некоторую их часть. На доказательство отсутствия ошибок ориентированы методы верификации.
Верификация – это математическое доказательство правильности программ. Общая методология доказательства состоит в том, что для программы составляется некоторая система утверждений, истинность которых доказывается с помощью правил логического вывода. Методы верификации сложны и их широкое практическое применение возможно при решении задачи автоматизации доказательств.
В процессе эксплуатации надежность ПО выражается через свойства ее устойчивости и безопасности. Эти свойства, также как и аналогичные свойства аппаратных средств, обеспечиваются введением избыточности, которая может быть структурной, информационной и временнóй. Дополнительные программные средства должны обеспечивать последовательное решение задач обнаружения искажения вычислительного процесса, ограничения последствий этого искажения в пределах некоторого участка программы (программного модуля) и восстановления правильного результата вычислений. В безопасных управляющих программах вместо восстановления возможен перевод системы в защитное состояние.
Временнáя избыточность состоит в выделении специальных интервалов времени для организации процедур контроля и восстановления. При этом функциональные задачи не решаются, поэтому производительность вычислительной системы уменьшается. Широко применяют предстартовый функциональный контроль с использованием контролирующих и диагностических тестов при решении контрольных задач. Сохранность программ проверяется суммированием кодов программы и сравнением результата с контрольной суммой. В системах управления, работающих в реальном масштабе времени, временнáя избыточность используется, если существуют технологические перерывы в работе систем, во время которых осуществляется тестирование или повторный счет.
Информационная избыточность заключается в резервировании информационных массивов и в применении корректирующих кодов для представления информации. В случае разрушения основного информационного массива программа обращается к резервному, который используется до полного восстановления основного массива. Эффективным способом обеспечения устойчивости является организация дополнительной информации о текущем состоянии программы в контрольных точках. Это состояние сохраняется для восстановления вычислительного процесса с ближайшей контрольной точки от места возникновения ошибки.
При организации структурной избыточности программ используют методы п-вариантного и самопроверяемого программирования. Варианты одной и той же программы могут быть одинаковыми или различаться методами решения задачи или способами программной реализации одного и того же метода. Целесообразно также, чтобы разные варианты программ были написаны различными бригадами программистов. При исполнении программы результат вычислений выбирают голосованием. Последовательная реализация программ требует больших затрат времени, поэтому п-вариантное программирование используют обычно в многопроцессорных вычислительных системах. В безопасных системах часто применяют двухвариантное программирование с контролем совпадения результатов. На применение в безопасных системах ориентированы также самопроверяемые программы.
Управляющая программа называется самопроверяемой, если она защищена от ошибок и является самотестируемой. Программа называется защищенной, если при возникновении любой ошибки из заданного класса на любой рабочей последовательности входных данных выходные данные вычисляются правильно или являются защитными. Программа называется самотестируемой, если для каждой ошибки из заданного класса существует хотя бы одна рабочая последовательность входных данных, на которой вычисляется хотя бы одно защитное значение выходных данных.
Защищенность от ошибок исключает неправильные воздействия со стороны программной системы на управляемые объекты. Самотестируемость исключает наличие необнаруживаемых ошибок и их накопление с течением времени. Эти два свойства являются основными требованиями к безопасным системам. Самопроверяемая программа П имеет контрольный модуль КМ (рис. 3.39). Его задачей является анализ выходных данных программы и решение вопроса о том, имела ли место ошибка в процессе вычисления. Контрольный модуль должен быть самопроверяемым, то есть обнаруживать собственные ошибки.
Дата: 2018-11-18, просмотров: 971.