Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

2830.Встроенные микропроцессорные системы

..pdf
Скачиваний:
41
Добавлен:
15.11.2022
Размер:
52.65 Mб
Скачать

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

2.5.2.4. Модели непротиворечивости памяти

Еще одна весьма тонкая проблема потоков связана с моделью памяти программы. Некоторые отдельные реализации потоков исходят из определенной модели непротиворечивости (консистентности) памяти, определяющей, как переменные, которые читаются и записываются разными потоками, представляются этим потокам. Интуитивно подразумевается, что чтение переменной должно иметь своим результатом последнее, записанное в переменную значение, но что означает «последняя»? Рассмотрим, например, сценарий, где все переменные инициализируются значением 0 и поток A выполняет два оператора:

x = 1; w = y;

тогда как поток B выполняет следующие два оператора:

y = 1; z = x;

Интуитивно получаем, что после выполнения потоками этих операций мы должны надеяться, что хотя бы одна из двух переменных w и z имеет значение 1. Такая гарантия относится к последовательной непротиворечивости. Это означает, что результат любого выполнения точно такой же, как если бы операции всех потоков всегда выполнялись в некотором порядке и операции каждого индивидуального потока всегда появлялись в этой последовательности в порядке, определенном в потоке.

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

151

так как не существует зависимости между ними (так это может казаться компилятору). Даже если компилятор не переупорядочивает команды, это может сделать аппаратура. Поэтому для доступа к разделяемым переменным остается лишь использовать взаимоисключающие замки и надеяться, что они реализованы корректно. О проблемах непротиворечивости памяти можно посмотреть в работах [28], [29].

2.5.2.5. Проблемы с потоками

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

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

void update(int newx) { x = newx;

// копирование списка в headc и tailc pthread_mutex_lock(&lock); element_t* headc = NULL; element_t* tailc = NULL;

element_t* element = head; while (element != 0) {

if (headc == NULL) {

headc = malloc(sizeof(element_t)); headc->listener = head->listener; headc->next = 0;

tailc = headc;

}else {

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

tailc->listener = element->listener; tailc->next = 0;

}

element = element->next;

}

pthread_mutex_unlock(&lock);

152

//извещение пользователей с помощью копии списка element = headc;

while (element != 0) { (*(element->listener))(newx); element = element->next;

}

}

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

Этот код, однако, имеет потенциально серьезную проблему, которая не может быть обнаружена тестированием. Предположим, что поток A вызывает update с аргументом newx = 0, индицирующим, что «все системы в норме». Предположим, что поток A отложен сразу после освобождения замка, но перед выполнением оповещения. Предположим, что, пока он отложен, поток B вызывает update с аргументом newx = 1, означающим «авария! двигатель в огне!». Предположим, что этот вызов update завершается перед тем, как поток A получает шанс на выполнение. Когда поток A получает разрешение на выполнение, он будет оповещать всех слушателей, но неправильным значением! Если один из слушателей обновляет дисплей пилота самолета, дисплей будет показывать, что все нормально, когда фактически двигатель объят пламенем. Нетривиальные многопоточные программы очень трудно понимать. Они могут содержать серьезные ошибки, состязания и взаимоблокировку. Проблемы многопоточных программ могут оставаться незамеченными годы, даже при интенсивном использовании программ. Эти проблемы очень важны для встроенных систем, так как оказывают влияние на безопасность и средства существования. Фактически каждая встроенная система включает параллельное программное обеспечение, потому инженеры, проектирующие встроенные системы, должны противостоять всевозможным ловушкам.

2.5.3. Процессы и передача сообщений

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

153

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

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

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

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

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

Пример простой программы передачи сообщений приведен на рис. 82. Эта программа использует шаблон producer/consumer, в котором один поток генерирует последовательность сообщений, а второй – их потребляет. Такой шаблон может быть использован для реализации рассмотренного ранее шаблона observer без риска взаимной блокиров-

154

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

1 void* producer(void* arg) {

2int i;

3for (i = 0; i < 10; i++) {

4send(i);

5}

6return NULL;

7}

8void* consumer(void* arg) {

9while(1) {

10printf("received %d\n", get());

11}

12return NULL;

13}

