To nie jest tak naprawdę ponowne wejście ; nie uruchamiasz funkcji dwukrotnie w tym samym wątku (lub w różnych wątkach). Możesz to uzyskać poprzez rekurencję lub przekazanie adresu bieżącej funkcji jako argument funkcji zwrotnej arg do innej funkcji. (I nie byłoby to niebezpieczne, ponieważ byłoby synchroniczne).
To jest po prostu waniliowy wyścig danych UB (niezdefiniowane zachowanie) między procedurą obsługi sygnału a głównym wątkiem: tylko sig_atomic_t
jest bezpieczny . Inne mogą działać, na przykład w twoim przypadku, gdy 8-bajtowy obiekt można załadować lub zapisać za pomocą jednej instrukcji na x86-64, a kompilator wybiera taki asm. (Jak pokazuje odpowiedź @ icarus).
Zobacz programowanie MCU - optymalizacja C ++ O2 zrywa podczas pętli - procedura obsługi przerwań na mikrokontrolerze z jednym rdzeniem jest w zasadzie taka sama jak procedura obsługi sygnałów w programie z jednym wątkiem. W takim przypadku wynikiem UB jest to, że ładunek został wyciągnięty z pętli.
Twój przypadek testowy zerwania faktycznie zachodzi z powodu wyścigu danych UB został prawdopodobnie opracowany / przetestowany w trybie 32-bitowym lub ze starszym głupszym kompilatorem, który ładował osobno elementy struktury.
W twoim przypadku kompilator może zoptymalizować zapasy z nieskończonej pętli, ponieważ żaden program bez UB nigdy ich nie zaobserwuje. data
nie ma _Atomic
lubvolatile
, i nie ma innych efektów ubocznych w pętli. Więc nie ma mowy, aby jakikolwiek czytnik mógł zsynchronizować się z tym pisarzem. Dzieje się tak, jeśli kompilujesz z włączoną optymalizacją ( Godbolt pokazuje pustą pętlę u dołu głównego). Zmieniłem również struct na dwa long long
, a gcc używa pojedynczego movdqa
16-bajtowego magazynu przed zapętleniem. (Nie jest to gwarantowane atomowo, ale w praktyce działa na prawie wszystkich procesorach, zakładając, że jest wyrównane, lub na Intelie po prostu nie przekracza granicy linii pamięci podręcznej. Dlaczego przypisanie liczb całkowitych naturalnie wyrównanej zmiennej atomowej na x86? )
Zatem kompilacja z włączoną optymalizacją również przerwałaby test i za każdym razem pokazywałaby tę samą wartość. C nie jest przenośnym językiem asemblera.
volatile struct two_int
zmusiłoby również kompilator do nieoptymalizowania ich, ale nie zmusiłoby go do załadowania / przechowywania całej struktury atomowo. (Nie byłoby powstrzymać go od tego czy, choć.) Zauważ, że volatile
nie nie uniknąć danych wyścigu UB, ale w praktyce jest to wystarczające dla komunikacji między gwintem i było to, jak ludzie budowane ręcznie walcowane ATOMiCS (wraz z inline ASM) przed C11 / C ++ 11, dla normalnych architektur CPU. Są cache-spójny tak volatile
jest w praktyce przeważnie podobny do _Atomic
zmemory_order_relaxed
czystej obciążenia i czystej-sklepu, jeśli są stosowane dla typów zawęzić tyle że kompilator użyje pojedynczą instrukcję, aby nie dostać łzawienie. I oczywiścievolatile
nie ma żadnych gwarancji ze standardu ISO C w porównaniu do pisania kodu, który kompiluje się do tego samego asm przy użyciu _Atomic
i mo_relaxed.
Jeśli miałeś funkcję, która działała global_var++;
na int
lub long long
, że biegniesz z głównego i asynchronicznie z procedury obsługi sygnału, byłby to sposób na użycie ponownego wejścia do utworzenia UB wyścigu danych.
W zależności od sposobu kompilacji (do miejsca docelowego pamięci inc lub add, lub do oddzielenia load / inc / store) byłoby atomowe lub nie w odniesieniu do procedur obsługi sygnałów w tym samym wątku. Zobacz Can num ++ be atomic dla 'int num'? więcej informacji o atomowości na x86 i w C ++. (C11 stdatomic.h
i _Atomic
atrybut zapewniają funkcjonalność równoważną std::atomic<T>
szablonowi C ++ 11 )
Przerwanie lub inny wyjątek nie może się zdarzyć w środku instrukcji, więc dodanie do miejsca docelowego pamięci jest niepodzielne. kontekst włącza jednordzeniowy procesor. Jedynie (spójny z pamięcią podręczną) moduł zapisujący DMA mógł „nadepnąć” na przyrost z prefiksu add [mem], 1
bez lock
prefiksu na jednordzeniowy procesor. Nie ma żadnych innych rdzeni, na których mógłby działać inny wątek.
Jest to więc podobne do przypadku sygnałów: procedura obsługi sygnału działa zamiast normalnego wykonania wątku obsługującego sygnał, więc nie można go obsłużyć w środku jednej instrukcji.