[c] IBM 예제 코드, 재진입 할 ​​수없는 기능이 시스템에서 작동하지 않습니다

나는 프로그래밍에서 재진입을 공부하고 있었다. 이 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입니다!

결국 무슨 일이야? 정적 전역 변수를 사용한 재진입에 문제가없는 이유는 무엇입니까?



답변

그것은 실제로 재진입 은 아니다 . 동일한 스레드 (또는 다른 스레드)에서 함수를 두 번 실행하지 않습니다 . 재귀를 통해 또는 현재 함수의 주소를 콜백 함수 포인터 인수로 다른 함수에 전달하여 얻을 수 있습니다. (동기화되기 때문에 안전하지 않습니다).

이 신호 처리기 및 메인 스레드 사이에 그냥 일반 바닐라 데이터 레이스 UB (정의되지 않은 동작)입니다 : 만 sig_atomic_t이 안전 보장된다 . 8 바이트 객체를 x86-64에서 하나의 명령으로로드하거나 저장할 수 있고 컴파일러가 해당 asm을 선택하는 경우와 같이 다른 것들이 작동 할 수 있습니다. (@icarus의 답변이 보여 주듯이).

단일 코어 마이크로 컨트롤러의 인터럽트 핸들러는 기본적으로 단일 스레드 프로그램의 신호 핸들러와 동일합니다. MCU 프로그래밍-C ++ O2 최적화가 중단되는 동안 루프를 참조하십시오 . 이 경우 UB의 결과는로드가 루프에서 게양되었습니다.

데이터 레이싱 UB로 인해 실제로 발생하는 테어 링 테스트 사례는 아마도 32 비트 모드에서 또는 struct 멤버를 별도로로드하는 오래된 멍청한 컴파일러로 개발 / 테스트되었을 것입니다.

귀하의 경우, 컴파일러는 UB 프리 프로그램이 관찰 할 수 없기 때문에 무한 루프에서 저장소를 최적화 할 수 있습니다. data없다 _Atomic거나volatile , 루프의 다른 부작용이 없다. 따라서 어떤 독자도이 작가와 동기화 할 수있는 방법이 없습니다. 이것은 최적화가 활성화 된 상태에서 컴파일하면 발생합니다 ( Godbolt 는 메인의 하단에 빈 루프를 보여줍니다). 또한 구조체를 two로 변경했으며 long longgcc는 movdqa루프 전에 단일 16 바이트 저장소를 사용합니다. (이것은 원 자성 이 보장 되지는 않지만 , 실제로는 정렬 된 것으로 가정하거나 거의 모든 CPU에서 또는 인텔에서 캐시 라인 경계를 넘지 않는다고 가정합니다. x86에서 자연스럽게 정렬 된 변수 원자에 정수 할당이 왜 필요한가요? )

따라서 최적화를 사용하여 컴파일하면 테스트가 중단되고 매번 동일한 값이 표시됩니다. C는 이식 가능한 어셈블리 언어가 아닙니다.

volatile struct two_int또한 컴파일러가 최적화하지 못하게하지만 전체 구조체를 원자 적으로로드 / 저장하도록 강제 하지는 않습니다 . (그것은 않을 것 중지 참고.하지만, 하나 그렇게에서)를 volatile않습니다 하지 데이터 레이스 UB를 방지,하지만 실제로는 스레드 간 통신을위한 충분한의 사람들이 (인라인 ASM과 함께) 손으로 압연 아토을 구축하는 방법이었다 일반 CPU 아키텍처의 경우 C11 / C ++ 11 이전 그들은있는 거 캐시 일관성 이렇게 volatile이다 에 대부분 비슷한 연습 _Atomicmemory_order_relaxed 순수한 부하 및 순수 상점, 유형에 사용되는 경우에 당신이 찢어하지 않도록 컴파일러는 단일 명령어를 사용합니다 충분히 좁힐. 그리고 물론volatile_Atomic및 mo_relaxed를 사용하여 동일한 asm으로 컴파일하는 코드 작성과 ISO C 표준의 보증은 없습니다 .


당신이 한 기능이 있다면 global_var++;int또는 long long당신이 주에서 실행하는 것이 신호 처리기에서 비동기 적으로, 즉 데이터-레이스 UB를 만드는 사용 재진입 할 수있는 방법이 될 것입니다.

컴파일 된 방법에 따라 (메모리 대상 inc 또는 add 또는 load / inc / store를 분리하기 위해) 동일한 스레드의 신호 처리기와 관련하여 원자 적이거나 그렇지 않습니다. ‘int num’에 대해 num ++가 원자 성일 수 있습니까?를 참조하십시오 . x86 및 C ++의 원자성에 대해 자세히 알아보십시오. (C11 stdatomic.h_Atomic속성은 C ++ 11의 std::atomic<T>템플릿 과 동등한 기능을 제공합니다 )

명령 도중에 인터럽트 나 다른 예외가 발생할 수 없으므로 메모리 대상 추가는 원자 wrt입니다. 단일 코어 CPU의 컨텍스트 스위치. (캐시 코 히어 런트) DMA 기록기 만이 단일 코어 CPU add [mem], 1lock프리픽스 없이 증분 “스텝”할 수 있습니다 . 다른 스레드가 실행될 수있는 다른 코어가 없습니다.

따라서 신호의 경우와 유사합니다. 신호 처리기는 스레드를 정상적으로 처리하여 신호를 처리하는 대신 실행되므로 한 명령의 중간에서 처리 할 수 ​​없습니다.


답변

상기 상대 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 년에 일반적인 CPU에 대해서는 어느 것이 옳았을지 모르지만 코드에서 알 수 있듯이 지금은 그렇지 않습니다. 구조체가 두 개의 정수가 아닌 두 개의 정수를 갖도록 변경하면 문제가 표시됩니다.

나는 이전에 이것이 게으른 “원자”라고 썼다. 이 프로그램은 단일 CPU에서만 실행됩니다. 각 명령어는이 CPU의 관점에서 완료됩니다 (dma와 같은 메모리를 변경하는 다른 것이 없다고 가정).

따라서 C레벨에서 컴파일러가 구조체를 작성하기 위해 단일 명령을 선택하도록 정의되지 않았으므로 IBM 백서에서 언급 된 손상이 발생할 수 있습니다. 현재 CPU를 대상으로하는 최신 컴파일러는 단일 명령어를 사용합니다. 단일 명령어는 단일 스레드 프로그램의 손상을 피하기에 충분합니다.


답변