14int main(void) {

15pthread_t threadID1, threadID2;

16void* exitStatus;

17pthread_create(&threadID1, NULL, producer, NULL);

18pthread_create(&threadID2, NULL, consumer, NULL);

19pthread_join(threadID1, &exitStatus);

20pthread_join(threadID2, &exitStatus);

21return 0;

22}

Рис. 82. Пример простой программы передачи сообщений

Функция producer (стартовая функция потока-отправителя) в строке 4 вызывает send (должна быть определена) для отправки сообщения

ввиде целого значения. Функция consumer обеспечивается тем, что get не возвратится, пока она реально не получит сообщение. Заметим, что

вэтом случае consumer никогда не вернется. Эта программа не завершается собственными средствами.

Реализация send и get с использованием Pthreads приведена на рис. 83. Реализация использует связанный список, подобный рис. 78, но нагрузка является целой величиной. Связанный список реализован как неограниченная очередь с дисциплиной FIFO (first-in, first-out), когда новый элемент помещается в tail (хвост), а старые элементы удаляются из head (головная часть).

Рассмотрим первой реализацию send. Она использует мутекс для того, чтобы send и get одновременно не модифицировали связанный список. В дополнение она использует переменную условия для взаимодействия с процессом consumer, который изменяет размер очереди.

155

Переменная условия sent объявляется и инициализируется в строке 7.

В строке 23 поток producer вызывает функцию pthread_cond_signal, ко-

торая «пробуждает» другой поток, блокированный переменной условия, если такой поток существует.

1 #include <pthread.h>

2 struct element {int payload; struct element* next;};

3 typedef struct element element_t; 4 element_t *head = 0, *tail = 0;

5 int size = 0;

6pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;

7pthread_cond_t sent = PTHREAD_COND_INITIALIZER;

8

9void send(int message) {

10pthread_mutex_lock(&mutex);

11if (head == 0) {

12head = malloc(sizeof(element_t));

13head->payload = message;

14head->next = 0;

15tail = head;

16} else {

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

18tail = tail->next;

19tail->payload = message;

20tail->next = 0;

21}

22size++;

23pthread_cond_signal(&sent);

24pthread_mutex_unlock(&mutex);

25}

26int get(void) {

27element_t* element;

28int result;

29pthread_mutex_lock(&mutex);

30while (size == 0) {

31pthread_cond_wait(&sent, &mutex);

32}

33result = head->payload;

34element = head;

35head = head->next;

36free(element);

37size--;

38pthread_mutex_unlock(&mutex);

39return result;

40}

Рис. 83. Функции send и get для передачи сообщений

156

Чтобы увидеть, что означает «пробуждает» другого потока, посмотрим на функцию get. В строке 31, если поток, вызывающий get, обнаружил, что размер очереди равен 0, он вызывает pthread_cond_wait, который будет блокировать поток до тех пор, пока некоторый другой поток не вызовет pthread_cond_signal. (Существуют другие условия, которые заставят вернуться из pthread_cond_wait. Так код должен периодически ждать, пока не обнаружит отличный отнуля размер очереди.)

Критично то, что функции pthread_cond_signal и pthread_cond_wait

вызываются, пока владеют замком мутекса. Предположим, что строки 23 и 24 переставлены местами и pthread_cond_signal была вызвана после освобождения замка мутекса. В этом случае будет возможным вызов pthread_cond_signal, пока поток consumer приостановлен (но еще не заблокирован) между строками 30 и 31. В этом случае, когда поток consumer разрешен для работы, будет исполнена строка 31 и наступит блокировка ожидания сигнала. Но сигнал уже был послан, и он не может быть послан повторно, так что поток consumer будет постоянно блокироваться.

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

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

157

В 60-е гг. прошлого века Е. Дейкстра заимствовал эту идею, чтобы показать, как программы могут надежно разделять ресурсы. Вычислительный семафор (PV-семафор Дейкстра) – это неотрицательная целая (натуральная) переменная. Значение 0 рассматривается обособленно. Фактически переменная size из предыдущего примера является семафором. Она увеличивается при отправке сообщений, а величина 0 блокирует consumer, пока значение станет ненулевым. Переменная условия обобщает эту идею, поддерживая произвольные условия, а не только 0 или не 0 как критерий для блокировки. Более того, по крайней мере, в Pthreads переменные условия координируются с мутексами для облегчения написания программ.

