Атомарный доступ к переменным |
![]() |
Добавил(а) microsin |
При отладке встраиваемых приложений, наиболее сложно отловить ошибки, проявляющие себя не постоянно, а лишь время от времени. Одна из причин подобных багов: переменные, доступ к которым осуществляется асинхронно. Такие переменные должны быть правильно определены, и иметь соответствующую защиту. Определение должно включать ключевое слово volatile. Оно информирует компилятор, о том, что переменная может быть изменена не только из текущего выполняемого кода, но и из других мест. Тогда компилятор будет избегать определенных оптимизаций этой переменной. Чтобы защитить общую переменную, каждая операция доступа к ней должна быть атомарной. То есть не должна прерываться до своего окончания. Например, доступ к 32 или 16 разрядной переменной на 8-ми разрядной архитектуре не атомарный, поскольку операции чтения или записи требуют больше одной инструкции. Рассмотрим типичный пример общедоступной переменной – программный таймер. В обработчике прерывания его значение изменяется, а в основном коде - считывается. Если в обработчике другие прерывания запрещены, как, например, по дефолту сделано в микроконтроллерах AVR, то операция изменения переменной атомарна и никаких косяков не случится. volatile unsigned long system_timer = 0; #pragma vector = TIMER0_COMP_vect __interrupt void Timer0CompVect(void) { system_timer++; } С другой стороны в основном цикле программы прерывания чаще всего разрешены, и вариант небезопасного кода мог бы выглядеть так: if (system_timer >= next_cycle) { next_cycle += 100; do_something(); } Этот код небезопасен, потому что операция чтение переменной system_timer не атомарна. В то время как мы читаем один из байтов переменной system_timer, может возникнуть прерывание TIMER0_COMP и обработчик изменит ее значение. Тогда, по возвращению в основную программу, мы прочтем оставшуюся часть переменной уже от ее нового значения. В ряде случаев микс из старого и нового значения не вызовет сбоев, но в других может сильно повлиять на поведение программы. Ну, например, если старое значение system_timer было 0x00ffffff, а новое 0x01000000. Чтобы защитить доступ к переменной system_timer, можно использовать мониторную функцию, для этого перед именем функции указывается ключевое слово __monitor. __monitor unsigned long get_system_timer(void) { return system_timer; } ... if (get_system_timer() >= next_cycle) { next_cycle += 100; do_something();
} Мониторная функция – это функция, которая при входе сохраняет регистр SREG, запрещает прерывания на время своего выполнения, а перед выходом восстанавливает содержимое SREG. Если требуется, чтобы прерывания запрещались в каком-то конкретном участке кода, можно использовать intrinsic функции. #include < intrinsics.h > … unsigned long tmp; unsigned char oldState; oldState = __save_interrupt();//сохраняем регистр SREG __disable_interrupt(); //запрещаем прерывания tmp = system_timer; //считываем значение system_timer // во временную переменную __restore_interrupt(oldState);//восстанавливаем SREG if (tmp >= next_cycle) { next_cycle += 100; do_something(); } Средства Си++ позволяют встроить эту логику в класс. #include < intrinsics.h > class Mutex { public: Mutex () { current_state = __save_interrupt(); __disable_interrupt(); } ~Mutex () { __restore_interrupt(current_state); } private: unsigned char current_state; }; … unsigned long tmp; { Mutex m; //создаем объект класса, теперь доступ будет атомарным tmp = system_timer; //сохраняем system_timer во временной переменной } if (tmp >= next_cycle) { next_cycle += 100; do_something(); } При создании объекта m конструктор сохранит регистр SREG, и запретит прерывания. По окончанию блока – деструктор восстановит содержимое SREG. Красиво, да? В общем случае принцип написания кода с атомарными операциями везде один, а вариантов реализации много. Можно, например, при доступе к переменной запрещать не все прерывания, а только те, в которых используется эта переменная. Проблема возможна и с восьмибитной переменной. Операция вроде system_timer -= 100 компилируется в несколько ассемблерных инструкций и в основном коде также может быть прервана между чтением system_timer и записью результата. Есть еще один способ чтения многобайтовых асинхронных счетчиков (без запрета прерываний) - считать переменную два раза и сравнить все байты, кроме младшего. Если байты в копиях равны - берем последнее считанное значение, если не равны - считываем до тех пор, пока в двух последних считанных значениях байты не будут равны. Младший байт счетчика между чтениями может успеть измениться без переноса, поэтому он в проверке не участвует. Как видно из примеров, код приведен для компилятора IAR. В WinAVR подобная проблема решается включением файла atomic.h, в котором определены макросы для реализации атомарного доступа. Например так: #include < util/atomic.h > ... ATOMIC_BLOCK(AT OMIC_RESTORESTA TE) { // блок кода с запрещенными прерываниями } ... [Ссылки] 1. Атомарный доступ к переменным site:chipenable.ru - оригинал статьи. |