Código de ejemplo de IBM, las funciones no reentrantes no funcionan en mi sistema

11

Estaba estudiando el reencuentro en la programación. En este sitio de IBM (realmente bueno). He fundado un código, copiado a continuación. Es el primer código que viene rodando por el sitio web.

El código intenta mostrar los problemas relacionados con el acceso compartido a la variable en un desarrollo no lineal de un programa de texto (asincronía) al imprimir dos valores que cambian constantemente en un "contexto peligroso".

#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;
   }
}

Los problemas aparecieron cuando intenté ejecutar el código (o mejor, no apareció). Estaba usando gcc versión 6.3.0 20170516 (Debian 6.3.0-18 + deb9u1) en la configuración predeterminada. No se produce la salida equivocada. ¡La frecuencia para obtener valores de par "incorrectos" es 0!

¿Qué está pasando después de todo? ¿Por qué no hay ningún problema en volver a estar fascinado usando variables globales estáticas?

Daniel Bandeira
fuente
1
Asegúrese de que toda la optimización del compilador esté deshabilitada e intente nuevamente
roaima
Supuse que ... pero ¿qué opciones cambiaría? No tengo idea. :-(
Daniel Bandeira
55
Esto parece una pregunta de programación (desbordamiento de pila). La dosis no parece estar bien ubicada aquí. (Lo siento, con menos sitios secundarios; está muy cortado. Pero así son las cosas.)
ctrl-alt-delor
1
El código entrante más simple es inmutable.
ctrl-alt-delor
En el primer momento, creo que la pregunta estaría relacionada con el entorno gcc y Linux. Evolucionando, por ejemplo, la programación del sistema operativo (ejecutando más texto del programa después de la señal de interrupción antes de llamar a la rutina del controlador), por ejemplo.
Daniel Bandeira

Respuestas:

12

Eso no es realmente arrepentimiento ; no está ejecutando una función dos veces en el mismo hilo (o en hilos diferentes). Puede obtener eso a través de la recursión o pasando la dirección de la función actual como un argumento de puntero de función de devolución de llamada a otra función. (Y no sería inseguro porque sería sincrónico).

Esto es simplemente UB (comportamiento indefinido) de vainilla de datos entre un controlador de señal y el hilo principal: solo sig_atomic_tse garantiza su seguridad . Puede que otros funcionen, como en el caso en que un objeto de 8 bytes se puede cargar o almacenar con una instrucción en x86-64, y el compilador elige ese asm. (Como muestra la respuesta de @ icarus).

Consulte la programación de MCU: la optimización de C ++ O2 se rompe durante el bucle : un controlador de interrupción en un microcontrolador de núcleo único es básicamente lo mismo que un controlador de señal en un programa de subproceso único. En ese caso, el resultado del UB es que una carga se levantó de un bucle.

Su caso de prueba de desgarro en realidad debido a la carrera de datos UB probablemente se desarrolló / probó en modo de 32 bits, o con un compilador más tonto que cargó los miembros de la estructura por separado.

En su caso, el compilador puede optimizar las tiendas desde el bucle infinito porque ningún programa sin UB podría observarlas. datano es _Atomicovolatile , y no hay otros efectos secundarios en el ciclo. Así que no hay forma de que ningún lector pueda sincronizarse con este escritor. De hecho, esto sucede si compila con la optimización habilitada ( Godbolt muestra un bucle vacío en la parte inferior de main). También cambié la estructura a dos long long, y gcc usa una sola movdqatienda de 16 bytes antes del ciclo. (Esto no está garantizado atómico, pero en la práctica en casi todas las CPU, suponiendo que esté alineado, o en Intel simplemente no cruza un límite de línea de caché. ¿Por qué la asignación de enteros en una variable atómica alineada naturalmente en x86? )

Por lo tanto, compilar con la optimización habilitada también rompería su prueba y le mostraría el mismo valor cada vez. C no es un lenguaje ensamblador portátil.

volatile struct two_intTambién forzaría al compilador a no optimizarlos, pero no forzaría a cargar / almacenar la estructura completa atómicamente. (No sería dejar que lo hagan bien, sin embargo.) Tenga en cuenta que volatileno no evitar UB-raza de datos, pero en la práctica es suficiente para la comunicación entre hilos y fue cómo las personas construyen atómica enrollados a mano (junto con asm en línea) antes de C11 / C ++ 11, para arquitecturas de CPU normales. Son caché coherente por lo que volatilees en la práctica, sobre todo similar a _Atomiclamemory_order_relaxed de pura carga y puro de la tienda, si se utiliza para Limitar suficiente como para que el compilador se utilice una sola instrucción para que no se consigue rasgado. Y por supuestovolatileno tiene ninguna garantía del estándar ISO C vs. escritura de código que se compila al mismo asm usando _Atomicy mo_relaxed.


Si tuviera una función que hiciera global_var++;en una into long longque se ejecute desde main y de forma asincrónica desde un controlador de señal, esa sería una forma de usar el reencuentro para crear UB de carrera de datos.

Dependiendo de cómo se compiló (a un destino de memoria inc o add, o para separar load / inc / store) sería atómico o no con respecto a los manejadores de señal en el mismo hilo. Consulte ¿Puede num ++ ser atómico para 'int num'? para más información sobre atomicidad en x86 y en C ++. (C11 stdatomic.hy el _Atomicatributo proporcionan una funcionalidad equivalente a la std::atomic<T>plantilla de C ++ 11 )

Una interrupción u otra excepción no puede suceder en medio de una instrucción, por lo que una adición de destino de memoria es atómica wrt. El contexto activa una CPU de un solo núcleo. Solo un escritor de DMA (coherente de caché) podría "pisar" un incremento desde un add [mem], 1sin lockprefijo en una CPU de un solo núcleo. No hay otros núcleos en los que se pueda ejecutar otro subproceso.

Por lo tanto, es similar al caso de las señales: se ejecuta un controlador de señales en lugar de la ejecución normal del subproceso que maneja la señal, por lo que no se puede manejar en medio de una instrucción.

Peter Cordes
fuente
2
Me impulsaron a aceptar la suya como la mejor respuesta, a pesar de que la respuesta de Icaru fue suficiente para mí. Los conceptos claros que nos dijiste me dan un montón de temas para estudiar todo este día (y más). De hecho, apenas tengo lo que escribes en los dos primeros párrafos a primera vista. ¡Gracias! Si publica artículos en Internet sobre computadoras y programación, ¡denos el enlace!
Daniel Bandeira
17

Mirando el explorador del compilador godbolt (después de agregar los que faltan #include <unistd.h>), uno ve que para casi cualquier compilador x86_64 el código generado usa movimientos QWORD para cargar onesy zerosen una sola instrucción.

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

El sitio de IBM dice On most machines, it takes several instructions to store a new value in data, and the value is stored one word at a time.qué podría haber sido cierto para los cpus típicos en 2005, pero como muestra el código, ahora no es cierto. Cambiar la estructura para tener dos longs en lugar de dos ints mostraría el problema.

Anteriormente escribí que esto era "atómico", que era vago. El programa solo se ejecuta en una sola CPU. Cada instrucción se completará desde el punto de vista de esta CPU (suponiendo que no haya nada más que altere la memoria, como dma).

Entonces, en el Cnivel no está definido que el compilador elegirá una sola instrucción para escribir la estructura, por lo que puede ocurrir la corrupción mencionada en el documento de IBM. Los compiladores modernos que apuntan a los cpus actuales utilizan una sola instrucción. Una sola instrucción es lo suficientemente buena como para evitar la corrupción de un solo programa de subprocesos.

Ícaro
fuente
3
Intente cambiar el tipo de datos de inta long longy compile a 32 bits. La lección es que nunca se sabe si / cuándo se romperá.
ctrl-alt-delor
2
¿eso significa que, en mi máquina, la asignación de estos dos valores es una operación atómica? (considerando la compilación para la arquitectura x86_64)
Daniel Bandeira
1
long longTodavía compila a una instrucción para x86-64: 16 bytes movdqa. A menos que desactive la optimización, como en su enlace Godbolt. (El valor predeterminado de GCC es el -O0modo de depuración, que está lleno de ruido de almacenamiento / recarga y, por lo general, no es interesante de ver.)
Peter Cordes
Cambié el tipo a "mucho, mucho" después de leer todos los comentarios. El resultado fue interesante: se lograron los resultados esperados y, al configurar algunos contadores, pudo mejorar otras concepciones sobre cómo la tasa de datos no coincidentes está influenciada por el resto del código. Gracias por toda la ayuda!
Daniel Bandeira