Безопасен ли следующий доступ к переменной "flag" между прерыванием и кодом пользователя?
Мы унаследовали проект, нацеленный на микроконтроллер Renesas RX231, на который я смотрел.
Этот uC имеет только одну инструкцию, которая блокирует шину для атомарности (XCHG).
Поскольку процессор является единственным компонентом, который осуществляет доступ к оперативной памяти (без использования DMA или DTC), для управления переменными в пользовательском коде, которые используются совместно с прерываниями, прерывания отключаются (в регистре слов состояния процессора) на время доступа, т.е.
disable_interrupts(); /* set_psw(get_psw() & ~(1 << 16)); */
/* access or modify shared variables */
enable_interrupts(); /* set_psw(get_psw() | (1 << 16)); */
Однако есть также "флаги", которые используются совместно без защиты, которые устанавливаются в прерываниях и опрашиваются в коде пользователя следующим образом:
volatile unsigned char event_request_message = 0;
unsigned char condition_sending_message = 0;
#pragma interrupt
void on_request_message()
{
...
event_request_message = 1; // mov.l #0x3df5, r14
// mov.b #1, [r14]
...
}
void user_code()
{
for(;;)
{
...
/* might be evaluated multiple times before transmit message is completed */
if(event_request_message && !condition_sending_message) // mov.l #0x3df5, r14
// movu.b [r14], r14
// cmp #0, r14
// beq.b 0xfff8e17b <user_code+185>
// mov.l #0x5990, r14
// movu.b [r14], r14
// cmp #0, r14
// bne.b 0xfff8e16f <user_code+173>
{
event_request_message = 0; // mov.l #0x3df5, r14
// mov.b #0, [r14]
condition_sending_message = 1; // mov.l #0x5990, r14
// mov.b #1, [r14]
/* transmit message */
...
}
...
}
}
Мое понимание отсутствия защиты (путем отключения прерываний в коде пользователя) в этом случае будет:
- Для чтения, установки или сброса "флажка" всегда используются две инструкции: одна для помещения адреса памяти в регистр и одна для чтения / установки / очистки.
- Адреса памяти всегда одинаковы, поэтому их можно исключить из рассмотрения
- Каждая операция чтения / установки / очистки является отдельной инструкцией, и поэтому доступ / манипулирование являются атомарными.
Вопрос в том, правильно ли мое понимание? Безопасны ли такие переменные "флаг" доступа и манипулирования в этом случае?
Или могут быть какие-либо возможные ошибки / ошибки?
- Предположим, что используемый компилятор и параметры компилятора всегда одинаковы.
- Предположим, что описанные операции являются единственным способом доступа / манипуляции с такими "флагами" (установка на 0 или 1, чтение (все показано в коде сборки)) (без сложения, умножения и т. Д.)
Что если нам нужно обновить компилятор или изменить параметры компилятора?
Могут ли такие простые операции привести к более чем "одной инструкции"?
Обоснование использования таких "флагов" без охраны слишком ограничивает количество отключаемых временных прерываний.
Глядя на логику кода, ожидаемое поведение таково, что вы можете запросить сообщение один или несколько раз, но получите только один ответ.
PS. Я попытался использовать следующие дополнительные теги: "cc-rx", "rxv2-инструкция-набор", "rx231".
1 ответ
В зависимости от ваших целей, например, пишете ли вы только для конкретной платформы или хотите обеспечить мобильность, вам нужно помнить несколько дополнительных вещей:
С включенными оптимизациями многие компиляторы будут с легкостью переупорядочивать доступ к изменчивым переменным с доступом к постоянным переменным, если конечный результат операции неотличим для однопотокового сценария. Это означает, что код такой:
int a = 0; volatile int b = 0; void interrupt_a(void) { a = b + 1; b = 0; // set b to zero when done }
может быть перекомпонован компилятором в:
load acc from [b] store 0 into [b] // set b to zero *before* updating a, to mess with you a bit add 1 to acc store acc into [a]
Чтобы предотвратить изменение порядка в оптимизирующем компиляторе, можно сделать обе переменные изменчивыми. (Или, если доступно, использовать C11
_Atomic
сmemory_order_release
магазины иmemory_order_acquire
загружает, чтобы упорядочить его относительно операций над неатомарными переменными.)Если вы используете многоядерный UC, он может переупорядочить операции с памятью, так что это не решит проблему, и реальное решение заключается в создании ограждения как для компилятора, так и для процессора, если вы заботитесь о наблюдателях на других ядра (или в MMIO даже на одноядерном uC). Инструкции аппаратного ограничения не нужны для одного ядра или одного потока, потому что даже неиспользуемый процессор выполнения видит, что его собственные операции происходят в программном порядке.
Опять же, если компилятор, который вы получили с набором инструментов для вашей конкретной встроенной системы, ничего не знает о заборах, то вполне вероятно, что он воздержится от подобных вещей. Так что вам нужно изучить документацию и проверить скомпилированную сборку.
Например, документы ARM утверждают, что процессору "разрешено" переупорядочивать инструкции, и программист должен позаботиться о добавлении барьеров памяти, но сразу после этого также говорится (в "подробностях реализации"), что процессоры Cortex M не переупорядочивают инструкции, Тем не менее, они по-прежнему настаивают на том, что надлежащие барьеры должны быть вставлены, поскольку это упростит перенос на более новую версию процессора.
В зависимости от длины конвейера, после выполнения запроса может потребоваться пара инструкций, пока прерывание не будет полностью включено или отключено. Опять же, вам нужно проверить документы для этого конкретного uC/ компилятора, но иногда после записи в регистр иногда требуется какой-то забор. Например, в ARM Cortex после отключения прерывания необходимо выполнить инструкции DSB и ISB, чтобы убедиться, что прерывание не будет введено в следующих нескольких инструкциях.
// you would have to do this on an ARM Cortex uC DisableIRQ(device_IRQn); // Disable certain interrupt by writing to NVIC_CLRENA DSB(); // data memory barrier ISB(); // instruction synchronization barrier // <-- this is where the interrupt is "really disabled"
Конечно, ваша библиотека может содержать все необходимые инструкции по забору, когда вы звоните
disable_interrupts();
или они могут вообще не понадобиться для этой архитектуры.Инкрементная операция (
x++
) не следует считать атомарным, даже он может "случайно" оказаться атомарным на определенном одноядерном процессоре. Как вы заметили, это не атомарно для вашего конкретного uC, и единственный способ гарантировать атомарность - отключить прерывания вокруг этой операции.
Итак, в конечном итоге вы должны убедиться, что вы прочитали документацию для этой платформы и поняли, что компилятор может и чего не может делать. То, что работает сегодня, может не сработать завтра, если компилятор решит переупорядочить инструкции после того, как вы добавили, казалось бы, незначительное изменение, тем более что условие гонки может быть недостаточно частым для его немедленного обнаружения.