Использование передачи сообщений в приложениях может быть проще, чем использование потоков разделяемых переменных, но даже и в этом случае существуют опасности. Реализация шаблона producer/consumer фактически имеет четкий дефект. В частности, не налагаются ограничения на размер очереди сообщений. В некоторый момент времени поток producer вызывает send, будет выделена память для запоминания сообщения, память не будет возвращена, пока сообщение не употребится. Если поток producer генерирует сообщения быстрее, чем consumer потребляет, то программа в конце концов исчерпает допустимую память. Это может быть зафиксировано ограничением размера буфера, но какой размер является приемлемым? Выбор маленького буфера может вызвать взаимоблокировку, большого – неэкономно. Эта проблема не имеет тривиального решения.

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

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

158

2.6. Интегрированная среда разработки прикладного программного обеспечения

2.6.1. Средства разработки программ для встроенных систем на Си

Существуют как платные, так и бесплатные средства разработки программ для встроенных систем на Си. Мощное средство разработки бесплатно предоставляет компания Microsoft на своем интернет-сайте [30]. Однако это средство довольно сложное и требует установки на компьютер с операционной системой Windows громоздких средств разработки Visual Studio (несколько гигабайт) и MSDN объемом более 3 ГБ, а также Windows Mobile SDK (около двухсотен мегабайт) иэмуляторов.

Средство под названием Pelles C for Windows объемом около 8 МБ свободно доступно на сайте его разработчиков [31]. Данное средство разработки позволяет создавать разнообразные программы как под Windows CE (Windows Mobile), так для обычных персональных компьютеров под Windows. Данный пакет разработки включает в себя интерфейс среды разработки IDE, компилятор языка программирования Си, заголовочные файлы и библиотеки под разные платформы, набор полезных утилит, примеры кода и встроенный справочник среды разработки и стандарта языка программирования ISO C99 [32].

Проекты. В Pelles C проект включает в себя исходные файлы и команды для построения ординарного файла. Существуют различные типы проектов для исполнимой программы (EXE), динамические библиотеки (DLL), статические библиотеки (LIB), так же как и разные ОС Windows CE и Windows. Только один проект может быть активным в каждый момент времени, но несколько проектов могут быть загружены в рабочую область (workspace). Когда проект загружен в IDE, он показывается в окне проекта (рroject window). Окно проекта может показывать как исходные файлы (режим source view), так и выходные файлы (режим arget view), которые создаются во время сборки проекта.

Проект сохраняется в файле проекта (.ppj), дополнительные установочные параметры – в файле расширения проекта (.ppx), а рабочая область – в файле рабочей области (.ppw). Все они являются текстовыми файлами. Файл проекта в действительности является MAKEFILE со специальными макросами для IDE.

Редактор исходных текстов используется для редактирования некоторых текстовых файлов, например исходного Си-файла.

159

Редактор ресурсов используется для того, чтобы визуально редактировать скрипты ресурсов (.rc). Как и другие редакторы IDE, он имеет собственное меню.

Редактор изображений используется для визуального редактирования курсоров, пиктограмм и растровых отображений графических объектов (ресурсы CURSOR, ICON и BITMAP).

Отладчик используется для отладки программ. Программа может содержать отладочную информацию в формате CodeView или COFF.

Программа POASM (Pelles macro assembler – макроассемблер Pelles) создает объектный файл (OBJ) в формате COFF (common object file format) из исходного ассемблерного файла (ASM).

Программа POLINK (редактор связей Pelles) создает исполняемые программы (EXE) или динамические библиотеки (DLL) из файлов библиотек и объектных файлов формата COFF.

2.6.2. Комплект программ Telelogic Tau SDL Suite

Telelogic Tau SDL Suite [33] является инструментом разработки и реализации программного обеспечения реального времени на SDL. SDL Suite состоит из графического редактора и синтаксического анализатора, симулятора и валидатора и нескольких оптимизирующих генераторов кода для компиляции исполнительного кода. На рис. 84 приведена структура SDL Suite.

Рис. 84. Структура SDL Suite

Organizer (органайзер) является инструментом, который помогает разработчику при работе с SDL- и MSC-диаграммами. Органайзер может манипулировать текстовыми файлами, объектными моделями и Си-файлами (рис. 85). Также органайзер координирует работу других инструментов SDL Suite.

160