Будет ли отключение прерываний защищать энергонезависимую переменную или может произойти переупорядочение? - PullRequest
0 голосов
/ 13 декабря 2018

Предположим, INTENABLE - это регистр микроконтроллера, который включает / отключает прерывания, и я объявил его где-то в моих библиотеках как переменную, расположенную по соответствующему адресу.my_var - это некоторая переменная, которая изменяется в пределах одного или нескольких прерываний, а также в пределах my_func.

В пределах my_func Я хотел бы выполнить некоторую операцию в my_var, которая читает и затем записывает (например, +=) атомарно (в том смысле, что оно должно произойти полностью после или до прерывания - прерывание не может произойти, пока оно происходит).

То, что я обычно имел бытогда что-то вроде этого:

int my_var = 0;

void my_interrupt_handler(void)
{
    // ...

    my_var += 3;

    // ... 
}

int my_func(void)
{
    // ...

    INTENABLE = 0;
    my_var += 5;
    INTENABLE = 1;

    // ...
}

Если я правильно понимаю вещи, если my_var был объявлен volatile, то my_var гарантированно будет "чисто" обновлено (то естьпрерывание не будет обновлять my_var в промежутке между чтением и записью my_func), поскольку стандарт C гарантирует, что обращения к энергозависимой памяти происходят по порядку.

Часть, о которой я хотел бы получить подтверждение, - это когдане объявлено volatile.Тогда компилятор не будет гарантировать, что обновление происходит с отключенными прерываниями, это правильно?

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


РЕДАКТИРОВАТЬ:

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

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

РЕДАКТИРОВАТЬ 2:

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

volatile int my_volatile_var;

int my_non_volatile_var;

void my_func(void)
{
    my_volatile_var = 1;
    my_non_volatile_var += 2;
    my_volatile_var = 0;
    my_non_volatile_var += 2;
}

Использование arm-none-eabi-gcc Версия 7.3.1 для компиляции с -O2 для Cortex-M0 (arm-none-eabi-gcc -O2 -mcpu=cortex-m0 -c example.c) Я получаю следующую сборку:

