¿Es una mala idea desactivar las interrupciones sensibles al tiempo en C?

5

A veces veo un código que deshabilita las interrupciones, por ejemplo, para realizar una lectura / escritura no atómica en una variable global utilizada en un ISR. En AVR con gcc, esto puede parecer:

ExpensiveOperation();
cli();
// Perform a non-atomic read/write
sei(); // Assume that it is acceptable to enable global interrupts here.

Las macros cli y sei se expanden a asm declaraciones volátiles con barreras de memoria. La palabra clave volátil garantiza que las instrucciones cli / sei no se optimicen, mientras que las barreras de memoria aseguran que si la lectura / escritura no atómica es una variable volátil, ocurrirá entre las instrucciones cli y sei. Sin embargo, esta página sugiere que nada impide que el compilador ponga ExpensiveOperation después de la instrucción cli .

Las interrupciones a menudo requieren una sincronización precisa. Si deshabilitar las interrupciones en C corre el riesgo de que las operaciones costosas se ejecuten con interrupciones deshabilitadas, ¿la sincronización de interrupciones críticas solo debe deshabilitarse en el ensamblaje en línea (o su programa debe reescribirse para usar solo lecturas / escrituras atómicas)?

    
pregunta varactyl

4 respuestas

2

Dependiendo de qué tipo de efectos secundarios tenga ExpensiveOperation() , puede ser ilegítimo que un compilador lo mueva más allá de una llamada a una función que el compilador no puede "ver" en if, desde el punto de vista del compilador , habría una posibilidad de que tal función haga uso de tales efectos secundarios. Sería útil si hubiera una función estándar, típicamente implementada por un intrínseco, que en realidad no haría nada, pero que un compilador tendría que tratar como si pudiera hacer algo. Los compiladores que trataron esta función como una intrínseca podrían evitar tener que generar realmente una instrucción de "llamada" inútil [o cualquier otro código], más allá del hecho necesario para garantizar que cualquier variable global que se almacene en caché en los registros se elimine antes de " llamada ".

Lamentablemente, aunque llamar primero a una función externa obligaría a un compilador a procesar ExpensiveOperation() en su totalidad primero, no sé de nada que elimine al 100% la posibilidad de que un compilador identifique partes de esa función cuya ejecución podría ser diferido Sin embargo, lo mejor que se puede esperar es agregar un efecto secundario secuencial a ExpensiveOperation() y llamar a una función externa que requiera que se haya completado dicho efecto secundario.     

respondido por el supercat
3

El problema principal aquí es que este compilador aparentemente decidió reordenar las instrucciones del ensamblador en línea como si fueran cualquier código C, sin tener una idea de lo que hacen esas instrucciones. Entonces, mientras que esas instrucciones pueden no acceder a ninguna variable de nivel C, ¡acceden al registro de código de condición muy fundamental!

El compilador no tiene explícitamente permiso para reordenar las instrucciones si afectaría el comportamiento del programa, punto. Y es bastante difícil encontrar algo que pueda afectar el comportamiento del programa más que lo que se escribe en el registro más fundamental del núcleo de la CPU.

Por lo tanto, este es un comportamiento sin sentido y solo se puede considerar como un error del compilador, probablemente en el puerto AVR.

Una posible solución alternativa para este error del compilador podría ser la introducción de un efecto secundario, según lo especificado por el estándar C, antes de la llamada cli (). Una forma de hacer esto es implementar ExpensiveOperation () como una función C real. Tenga en cuenta que en el ejemplo vinculado, la operación costosa no era una función. Otra forma sería escribir algún código ficticio como

ExpensiveOperation();
volatile uint8_t invoke_side_effect = 0;
cli();

De lo contrario, el procedimiento estándar cuando se encuentra con un optimizador de errores es deshabilitar ese optimizador. Si puedes encontrar la opción particular que le permita al compilador reordenar las instrucciones y deshabilitarlas, eso es lo mejor que puedes hacer.

    
respondido por el Lundin
1

Puede intentar poner la secuencia cli () / write / sei () en su propia función. Descubrí que los optimizadores tienden a ser reacios a mover el código a través de las funciones, especialmente cuando optimizan el tamaño. Sin embargo, no estoy seguro de AVR-GCC específicamente.

    
respondido por el Adam Haun
1

