Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Учеб Пособ_Гончаровский.doc
Скачиваний:
1316
Добавлен:
29.03.2015
Размер:
3.65 Mб
Скачать

2.5. Многозадачность.

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

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

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

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

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

2.5.1. Язык программирования Си

Программы с языка проектирования автоматически или полуавтоматически транслируются сначала в программы на языке программирования Си, а затем компилируются в объектный код. Этот процесс называют генерацией кода. Часть разработчиков встроенных систем предпочитаюют писать проекты сразу на Си и даже для «голого железа» (без использования операционных систем).

Языки программирования, которые выражают вычисления как последовательность операций, называют императивными. Язык Си является таким языком.

Рассмотрим модель памяти в Си. Си-программы сохраняют данные в стеке, «куче» (heap) и фиксированных ячейках памяти, назначаемых компилятором. Рассмотрим пример Си-программы:

1 int a = 2;

2 void foo(int b, int* c) {

3 ...

4 }

5 int main(void) {

6 int d;

7 int* e;

8 d = …; // присвоение некоторой величины d.

9 e = malloc(sizeInBytes); // выделение памяти для e.

10 *e = …; // присвоение некоторой величины e.

11 foo(d, e);

12 …

13 }

Переменная ‘a’ является глобальной переменной, т.к. объявлена вне определений функций. Компилятор назначит ее на определенное место в памяти. Переменные ‘b’ и ‘c’ являются параметрами. Им выделяется место в стеке, когда вызывается функция foo (компилятор может также назначить их на регистры). Переменные ‘d’ и ‘e’ локальные переменные. Они объявляются внутри тела функции (в примере в main). Компилятор зарезервирует для них место в стеке.

Когда в строке 11 вызывается функция foo, ячейка стека, назначенная для ‘b’, получает копию переменной ‘d’, установленной в строке 8. Это является примером передачи параметров в функцию. Данные передаваемые указателем ‘e’ наоборот запоминаются в памяти, выделенной под кучу и проходят через ссылку (указатель на ‘e’ проходит как величина). Адрес запоминается в ячейке стека для ‘c’. Если foo содержит оператор присвоения для *c, то после возврата из foo это значение может бать прочитано разименованием ‘e’.

Рассмотрим некоторые ключевые моменты Си на примере программы на рис. 78. Эта программа реализует часто используемый шаблон, называемый « observer» (наблюдатель). В этом шаблоне функция update изменяет величину переменной ‘х’. Наблюдатели (другие программы или части программы) будут оповещаться (notify) функций обратного вызова (callback) всякий раз, когда изменяется ‘х’. В программе используется связанный список – структура данных для хранения списка элементов, длина которого может изменяться во время выполнения программы. Каждый элемент списка содержит полезную нагрузку (значение элемента) и указатель на следующий элемент в списке (или нуль-указатель, если элемент последний).

typedef struct element element_t; // Тип элемента списка функций оповещения.

element_t* head = 0; // Указатель на начало списка.

element_t* tail = 0; // Указатель на конец списка.

void addListener(notifyProcedure* listener) { // Функция регистрации слушателей.

if (head == 0) {

head = malloc(sizeof(element_t));//Динамическое выделение памяти под пере

head->listener = listener; // менную типа element_t.

head->next = 0;

tail = head;

}

else {

tail->next = malloc(sizeof(element_t));

tail = tail->next;

tail->listener = listener;

tail->next = 0; }

}

void update(int newx) { // Функция обновления x

x = newx;

element_t* element = head;

while (element != 0) { // Оповещение всех зарегистрированных слушателей

(*(element->listener))(newx);

element = element->next;

}

}

void print(int arg) {// Пример callback-функции оповещения.

printf("%d ", arg);

}

Рис. 78. Пример программы-шаблона «observer» на Си

Д

1 typedef void notifyProcedure(int);

2 struct element {

3 notifyProcedure* listener;

4 struct element* next;

5 };

6 typedef struct element element_t;

7 element_t* head = 0;

8 element_t* tail = 0;

ля программы на рис. 78 структура данных связанного списка определяется так:

Первая строка декларирует, что notifyProcedure принадлежит к типу Си-функций с аргументом типа int, функция ничего не возвращает. Строки 2 – 5 декларируют struct (структура) – составной тип данных в С. Эта структура состоит из двух элементов: listener типа notifyProcedure* (указатель на функцию) и next – указатель на экземпляр этой же структуры. Строка 7 декларирует, что element_t является типом, относящимся к экземпляру структуры element. Строка 7 декларирует указатель head на список element. head инициализируется значением 0, индицирующим пустой список. Функция addListener создает первый элемент списка, используя следующий код:

1 head = malloc(sizeof(element_t));

2 head->listener = listener;

3 head->next = 0;

4 tail = head;

В первой строке выделяется память из кучи с использованием стандартной С-функции malloc для сохранения списка element и запоминается в head указатель на element. heap – это структура данных, которая помогает сохранять сведения об областях памяти, используемыми приложениями. В строке 2 запоминается нагрузка, в строке 3 индицируется, что это последний элемент списка. В строке 4 устанавливается значение в tail – указатель на последний элемент списка. Когда список не пуст функция addListener будет использовать tail для добавления элемента к списку.

Программа main использует функции, определенные на рис. 78:

int main(void) {

addListener(&print);

addListener(&print);

update(1);

addListener(&print);

update(2);

return 0;

}

Она дважды регистрирует print как callback-функцию, затем выполняет update (устанавливает x = 1), снова регистрирует print и в завершении, снова выполняет update (устанавливает x = 2). Функция print просто выводит на дисплей текущую величину. В процессе работы программы на дисплее появится последовательность 1 1 2 2 2.

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

В программе на рис. 78 состояние памяти включает значение глобальной переменной ‘x’ и список элементов указываемых переменной head (другая глобальная переменная). Сам список представляется как связанный список, в котором каждый элемент является указателем (адресом) функции, которая вызывается при изменении ‘x’. Во время выполнения Си-программы в состояние памяти включается также стек содержащий локальные переменные. Используя EFSM можно смоделировать выполнение простой С-программы, предполагая, что программа имеет фиксированное и ограниченное число переменных. Переменные Си-программы будут переменными EFSM. Состояния EFSM соответствуют места в программе, а переходы – выполнению программы.

На рис. 79 приведена модель функции update из примера на рис. 78. Тип сигнала pure (строгий) означает, что в каждый момент либо сигнал отсутствует (нет события), либо присутствует (есть событие). Такой сигнал не переносит значений, а лишь свидетельствует о присутствии.

Рис. 79. Пример модели Си-программы «шаблон observer»

Автомат переходит из начального состояния Idle, когда вызывается функция update. Вызов сигнализирует о наличии входного аргумента типа int. Когда выполняется этот переход, newx (в стеке) будет присвоено значение аргумента и глобальная переменная ‘x’ получит обновление. После первого перехода EFSM попадает состояние 31 (соответствует оператору element_t* element = head; ).

Затем следует безусловный переход в состояние 32 (while (element != 0)) и устанавливается значение element. Из состояния 32 есть два варианта перехода. Если element = 0, то EFSM перейдет и состояние Idle с выходным значением return (возврат из функции), иначе переход в состояние 33. Переход из состояния 33 в 34 сопровождается вызовом функция listener с аргументом равным переменной newx из стека. Переход из 34 обратно в 32 выполняется после получения сигнала returnFromListener, индицирующего возврат из listener.

При построении модели подобной рис. 79, необходимо решить на каком уровне детализации остановится, решить какие действия считать

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