movs    r2, #1
movs    r1, #0
ldr     r3, [pc, #12]   ; (14 <my_func+0x14>)
str     r2, [r3, #0]
ldr     r2, [pc, #12]   ; (18 <my_func+0x18>)
str     r1, [r3, #0]
ldr     r3, [r2, #0]
adds    r3, #4
str     r3, [r2, #0]
bx      lr

Там, где вы можете ясно видеть, два my_non_volatile_var += 2 были объединены в одну инструкцию, которая происходит после обоих изменчивых обращений.Это означает, что GCC действительно меняет порядок при оптимизации (и я собираюсь пойти дальше и предположить, что это разрешено стандартом).

Ответы [ 3 ]

0 голосов
/ 13 декабря 2018

Здесь есть несколько моментов, вызывающих беспокойство.

Переупорядочение инструкций

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

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

Это означает, что если включение / отключение прерывания в вашем примере сводится к установке / очистке маски общего прерывания, каккакая-то форма встроенного макроса ассемблера, тогда компилятор не может переупорядочить его.Если это доступ к реестру аппаратного обеспечения, то он (будем надеяться) будет квалифицирован как volatile и также не может быть переупорядочен.

Это означает, что материал между инструкциями встроенного ассемблера / доступ к volatile защищен отпереупорядочение по отношению к встроенному ассемблеру / изменчивому доступу, но не по отношению к чему-либо еще.

Оптимизация удаленных переменных, общих с ISR / без видимых побочных эффектов

На это в основном отвечают здесь .В вашем конкретном примере my_var не имеет заметных побочных эффектов и может быть оптимизировано.То же самое происходит, если он изменен от прерывания.В этом и заключается большая опасность, так как встроенный доступ asm / volatile, связанный с доступом к энергонезависимым переменным, не имеет значения.

При использовании «спагетти глобалов» / дизайна внешних связей компилятор действительно может бытьзаблокирован от различных предположений при оптимизации.Я не совсем уверен, что бы здесь означала оптимизация gcc во время компоновки, но если вы скажете компоновщику не беспокоиться о спагетти-доступе к другим блокам перевода, то, действительно, я думаю, что могут произойти плохие вещи.Не из-за повторного заказа, а из-за общей оптимизации без побочных эффектов.Хотя, возможно, это меньше всего беспокоит вас, если вы выплеснули extern по всей программе.


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

0 голосов
/ 14 декабря 2018

C / C ++ volatile имеет очень узкий диапазон гарантированных применений: для непосредственного взаимодействия с внешним миром (обработчики сигналов, написанные на C / C ++, находятся "снаружи", когда они вызываются асинхронно);Вот почему доступ к изменчивым объектам определяется как наблюдаемые , так же как консольный ввод-вывод и выходное значение программы (возвращаемое значение main).

Один из способов увидеть этопредставьте себе, что любой изменчивый доступ фактически транслируется с помощью ввода-вывода на специальной консоли или терминале или паре устройств FIFO с именами Доступы и Значения , где:

  • энергозависимая запись x = v; к объекту x типа T преобразуется в запись в FIFO Доступ порядок записи, указанный как 4-uplet ("write", T, &x, v)
  • энергозависимое чтение (lvalueпреобразование в rvalue) x переводится в запись в Доступы 3-uplet ("read", T, &x) и ожидание значения в Значения .

Таким образом, volatile точно так же, как интерактивная консоль.

Хорошая спецификация volatile - это семантика ptrace (которую никто, кроме меня, не использует, но это все же самая хорошая спецификация volatile):

  • переменная может быть exaдобывается отладчиком / ptrace после того, как программа была остановлена ​​в четко определенной точке ;
  • доступ к любому энергозависимому объекту представляет собой набор четко определенных точек ПК (счетчик программ), так что можно установить точку останова (**): выражение, выполняющее энергозависимый доступ, преобразуется в набор адресов в кодегде разрыв вызывает разрыв в определенном выражении C / C ++;
  • состояние любого изменчивого объекта может быть произвольно изменено (*) с помощью ptrace при остановке программы, ограничено только допустимыми значениямиобъект в C / C ++;изменение битового шаблона энергозависимого объекта с помощью ptrace эквивалентно добавлению выражения присваивания в C / C ++ в четко определенную точку останова C / C ++, поэтому это эквивалентно изменению исходного кода C / C ++ во время выполнения.

Это означает, что у вас есть хорошо определенное наблюдаемое состояние ptrace энергозависимых объектов в этих точках, точка.

(*) Но вы не можете установить для энергозависимого объекта недопустимый битовый шаблон с помощью ptrace:компилятор может предположить, что любой объект имеет допустимую битовую комбинацию , как определено ABI .Любое использование ptrace для доступа к изменчивому состоянию должно соответствовать спецификации ABI объектов, совместно используемых с отдельно скомпилированным кодом.Например, компилятор может предположить, что у изменчивого числового объекта нет отрицательного нулевого значения, если ABI не позволяет это.(Очевидно, отрицательный ноль является допустимым состоянием, семантически отличным от положительного нуля, для чисел с плавающей запятой IEEE.)

(**) Встраивание и развертывание цикла могут генерировать много точек в ассемблере / двоичном коде, соответствующих уникальному C / C ++точка;отладчики справляются с этим, устанавливая множество точек останова уровня ПК для одной точки останова уровня источника.

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

[На практике все компиляторы предоставляют более строгую гарантию, чем семантика ptrace: все изменчивые объекты имеютстабильный адрес, даже если его адрес никогда не берется в коде C / C ++;эта гарантия иногда бесполезна и строго пессимистична.Легкая семантическая гарантия ptrace сама по себе чрезвычайно полезна для автоматической переменной в регистре в «сборке высокого уровня».]

Вы не можете проверить работающую программу (или поток), не остановив ее;вы не можете наблюдать с любого процессора без синхронизации (ptrace обеспечивает такую ​​синхронизацию).

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

При более высоком уровне оптимизации вычисления сокращаются, и переменные могут даже оптимизироваться, если они не содержат полезной информации для какого-либозаконный прогон;наиболее очевидным случаем является переменная «квазиконст», которая не объявлена ​​как константная, а использует a-if const: устанавливается один раз и никогда не изменяется.Такая переменная не несет никакой информации во время выполнения, если выражение, которое использовалось для ее установки, может быть пересчитано позже.

Многие переменные, которые содержат полезную информацию, все еще имеют ограниченный диапазон: если в программе нет выражения, которое может установитьцелочисленный тип со знаком и отрицательный математический результат (результат, который действительно отрицательный, а не отрицательный из-за переполнения в системе с 2 дополнениями), компилятор может предположить, что они не имеют отрицательных значений.Любая попытка установить для них отрицательное значение в отладчике или через ptrace будет не поддерживаться, так как компилятор может генерировать код, который интегрирует предположение;превращение объекта в энергозависимый заставило бы компилятор разрешить любое возможное допустимое значение для объекта, даже если в полном коде присутствуют только назначения положительных значений (код во всех путях, которые могут получить доступ к этому объекту, в каждом TU (единице перевода)который может получить доступ к объекту).

Обратите внимание, что для любого объекта, который совместно используется за пределами набора коллективно переведенного кода (все TU, которые скомпилированы и оптимизированы вместе), ничего о возможных значениях нетобъекта можно предположить рядом с применимым ABI.

Ловушка (а не ловушка, как в вычислениях) заключается в том, чтобы ожидать, что Java изменчива как семантика, по крайней мере, в одном ЦП, в линейном, упорядоченном семантическом программировании (где естьопределение no внеочередного исполнения, так как в состоянии только POV для состояния, единственного и единственного процессора):

int *volatile p = 0;
p = new int(1);

Нет никакой устойчивой гарантии, что p может быть только нулевым или указывать на объектсо значением 1: нет никакого изменчивого упорядочения, подразумеваемого между инициализацией int и настройку энергозависимого объекта, поэтому обработчик асинхронного сигнала или точка останова в энергозависимом присвоении могут не видеть инициализированный int.

Но энергозависимый указатель не может изменяться умозрительно: до тех пор, пока компиляторполучает гарантию, что выражение rhs (правая сторона) не будет генерировать исключение (таким образом, оставляя p нетронутым), оно не может изменять изменяемый объект (так как изменяемый доступ является наблюдаемым по определению).

Возвращаясь к вашему коду:

INTENABLE = 0; // volatile write (A)
my_var += 5;  // normal write
INTENABLE = 1; // volatile write (B)

Здесь INTENABLE является изменчивым, поэтому все доступы наблюдаемы;компилятор должен производить именно эти побочные эффекты;обычные записи являются внутренними для абстрактной машины, и компилятору нужно только сохранить эти побочные эффекты WRT для получения правильного результата, без учета каких-либо сигналов, которые находятся за пределами абстрактной семантики C / C ++.

В терминахиз семантики ptrace вы можете установить точку останова в точках (A) и (B) и наблюдать или изменить значение INTENABLE, но это все.Хотя my_var не может быть полностью оптимизирован, так как он доступен внешним кодом (кодом передачи сигнала), но в этой функции нет ничего другого, что может получить к нему доступ, , поэтому конкретное представление my_var не имеетчтобы соответствовать его значению согласно абстрактной машине в этой точке .

Это отличается, если у вас есть вызов к истинно внешнему (не анализируемому компилятором, вне "коллективно"переведенный код ") промежуточная функция:

INTENABLE = 0; // volatile write (A)
external_func_1(); // actual NOP be can access my_var 
my_var += 5;  // normal write
external_func_2(); // actual NOP be can access my_var 
INTENABLE = 1; // volatile write (B)

Обратите внимание, что необходимы оба этих внешних вызова функций" ничего не делать, возможно, делать что-либо ":

  • external_func_1() возможно наблюдает предыдущее значение my_var
  • external_func_2() возможно наблюдает новое значение my_var

Эти вызовы относятся к внешним, отдельно скомпилированным функциям NOP, которые должны выполняться в соответствии с ABI;таким образом * все глобально доступные объекты должны нести представление ABI их значения абстрактной машины : объекты должны достичь своего канонического состояния, в отличие от оптимизированного состояния, когда оптимизатор знает, что некоторое конкретное представление памяти некоторых объектов не достиглозначение абстрактной машины.

В GCC такая внешняя функция без действия может быть записана либо asm("" : : : "memory");, либо просто asm("");."memory" указан неопределенно, но ясно означает «доступ к чему-либо в памяти, чей адрес был пропущен глобально».

[Смотрите здесь, я полагаюсь на прозрачное намерение спецификации, а не на ее слова, так как слова очень часто неправильно выбираются (#) и никем не используются для построения реализации в любом случае, и толькомнение людей считается, слова никогда не имеют значения.

(#) по крайней мере в мире распространенных языков программирования, где люди не имеют квалификации для написания формальных или даже правильных спецификаций.]

0 голосов
/ 13 декабря 2018

Без прерываний, я думаю, вы в безопасности от переключения планировщика и изменения вашей переменной за вашей спиной.Но вплоть до мельчайших деталей, это, вероятно, зависит от архитектуры компьютера.Это верно для типичного x86.

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

Краткий ответ: нахождение в критической секции не спасет вашу энергонезависимую переменную от оптимизатора.

...