Hay varios problemas aquí:

  1. AFAIK, solo hay un bit para controlar las interrupciones en un AVR. Por lo tanto, la discusión sobre un control de interrupción más preciso no se aplica a esta CPU.
  2. El ejemplo ilustra un caso sólido para buscar en el ensamblador el código generado por el compilador .
    Debería ser práctico detectar todos los usos de cli to sei con un pequeño script de edición aplicado a la salida objdump (para que sea fácil de monitorear). Un script basado en programas podría resaltar todos los ejemplos de "llamada" entre ellos.
    Esto podría mostrar que no hay problema para resolver en su código.
  3. El control de las interrupciones usando cli () / sei () es la forma más fácil para que un desarrollador asegure el acceso atómico y, por lo tanto, mantenga la consistencia de la memoria, para valores de múltiples bytes en un AVR.
    Tener que escribir el ensamblador puede ser tan propenso a errores, o tener un impacto negativo en la optimización, que es un enfoque deficiente. No lo haría hasta que el resultado del paso 1 muestre que hay un problema que resolver.
  4. El ejemplo muestra que el compilador ha generado un código para el entero divida insertando una llamada de subrutina para val = 65535U / val , y esa llamada es el código que se ha 'optimizado' entre cli () a sei ().
    Sin embargo , no pruebe que las subrutinas generadas por el usuario se mueven alguna vez al código entre cli () y sei (). Así que esto puede no ser un problema para el caso, potencialmente mucho más significativo.

  5. Este ejemplo se soluciona introduciendo una nueva variable volatile :

    #define cli() __asm volatile( "cli" ::: "memory" )
    #define sei() __asm volatile( "sei" ::: "memory" )
    unsigned int ivar;  
    void test2( unsigned int val ) {  
       volatile unsigned int val1 = 65535U / val;
       cli();
       ivar = val1;
       sei();
    }
    

El código generado se convirtió en:

  92:   cf 93           push    r28
  94:   df 93           push    r29
  96:   00 d0           rcall   .+0         ; 0x98 <_Z5test2j+0x6>
  98:   cd b7           in  r28, 0x3d   ; 61
  9a:   de b7           in  r29, 0x3e   ; 62
  9c:   bc 01           movw    r22, r24
  9e:   8f ef           ldi r24, 0xFF   ; 255
  a0:   9f ef           ldi r25, 0xFF   ; 255
  a2:   0e 94 fb 00     call    0x1f6   ; 0x1f6 <__udivmodhi4>
  a6:   7a 83           std Y+2, r23    ; 0x02
  a8:   69 83           std Y+1, r22    ; 0x01
  aa:   f8 94           cli  <----------------------- switch off interrupts
  ac:   89 81           ldd r24, Y+1    ; 0x01
  ae:   9a 81           ldd r25, Y+2    ; 0x02
  b0:   90 93 01 01     sts 0x0101, r25
  b4:   80 93 00 01     sts 0x0100, r24
  b8:   78 94           sei  <----------------------- switch on interrupts
  ba:   0f 90           pop r0
  bc:   0f 90           pop r0
  be:   df 91           pop r29
  c0:   cf 91           pop r28
  c2:   08 95           ret

Esto no es bonito, pero es relativamente fácil y puede ser suficiente. Por supuesto, evitar optimizaciones prematuras; no haga cambios sutiles para controlar el compilador hasta que se vuelva importante, y preferiblemente después de que el código esté funcionando y sea estable.

Verificaría el código generado y esperaría a ver un problema, luego resolvería ese caso específico.

Creo que plantearía un informe de error si mi código de nivel de usuario se mueve entre cli () / sei (). Eso le daría a los desarrolladores del compilador la oportunidad de identificar soluciones o desarrollar arreglos. Los desarrolladores de compiladores tienen la libertad de inventar soluciones, a menudo utilizando pragma's, y pueden responder a un informe de errores ofreciendo una solución sólida.

Mientras tanto, sería más fácil continuar por el camino fácil, en lugar de hacer que el desarrollo sea más difícil, hasta que haya evidencia de un problema importante.

    
respondido por el gbulmer

Lea otras preguntas en las etiquetas