Пример кода IBM, не входящие функции не работают в моей системе


11

Я изучал повторный вход в программирование. На этом сайте IBM (действительно хороший). Я основал код, скопированный ниже. Это первый код, который катится по сайту.

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

#include <signal.h>
#include <stdio.h>

struct two_int { int a, b; } data;

void signal_handler(int signum){
   printf ("%d, %d\n", data.a, data.b);
   alarm (1);
}

int main (void){
   static struct two_int zeros = { 0, 0 }, ones = { 1, 1 };

   signal (SIGALRM, signal_handler); 
   data = zeros;
   alarm (1);
   while (1){
       data = zeros;
       data = ones;
   }
}

Проблемы возникли, когда я попытался запустить код (или, лучше сказать, не появился). Я использовал gcc версии 6.3.0 20170516 (Debian 6.3.0-18 + deb9u1) в конфигурации по умолчанию. Неправильный вывод не происходит. Частота получения «неправильных» парных значений равна 0!

Что происходит в конце концов? Почему нет проблем при повторном входе с использованием статических глобальных переменных?


1
Убедитесь, что вся оптимизация компилятора отключена, и попробуйте снова
roaima

Я предполагал, что ... но какие варианты я бы изменил? Не имею представления. :-(
Даниэль Бандейра

5
Это похоже на вопрос программирования (переполнение стека). Это доза, кажется, не очень хорошо здесь. (Извините, у меня было меньше суб-сайтов; это так порезано. Но так оно и есть.)
ctrl-alt-delor

1
Самый простой повторяющийся код является неизменным.
Ctrl-Alt-Delor

Сначала я думаю, что этот вопрос будет связан со средой gcc и Linux. Например, развитие расписания ОС (выполнение большего количества текста программы после сигнала прерывания перед вызовом процедуры обработчика), например.
Даниэль Бандейра

Ответы:


12

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

Это просто обычная гонка данных UB (Undefined Behavior) между обработчиком сигнала и основным потоком: только sig_atomic_tгарантировано безопасное для этого . Другие могут работать, как в вашем случае, когда 8-байтовый объект может быть загружен или сохранен с одной инструкцией на x86-64, и компилятор выбирает этот asm. (Как показывает ответ @ icarus).

См. Программирование MCU - оптимизация C ++ O2 прерывается во время цикла - обработчик прерываний в одноядерном микроконтроллере - это то же самое, что и обработчик сигналов в однопоточной программе. В этом случае результат UB - то, что груз был поднят из петли.

Ваш тестовый случай разрыва действительно происходит из-за гонок данных UB, вероятно, был разработан / протестирован в 32-битном режиме или с более старым тупым компилятором, который загружал члены структуры отдельно.

В вашем случае компилятор может оптимизировать хранилища из бесконечного цикла, потому что ни одна свободная от UB программа никогда не сможет их наблюдать. dataне является _Atomicилиvolatile , и нет никаких других побочных эффектов в цикле. Так что никакой читатель не сможет синхронизироваться с этим писателем. Фактически это происходит, если вы компилируете с включенной оптимизацией ( Godbolt показывает пустой цикл внизу main). Я также изменил структуру на два long long, и gcc использует одно movdqa16-байтовое хранилище перед циклом. (Это не является гарантированным атомарным, но на практике это происходит практически на всех процессорах, при условии, что он выровнен, или на Intel просто не пересекает границу строки кэша. Почему целочисленное присваивание для естественно выровненной переменной атомарно на x86? )

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

volatile struct two_intтакже заставит компилятор не оптимизировать их, но не заставит его загружать / хранить всю структуру атомарно. (Однако это также не помешало бы сделать это.) Обратите внимание, что volatileэто не предотвращает гонку данных UB, но на практике этого достаточно для связи между потоками, и это было то, как люди создавали атомарную структуру вручную (наряду с встроенным ассемблером). до C11 / C ++ 11, для нормальной архитектуры ЦП. Они кэш-когерентный так volatileэто на практике в основном аналогична _Atomicсmemory_order_relaxed для чистой нагрузки и чистого-магазина, если они используются для типов достаточно узко , что компилятор будет использовать одну команду , так что вы не получите слезотечение. И конечноvolatileне имеет никаких гарантий от стандарта ISO C против написания кода, который компилируется с использованием asm _Atomicи mo_relaxed.


Если у вас есть функция , которая сделала global_var++;по принципу intили long longзапускать от основной и асинхронно из обработчика сигнала, который был бы способ использовать повторно entrancy для создания данных гонки UB.

В зависимости от того, как он скомпилирован (в место назначения памяти inc или add, или для разделения загрузки / inc / store), он будет атомарным или нет относительно обработчиков сигналов в том же потоке. См. Может ли num ++ быть атомарным для int num? подробнее об атомарности на x86 и в C ++. ( Атрибут C11 stdatomic.hи _Atomicобеспечивает эквивалентную функциональность для std::atomic<T>шаблона C ++ 11 )

Прерывание или другое исключение не может произойти в середине инструкции, поэтому добавление к месту назначения памяти является атомарным. контекст переключается на одноядерный процессор. Только (совместимый с кэшем) DMA-писатель может «увеличить» шаг add [mem], 1без lockпрефикса на одноядерном процессоре. Нет никаких других ядер, на которых мог бы работать другой поток.

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


2
Я был вынужден принять ваш лучший ответ, несмотря на то, что ответ Икару был для меня достаточным. Четкие концепции, которые вы нам рассказали, дают мне кучу тем для изучения весь этот день (и далее). На самом деле, я не понимаю, что вы пишете в первых двух абзацах на первый взгляд. Спасибо! Если вы публикуете в интернете статьи о компьютерах и программировании, дайте нам ссылку!
Даниэль Бандейра

17

Глядя на проводник компилятора godbolt (после добавления отсутствующего #include <unistd.h>), можно увидеть, что почти для любого компилятора x86_64 сгенерированный код использует QWORD-перемещения для загрузки onesи zerosв одной инструкции.

        mov     rax, QWORD PTR main::ones[rip]
        mov     QWORD PTR data[rip], rax

На сайте IBM говорится, On most machines, it takes several instructions to store a new value in data, and the value is stored one word at a time.что это могло быть правдой для типичного процессора в 2005 году, но, как показывает код, сейчас это не так. Изменение структуры, чтобы иметь два long, а не два целых, показало бы проблему.

Я ранее писал, что это было «атомное», что было ленивым. Программа работает только на одном процессоре. Каждая инструкция завершится с точки зрения этого процессора (при условии, что ничто иное не изменяет память, такую ​​как dma).

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


3
Попробуйте изменить тип данных с intна long longи скомпилировать на 32 бита. Урок в том, что вы никогда не знаете, если / когда это сломается.
Ctrl-Alt-Delor

2
что означает, что в моей машине присвоение этих двух значений является атомарной операцией? (с учетом компиляции для архитектуры x86_64)
Даниэль Бандейра

1
long longпо-прежнему компилируется в одну инструкцию для x86-64: 16 байт movdqa. Если вы не отключите оптимизацию, как в вашей ссылке Godbolt. (По умолчанию в GCC используется -O0режим отладки, который полон шума хранения / перезагрузки и на него обычно не интересно смотреть.)
Питер Кордес

Я изменил тип на «длинный длинный» после прочтения всех комментариев. Результат был интересным: ожидаемые результаты были достигнуты, и, настроив некоторые счетчики, он смог улучшить другие представления о том, как остальная часть кода влияет на скорость несоответствующих данных. Спасибо за помощь!
Даниэль Бандейра
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.