Содержание

Советы по программированию для встраиваемых систем

Реентерабельность, атомарные переменные и рекурсия

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

Аккуратно распределяя ресурсы задач, мы можем использовать реентерабельные функции, допускающие множественные конкурентные вызовы без порчи данных. Реентерабельность была "придумана" для мейнфреймов, в дни когда память была драгоценным ресурсом, а операторы машин заметили, что часто в системной памяти оказываются абсолютно одинаковые программы, выполняемые разными пользователями. До сих пор захватывает дух от этой идеи: представьте память с объемом в 32 килослова. Если в этой памяти разместить реентерабельную функцию, то даже для 50 пользователей будет использоваться те же 32 килослова. Каждый из пользователей выполняет тот же самый код, находящийся по одному адресу, однако используя свои собственные данные. Операционная система переключая контекст меняет только адреса данных и информация одного пользователя не портится другим. Общая программа, но разные данные.

В мире встраиваемых систем реентерабельная функция должна удовлетворять следующим правилам:

Атомарные переменные

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

mov ax, bx

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

Первая часть Правила 1 требует атомарного использования глобальных переменных. Пусть две функции используют общую глобальную переменную foobar. Если функция A содержит код

temp = foobar;
temp += 1;
foobar = temp;

то она не является реентерабельной, так как доступ к переменной foobar не является атомарным, потому что для изменения foobar используется три действия а не одно. Этот код может прервать прерывание, в котором вызывается функция B, которая тоже выполняет какие-то действия с foobar. После возврата из прерывания значение temp может не соответствовать актуальному значению foobar.

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

foobar += 1;

Теперь операция атомарная, прерывание не остановит операцию над foobar посередине, конфликт не возникнет, значит эта операция реентерабельная.

Стойте… А вы действительно знаете, что ваш Си компилятор сгенерирует для этой операции? На x86 процессоре этот код может выглядить следущим образом:

mov ax, [foobar]
inc ax
mov [foobar], ax

что конечно же не является атомарной опрацией, а значит выражение foobar += 1; не является реентерабельным. Атомарная версия будет выглядеть так:

inc [foobar]

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

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

Рассмотрим следующий код:

int foo;
void some_function(void)
{
    foo++;
}

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

Си и Си++ могут оградить нас от этой опасности. Используем локальную переменную. Т.е. определим foo внутри функции. В этом случае каждый вызов some_function() будет использовать переменную, выделенную в стеке (что, конечно, не относится к PIC16 и PIC18. С последним правда можно использовать компилятор Microchip C18, который может реализовать программный стек и, соответственно, реентерабельность. Но нужна ли она там?):

void some_function(void)
{
    int foo;
    foo++;
}

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

Правило 2 говорит о том, что вызывающая функция наследует отсутствие реентерабельности в вызываемой. То есть если данные портятся в функции B которая была вызвана из функции A, говорить о реентерабельности А бессмысленно.

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

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

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

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

Добиваемся реентерабельности

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

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

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

Самый распространенный способ - запрещать прерывания на время выполнения нереентерабельного кода. Если прерывания запрещены, система из многозадачной превращается в однозадачную. На самом деле это не совсем так - кто мешает после запрещения прерывания вызвать сервис RTOS переключающий контекст? Запретите прерывания, выполните нереентерабельную часть кода, разрешите прерывания. Довольно часто это выглядит следующим образом:

long i;
void do_something(void)
{
    disable_interrupts();
    i += 0x1234;
    enable_interrupts();
}

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

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

Гораздо лучше выглядит следующая функция:

long i;
void do_something(void)
{
    push_interrupt_state();
    disable_interrupts();
    i+=0x1234;
    pop_interrupt_state();
}

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

Почти все коммерческие RTOS имеют объект синхронизации типа "семафор" (часто семафор, реализующий защиту от одновременного доступа называют мютексом; мютекс имеет дополнительные свойства). Если вы используете RTOS то семафоры - это ваш путь обеспечить реентерабельность. Не используете RTOS? Часто я вижу программы, использующие переменную-флаг для защиты ресурса:

while (in_use);           //wait till resource free
in_use = TRUE;            //set resource busy
Do_non_reentrant_stuff();
in_use = FALSE;           //set resource available

Выгляди просто и элегантно, но этот код опасен!

источник FIXME