Особенности построения программ реального времени на основе параллельных процессов. Реализация простой программы, которая выводит на экран текст приветствия и завершается. Создание массива из трехсот параллельных процессов, получающих уникальный индекс.
Аннотация к работе
Библиотека 2KLIB разрабатывалась как средство программирования систем реального времени, представляет собой параллельное расширение языка Си и дает программисту набор понятий-классов, позволяющих строить программу в терминах параллельных процессов и их взаимодействий. В терминах параллельного объектно-ориентированного подхода аппаратные устройства описываются как процессы-классы, наследующие свойства параллельного процесса и взаимодействующие с другими процессами. После завершения процесс удаляется из очереди процессов и затем вызывается его деструктор, освобождая занятую процессом память. В данном примере процесс создается явно и динамически - оператором new, а уничтожается неявно (диспетчером процессов) после завершения тела процесса. Каждый элемент массива процессов порождается от базового класса ТПРОЦЕСС и кроме обязательной функции тело_процесса() имеет конструктор, аргументом которого является индекс процесса в массиве.Разработке библиотеки предшествовала работа, связанная с реализацией программных моделей многопроцессорных систем и процессора, микропрограммно поддерживающего конструкции параллельного программирования. Эти попытки привели к разработке и реализации экспериментального языка параллельного программирования 2K и компилятора для него. В компиляторе было использовано гибкое решение проблемы кодогенерации на основе синтеза программной модели процессора, обладающего желаемой виртуальной системой команд - это позволило довольно оперативно вносить изменения в язык, компилятор и экспериментировать с различными конструкциями и алгоритмами параллельного программирования. В версии 2.
Введение
Библиотека 2KLIB разрабатывалась как средство программирования систем реального времени, представляет собой параллельное расширение языка Си и дает программисту набор понятий-классов, позволяющих строить программу в терминах параллельных процессов и их взаимодействий. Программы, написанные с использованием библиотеки, можно транслировать компилятором Borland C 3.1 и выполнять на машинах IBM PC под управлением операционной системы MSDOS. Библиотека позволяет в рамках DOS-программы запускать квазипараллельные процессы, разделяющие время процессора. Фактически можно считать, что библиотека предоставляет DOS-программе надстроечную многозадачную операционную среду. Основное понятие библиотеки и базовая единица работы - процесс. Процессы выполняются параллельно и могут взаимодействовать друг с другом, обмениваясь сообщениями. При взаимодействии происходит их синхронизация. Взаимодействие процессов может выполняться через механизм программных каналов, рандеву или посредством разделяемых переменных. Поскольку библиотека создавалась в основном для задач управления аппаратурой систем реального времени, большое внимание уделялось методологии программного определения аппаратуры. В терминах параллельного объектно-ориентированного подхода аппаратные устройства описываются как процессы-классы, наследующие свойства параллельного процесса и взаимодействующие с другими процессами. Под устройствами также понимается все многообразие логических устройств, непосредственно управляющих реальными физическими устройствами. С программной точки зрения устройства можно рассматривать как данные некоторого более или менее сложного типа.
Основная задача управляющих систем реального времени - сбор и обработка данных от физических устройств и управление физическими устройствами. Введение понятия параллельного процесса отражает тот факт, что физические устройства работают в реальном времени параллельно друг с другом и для естественного, адекватного описания их поведения, программные компоненты также должны работать параллельно (по крайней мере, с точки зрения программиста). Использование для этой цели последовательных языков ведет к неадекватному описанию, так как на последовательном языке приходится в рамках одной последовательности увязывать действия устройств в некоторую неестественную логическую временную цепь, хотя сами устройства такими логическими связями не обладают. Поэтому построение программ реального времени на основе параллельных процессов - это такое же мощное средство структуризации, как и введение процедур и функций в последовательных программах. С более общей точки зрения можно считать понятие параллельного процесса развитием понятий процедуры, функции, класса и рассматривать параллельное программирование не только как специфический тип программирования систем реального времени, но и как достаточно общую методологию разработки программного обеспечения.
Программа 1
Начнем с примера простой программы, которая по уже установившейся традиции для языка Си, выводит на экран дисплея текст приветствия и завершается. При описании мы предполагаем, что читатель достаточно хорошо знаком с языком Си и основами объектно-ориентированного программирования, поэтому будем рассматривать только те особенности программы, которые связаны с параллельным программированием.
#include
#include "2k.h"
VOIDТСЦЕНАРИЙ::тело_процесса(void)
{ cout<< "Здравствуй, мир
";
}
В отличие от обычных программ на Си, начинающих свое выполнение с главной функции main(), программы с использование библиотеки 2KLIB начинаются с выполнения главного процесса (создаваемого исполняющим ядром библиотеки). Главный процесс имеет тип ТСЦЕНАРИЙ. ТСЦЕНАРИЙ - это класс, порождаемый от базового абстрактного класса ТПРОЦЕСС. Для каждого порождаемого от Т Процесс класса необходимо переопределить чистую виртуальную функцию тело_процесса(), которая собственно и содержит исполняемый код. В нашем случае в теле процесса выполняется вывод строки в стандартный файл вывода, после чего программа завершается. Определения библиотеки 2KLIB подключаются к программе через файл 2k.h. В дальнейших примерах мы будем, для краткости, опускать указание включаемых файлов.
Примечание: библиотека создавалась с использованием разработанного автором препроцессора русской лексики для языка Си. Препроцессор позволяет использовать в Си-программах русские идентификаторы.
Программа 2 class Т :PUBLICТПРОЦЕСС
{ public: voidтело_процесса(void)
{ cout<< "Параллельный процесс
";
}
};
VOIDТСЦЕНАРИЙ::тело_процесса(void)
{ new Т;
cout<< "Сценарий
";
}
В этой программе в теле процесса-сценария создается новый процесс, описываемый классом Т, после чего оба процесса существуют одновременно. Выполнение программы завершится после того, как завершатся оба процесса. В ходе выполнения в стандартный вывод будут направлены строки: "Сценарий" и "Параллельный процесс". Класс Т порождается от базового класса ТПРОЦЕСС, наследуя его доступные члены. В классе Т не вводится никаких новых свойств процесса, а только определяется его тело - это самый тривиальный случай определения нового типа процессов. Создается новый процесс оператором new. Это приводит к тому, что выделяется память под контекст процесса и процесс устанавливается в очередь процессов, готовых для выполнения. После завершения процесс удаляется из очереди процессов и затем вызывается его деструктор, освобождая занятую процессом память. В данном примере процесс создается явно и динамически - оператором new, а уничтожается неявно (диспетчером процессов) после завершения тела процесса. Родительский процесс не контролирует завершение дочерних процессов и, после их создания, выполняется независимо, поэтому родственных связей между процессами нет. При создании каждому процессу назначается собственная область стека, размер которой можно указать как аргумент конструктора ТПРОЦЕСС. По умолчанию размер стека равен 1 КБАЙТ. Вторым (необязательным) параметром конструктора ТПРОЦЕСС является строка имени процесса - она используется только для диагностики ошибок.
Программа 3 class Т : PUBLICТПРОЦЕСС
{ private: intиндекс;
public: Т(inta_индекс)
{ индекс = а_индекс;
} voidтело_процесса(void)
{ cout<< "Параллельный процесс " << индекс << "
";
}
};
VOIDТСЦЕНАРИЙ::тело_процесса(void)
{ for (inti = 1; i<= 300; i)
NEWT(i);
} программа приветствие массив параллельный
В этой программе процесс-сценарий создает массив из трехсот параллельных процессов, причем каждый процесс получает при своем создании уникальный индекс. Каждый элемент массива процессов порождается от базового класса ТПРОЦЕСС и кроме обязательной функции тело_процесса() имеет конструктор, аргументом которого является индекс процесса в массиве.
Программа 4 class Т :PUBLICТПРОЦЕСС
{ public: voidтело_процесса(void);
~Т()
{ cout<< "Уничтожение процесса Т
";
}
};
void Т::тело_процесса(void)
{ for (;;)
{ cout<< "Параллельный процесс
";
пауза(100);
}
}
VOIDТСЦЕНАРИЙ::тело_процесса(void)
{
Т* процесс = new Т;
пауза(1000);
delete процесс;
}
В этой программе процесс-сценарий создает параллельный процесс Т, приостанавливается на 1 секунду и затем уничтожает процесс Т. Программа демонстрирует явное динамическое завершение процесса - такое действие используется при естественном завершении циклических процессов или в случае аварийной ситуации. Определение процесса Т имеет деструктор, вызываемый при завершении процесса.
Деструктор процесса используется для выполнения завершающих операций, например, закрытия файла, что гарантирует целостность данных, изменяемых в теле процесса. В случае управления аппаратными устройствами, конструктор должен выполнять все инициализирующие действия (например, начальный сброс), а деструктор - все завершающие действия (например, выключение привода).
Для приостановки текущего (активного) процесса используется глобальная функция пауза(), аргумент которой - время приостановки в миллисекундах. Семантика этой функции состоит в удалении процесса из очереди активных процессов (очередь диспетчера) и перемещении его в очередь ожидания к таймеру. После того, как пройдет заданный интервал времени, процесс будет возвращен в очередь активных процессов и его выполнение будет продолжено. Когда процесс не находится в очереди диспетчера, он выполняет пассивное ожидание, не занимая процессорного времени и предоставляя его другим активным процессам.
Здесь следует отметить, что алгоритм диспетчеризации процессов таков, что процесс выполняется до тех пор, пока самостоятельно не перейдет в состояние ожидания некоторого события (в данном примере - ожидания конца секундного интервала времени). Существуют два принципиально различных подхода к диспетчеризации процессов - первый основан на внутреннем поведении процесса, а второй - на принудительном внешнем ограничении выполнения процесса по времени (квантование времени) или по внешнему событию, например, появлению более приоритетного процесса (алгоритм с вытеснением). Выбор способа диспетчеризации многофакторный, определяется типами процессов (часто весьма разнокачественными) и вряд ли можно сделать совершенно однозначное предпочтение. Мы выбрали первый способ исходя из следующих причин: опыт работы с различными алгоритмами диспетчеризации на раз личных задачах, эффективность и простота реализации наиболее сложной проблемы: взаимного исключения, необходимого для выполнения неделимых (атомарных) операций, связанных с синхронизацией параллельных процессов, нереентерабельность MSDOS.
Таким образом, активный процесс выполняется до тех пор, пока самостоятельно не перейдет к ожиданию некоторого события. В библиотеке определен ряд операций, вызывающих ожидание различных событий. Наиболее простая - пауза(). Эта перегруженная функция может вызываться с одним аргументом - временем ожидания или без аргументов - в этом случае процесс не удаляется из очереди активных процессов, а остается в ней, но управление передается следующему готовому процессу.
Учитывая принятый способ диспетчеризации параллельных процессов было решено также отказаться от понятия явного приоритета процесса. Это довольно рискованное решение, особенно для систем реального времени, но оно было принято изза весьма тяжелой реализации явной системы приоритетов, которая, в частности, требует перехода от простых и эффективных очередей типа FIFO (первым пришел - первым обслуживается) к приоритетным очередям, причем как для процессов, так и для межпроцессных сообщений. Вместо явной системы приоритетов вводится понятие неявной системы приоритетов, причем приоритетность закладывается на этапе программирования. Идея состоит в том, чтобы рассматривать приоритет как относительную частоту активизации процесса - приоритет тем выше, чем большее время процесс находится в активном состоянии. Недостатком такого решения является трудность обеспечения быстрой реакции на особо приоритетные процессы, хотя эту трудность можно обойти для наиболее важного случая - активизации процесса по аппаратному прерыванию - в этом случае обработка разделяется на две фазы - быстрое предварительное реагирование в драйвере обслуживания прерывания и последующая дообработка в теле процесса, активизируемого сообщением. Продемонстрируем введение неявных приоритетов на примерах: // цикл, расположенный в теле высокоприоритетного процесса for (i = 0; i < 1000; i)
{ некоторое_действие();
}
// цикл, расположенный в теле процесса со средним приоритетом for (i = 0; i < 1000; i)
{ некоторое_действие();
пауза();
}
// цикл, расположенный в теле низкоприоритетного процесса for (i = 0; i < 1000; i)
{ некоторое_действие();
пауза(приоритет);
}
В первом примере процесс активен все время, пока выполняется цикл. Во втором примере процесс после каждой итерации отдает процессорное время другим параллельным процессам не уходя при этом из очереди готовых процессов. В третьем примере процесс после каждой итерации уходит в ожидание на время, зависящее от значения переменной "приоритет". Изменяя значение этой переменной процесс может изменять частоту активизации - это соответствует понятию динамического приоритета. Отметим, что если переменную "приоритет" сделать доступной для других процессов, то приоритетом данного процесса можно управлять внешним образом, исходя из некоторого глобального критерия.
Так как динамический процесс может быть завершен в любой момент времени, при выполнении операций с ним необходимо быть уверенным в том, что процесс еще существует. Для реализации такого контроля каждый процесс при создании снабжается уникальной меткой которую можно получить функцией "идентификатор()". Контроль процесса выполняет глобальная логическая функция "существует()", принимающая два параметра - указатель процесса и его идентификатор. Эта функция проверяет целостность внутренней структуры процесса и соответствие его идентификатора. Например: Т* процесс = new Т;
intидентификатор_Т = процесс->идентификатор();
if (существует(процесс, идентификатор_Т)) действие_с_процессом... else восстановительные_действия...
Исполняющее ядро библиотеки всегда контролирует целостность процессов и при попытке доступа к несуществующему процессу аварийно завершает программу с выдачей диагностического сообщения. Естественно, что для систем реального времени такое поведение программы будет недопустимым. Практика надежного программирования динамических процессов состоит в том, что перед операциями доступа к процессу нужно проверять его существование и, в зависимости от существования процесса, выполнять то или иное действие, например, попытку восстановления.
Вывод
Библиотека параллельного программирования 2KLIB продолжает мои исследования, связанные с программированием систем реального времени. Разработке библиотеки предшествовала работа, связанная с реализацией программных моделей многопроцессорных систем и процессора, микропрограммно поддерживающего конструкции параллельного программирования. Эти попытки привели к разработке и реализации экспериментального языка параллельного программирования 2K и компилятора для него. В компиляторе было использовано гибкое решение проблемы кодогенерации на основе синтеза программной модели процессора, обладающего желаемой виртуальной системой команд - это позволило довольно оперативно вносить изменения в язык, компилятор и экспериментировать с различными конструкциями и алгоритмами параллельного программирования.
Реализация и практическая работа с первой версией библиотеки 2KLIB показала удачность выбранных решений. В версии 2.04 были сделаны следующие изменения: добавлено понятие статического процесса (первая версия поддерживала только динамические процессы);
введен библиотечный процесс клавиатуры;
каналы и рандеву реализованы как параметризованные классы, что позволило ввести сильную типизацию каналов и объектов рандеву (в первой версии каналы мог передавать сообщения любого типа, порожденного от базового);
конверт сообщения теперь создается неявно самим каналом (в первой версии его нужно было создавать явно);
синтаксис приема и передачи выполнен с использованием перегруженных операций > (в первой версии это делалось вызовом функций);
более эффективно реализован таймаут;
добавлен таймаут для рандеву;
добавлена идентификация процессов и проверка их существования;
улучшена диагностика и обработка ошибок.
Дополнительно к библиотеке 2KLIB была разработана библиотека классов 2KWIN, предоставляющая основные возможности по созданию программ с текстовым многооконным интерфейсом: окна, блоки диалогов, меню, гипертекстовая help-система. Библиотека 2KWIN построена на той же технологической основе, что и библиотека 2KLIB, позволяет параллельным процессам выполняться в различных окнах и организовывать интерактивное взаимодействие с пользователем используя процессы клавиатуры и мыши с передачей сообщений по каналам.
Библиотеки 2KLIB и 2KWIN были применены в двух практических разработках - системе автоматизации производства счетчиков электрической энергии и системе управления аппаратурой кукольного театра 2 КУ. Аппаратура обоих систем построена по магистрально-модульному принципу и соединяется с компьютером через параллельный (принтерный) порт.
В настоящее время разрабатывается версия библиотеки 2KLIB для Windows, что позволит применить ее для решения задач моделирования сложных динамических систем, требующих больших объемов памяти и развитых средств графической визуализации. Задачи этого типа, как и задачи реального времени, хорошо поддаются декомпозиции на взаимодействующие процессы.