Приклад коду 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, а компілятор вибирає цю зону. (Як показує відповідь @ icarus).

Див. Програмування MCU - оптимізація C ++ O2 перервана під час циклу - обробник переривання на одноядерному мікроконтролері - це те саме, що обробник сигналу в одній потоковій програмі. У такому випадку результат UB полягає в тому, що навантаження піднімається з петлі.

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

У вашому випадку компілятор може оптимізувати магазини з нескінченного циклу, оскільки жодна програма без UB не могла їх спостерігати. dataнемає _Atomicабоvolatile , і немає інших побічних ефектів у циклі. Тому жоден читач не міг би синхронізуватися з цим автором. Це насправді відбувається, якщо компілювати з увімкненою оптимізацією ( Godbolt показує порожній цикл у нижній частині основного). Я також змінив структуру на дві long long, і gcc використовує єдиний movdqa16-байтовий сховище перед циклом. (Це не є гарантованим атомним, але це на практиці майже на всіх процесорах, якщо припустити, що він вирівняний, або в Intel просто не перетинає межу кеш-лінії. Чому присвоєння цілого числа природно вирівняній атомній змінній на x86? )

Таким чином, компіляція з увімкненою оптимізацією також порушить ваш тест і показуватиме вам те саме значення кожного разу. C - це не портативна мова складання.

volatile struct two_intТакож змушує компілятор не оптимізувати їх, але не змусить його завантажувати / зберігати всю структуру атомним шляхом. (Це також не завадило б це зробити.) Зауважте, що volatileце не дозволяє уникнути UB-перегонів з передачею даних, але на практиці це достатньо для зв'язку між потоками і те, як люди будували атоміку вручну (разом з inline asm) до C11 / C ++ 11, для звичайних архітектур процесора. Вони кеш-когерентний так volatileце на практиці в основному аналогічна _Atomicзmemory_order_relaxed для чистої навантаження і чистого-магазину, якщо вони використовуються для типів досить вузько , що компілятор буде використовувати одну команду , так що ви не отримаєте сльозотеча. І звичайноvolatileне має жодних гарантій від стандарту ISO C проти коду написання, який компілюється в один _Atomicі той самий ASM, використовуючи та mo_relaxed.


Якщо у вас була функція, яка виконується global_var++;на intабо, long longщо ви запускаєтесь від головного та асинхронно від обробника сигналу, це був би спосіб використовувати повторне вступ для створення UB-перегону даних.

Залежно від способу компіляції (до місця призначення пам'яті inc або add, або для розділення завантаження / inc / store), це буде атомним чи ні щодо відносно оброблювачів сигналів в одному потоці. Див. Чи може num ++ бути атомним для 'int num'? Докладніше про атомність на x86 та на C ++. (C11 stdatomic.hта _Atomicатрибут забезпечує еквівалентну функціональність std::atomic<T>шаблону C ++ 11 )

Переривання або інший виняток не може статися в середині інструкції, тому додавання призначення пам'яті є атомним wrt. контекст вмикає одноядерний процесор. Лише (кешований кеш-пам'ять) 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 році, але, як показує код, зараз це неправда. Зміна структури на дві довжини, а не дві вставки, показало б проблему.

Я раніше писав, що це "атомне", що ледаче. Програма працює лише на одному процесорі. Кожна інструкція буде завершена з точки зору цього процесора (якщо припустити, що нічого іншого не змінить пам'ять, наприклад, 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.