19 votos

Cómo implementar secciones críticas en ARM Cortex A9

Estoy portando un código heredado de un núcleo ARM926 a CortexA9. Este código es baremetal y no incluye un sistema operativo o bibliotecas estándar, todo personalizado. Tengo un fallo que parece estar relacionado con una condición de carrera que debería evitarse mediante el seccionamiento crítico del código.

Quiero algunos comentarios sobre mi enfoque para ver si mis secciones críticas pueden no estar correctamente implementadas para esta CPU. Estoy usando GCC. Sospecho que hay algún error sutil.

Además, ¿hay alguna biblioteca de código abierto que tenga este tipo de primitivas para ARM (o incluso una buena biblioteca ligera de spinlock/semephore)?

#define ARM_INT_KEY_TYPE            unsigned int
#define ARM_INT_LOCK(key_)   \
asm volatile(\
    "mrs %[key], cpsr\n\t"\
    "orr r1, %[key], #0xC0\n\t"\
    "msr cpsr_c, r1\n\t" : [key]"=r"(key_) :: "r1", "cc" );

#define ARM_INT_UNLOCK(key_) asm volatile ("MSR cpsr_c,%0" : : "r" (key_))

El código se utiliza de la siguiente manera:

/* lock interrupts */
ARM_INT_KEY_TYPE key;
ARM_INT_LOCK(key);

<access registers, shared globals, etc...>

ARM_INT_UNLOCK(key);

La idea de la "llave" es permitir secciones críticas anidadas, y éstas se utilizan al principio y al final de las funciones para crear funciones reentrantes.

Gracias.

1 votos

Por favor, remítase a infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dht0008a/ no lo hagas en asm incrustado por cierto. hazlo en una función como lo hace el artículo.

0 votos

No sé nada acerca de ARM, pero yo esperaría que para el mutex (o cualquier función de sincronización entre hilos o entre procesos), deberías usar el clobber de "memoria" para asegurarte de que a) todos los valores de memoria actualmente almacenados en caché en los registros sean devueltos a la memoria antes de ejecutando el asm y b) cualquier valor en memoria al que se acceda después de que el asm se recargue. Tenga en cuenta que la realización de una llamada (como recomienda HuStmpHrrr) debería realizar implícitamente esta carga por usted.

0 votos

Además, aunque todavía no hablo ARM, tus restricciones para 'key_' no parecen correctas. Dado que usted dice que esto está destinado a ser utilizado para la re-entrada, declarando como "=r" en el bloqueo parece sospechoso. '=' significa que usted tiene la intención de sobrescribirlo, y el valor existente no es importante. Parece más probable que hayas querido usar '+' para indicar tu intención de actualizar el valor existente. Y de nuevo para unlock, listarlo como una entrada le dice a gcc que no tienes intención de cambiarlo, pero si no me equivoco, lo haces (cambiarlo). Supongo que esto también debería ser listado como una salida '+'.

16voto

Alex Andronov Puntos 178

La parte más difícil de manejar una sección crítica sin un sistema operativo no es realmente crear el mutex, sino más bien averiguar lo que debe suceder si el código quiere utilizar un recurso que no está actualmente disponible. Las instrucciones load-exclusive y conditional-store-exclusive facilitan la creación de una función "swap" que, dado un puntero a un entero, almacenará atómicamente un nuevo valor pero devolverá lo que contenía el entero apuntado:

int32_t atomic_swap(int32_t *dest, int32_t new_value)
{
  int32_t old_value;
  do
  {
    old_value = __LDREXW(&dest);
  } while(__STREXW(new_value,&dest);
  return old_value;
}

Dada una función como la anterior, se puede introducir fácilmente un mutex mediante algo como

if (atomic_swap(&mutex, 1)==0)
{
   ... do stuff in mutex ... ;
   mutex = 0; // Leave mutex
}
else
{ 
  ... couldn't get mutex...
}

En ausencia de un sistema operativo, la principal dificultad suele residir en el código "no pudo obtener el mutex". Si se produce una interrupción cuando un recurso protegido por el mutex está ocupado, puede ser necesario hacer que el código que maneja la interrupción establezca una bandera y guarde alguna información para indicar lo que quería hacer, y luego hacer que cualquier código tipo main que adquiere el mutex compruebe cada vez que va a liberar el mutex para ver si una interrupción quería hacer algo mientras el mutex estaba retenido y, si es así, realizar la acción en nombre de la interrupción.

Aunque es posible evitar problemas con interrupciones que quieran utilizar recursos protegidos por mutex simplemente deshabilitando las interrupciones (y de hecho, deshabilitar las interrupciones puede eliminar la necesidad de cualquier otro tipo de mutex), en general es deseable evitar deshabilitar las interrupciones más de lo necesario.

Un compromiso útil puede ser utilizar una bandera como la descrita anteriormente, pero hacer que el código de la línea principal que va a liberar el mutex desactive las interrupciones y compruebe la bandera mencionada justo antes de hacerlo (vuelva a activar las interrupciones después de liberar el mutex). Este enfoque no requiere dejar las interrupciones deshabilitadas por mucho tiempo, pero evitará la posibilidad de que si el código de la línea principal comprueba la bandera de la interrupción después de liberar el mutex, existe el peligro de que entre el momento en que ve la bandera y el momento en que actúa sobre ella, podría ser adelantado por otro código que adquiere y libera el mutex y actúa sobre la bandera de la interrupción; si el código de la línea principal no comprueba la bandera de la interrupción después de liberar el mutex, una interrupción que ocurra justo antes de que el código de la línea principal libere el mutex podría ser bloqueada por el mutex pero no ser notada por la línea principal.

En cualquier caso, lo más importante será contar con un medio por el cual el código que intente utilizar un recurso protegido por mutex cuando no esté disponible tenga un medio para repetir su intento una vez que el recurso sea liberado.

8voto

phani Puntos 36

Esta es una forma pesada de hacer secciones críticas; deshabilitar las interrupciones. Puede que no funcione si su sistema tiene/maneja fallos de datos. También aumentará la latencia de las interrupciones. El Linux irqflags.h tiene algunas macros que se encargan de esto. El cpsie y cpsid pueden ser útiles; sin embargo, no guardan el estado y no permiten el anidamiento. cps no utiliza un registro.

Para el Cortex-A serie, el ldrex/strex son más eficientes y pueden trabajar para formar un mutex para la sección crítica o pueden utilizarse con sin cerradura algoritmos para deshacerse de la sección crítica.

En cierto sentido, el ldrex/strex parece un ARMv5 swp . Sin embargo, son mucho más complejas de aplicar en la práctica. Se necesita una caché de trabajo y la memoria de destino del ldrex/strex tiene que estar en la caché. La documentación de ARM sobre el ldrex/strex es bastante nebuloso, ya que quieren que los mecanismos funcionen en CPUs que no sean Cortex-A. Sin embargo, para el Cortex-A el mecanismo para mantener la caché local de la CPU sincronizada con otras CPUs es el mismo que se utiliza para implementar el ldrex/strex instrucciones. Para la serie Cortex-A el reserva granual (tamaño de ldrex/strex memoria reservada) es lo mismo que una línea de caché; también es necesario alinear la memoria a la línea de caché si se pretende modificar varios valores, como con una lista doblemente enlazada.

Sospecho que hay algún error sutil.

mrs %[key], cpsr
orr r1, %[key], #0xC0  ; context switch here?
msr cpsr_c, r1

Hay que asegurarse de que la secuencia nunca pueda ser adelantado . De lo contrario, puede obtener dos clave variables con las interrupciones activadas y la liberación del bloqueo será incorrecta. Puede utilizar la función swp instrucción con el clave para garantizar la coherencia en el ARMv5, pero esta instrucción está obsoleta en el Cortex-A en favor de ldrex/strex ya que funciona mejor para los sistemas multi-CPU.

Todo esto depende del tipo de programación que tenga tu sistema. Parece que sólo tienes líneas principales e interrupciones. A menudo se necesita el sección crítica primitivas para tener algunos ganchos al planificador dependiendo de los niveles (sistema/espacio de usuario/etc) con los que quieras que trabaje la sección crítica.

Además, ¿hay alguna biblioteca de código abierto que tenga este tipo de primitivas para ARM (o incluso una buena biblioteca ligera de spinlock/semephore)?

Esto es difícil de escribir de forma portátil. Es decir, tales bibliotecas pueden existir para ciertas versiones de CPUs ARM y para sistemas operativos específicos.

3voto

ivkremer Puntos 108

Veo varios problemas potenciales en esas secciones críticas. Hay advertencias y soluciones para todos ellos, pero como resumen:

  • No hay nada que impida al compilador mover el código a través de estas macros, por optimización o por otras razones aleatorias.
  • Guardan y restauran algunas partes del estado del procesador que el compilador espera que el ensamblaje en línea deje en paz (a menos que se le indique lo contrario).
  • No hay nada que impida que se produzca una interrupción en medio de la secuencia y que cambie el estado entre el momento de la lectura y el de la escritura.

En primer lugar, definitivamente necesitas algo de barreras de memoria del compilador . GCC los implementa como clobbers . Básicamente, es una forma de decirle al compilador "No, no puedes mover los accesos de memoria a través de esta pieza de ensamblaje en línea porque podría afectar al resultado de los accesos de memoria". Específicamente, se necesitan ambos "memory" y "cc" en las macros de inicio y fin. Esto evitará que otras cosas (como las llamadas a funciones) sean reordenadas en relación con el ensamblaje en línea también, porque el compilador sabe que podrían tener accesos a la memoria. He visto que GCC para ARM mantiene el estado en los registros de código de condición a través del ensamblaje en línea con "memory" de la capa, por lo que definitivamente se necesita el "cc" de la capa.

En segundo lugar, estas secciones críticas guardan y restauran mucho más que si las interrupciones están activadas. Específicamente, están guardando y restaurando la mayor parte del CPSR (Registro de estado del programa actual) (el enlace es para el Cortex-R4 porque no pude encontrar un buen diagrama para un A9, pero debería ser idéntico). Hay restricciones sutiles en torno a qué trozos de estado se pueden modificar realmente, pero aquí es más que necesario.

Entre otras cosas, esto incluye los códigos de condición (donde los resultados de las instrucciones como cmp se almacenan para que las instrucciones condicionales posteriores puedan actuar sobre el resultado). Esto confundirá al compilador. Esto es fácilmente solucionable utilizando la función "cc" de la que se habla más arriba. Sin embargo, esto hará que el código falle cada vez, por lo que no suena como lo que está viendo problemas. Sin embargo, es una especie de bomba de relojería, ya que modificar otro código al azar puede hacer que el compilador haga algo diferente que se rompa con esto.

Esto también intentará guardar/restaurar los bits de TI, que se utilizan para implementar la ejecución condicional del pulgar . Ten en cuenta que si nunca ejecutas el código del Pulgar, esto no importa. Nunca he averiguado cómo el ensamblaje en línea de GCC se ocupa de los bits IT, aparte de concluir que no lo hace, lo que significa que el compilador nunca debe poner el ensamblaje en línea en un bloque IT y siempre espera que el ensamblaje termine fuera de un bloque IT. Nunca he visto que GCC genere código que viole estas suposiciones, y he hecho algunos ensamblajes en línea bastante intrincados con fuerte optimización, así que estoy razonablemente seguro de que se mantienen. Esto significa que probablemente no intentará cambiar los bits de TI, en cuyo caso todo está bien. Intentar modificar estos bits es clasificado como "arquitectónicamente imprevisible" Así que podría hacer todo tipo de cosas malas, pero probablemente no hará nada en absoluto.

La última categoría de bits que se guardarán/restaurarán (además de los que realmente desactivan las interrupciones) son los bits de modo. Estos probablemente no cambiarán, así que probablemente no importará, pero si tienes algún código que cambie deliberadamente de modo estas secciones de interrupción podrían causar problemas. Cambiar entre el modo privilegiado y el modo de usuario es el único caso de hacer esto que yo esperaría.

En tercer lugar, no hay nada que impida que una interrupción cambie otras partes de CPSR entre el MRS y MSR en ARM_INT_LOCK . Cualquier cambio de este tipo podría ser sobrescrito. En la mayoría de los sistemas razonables, las interrupciones asíncronas no cambian el estado del código que interrumpen (incluyendo CPSR). Si lo hacen, se hace muy difícil razonar sobre lo que hará el código. Sin embargo, es posible (cambiar el bit de desactivación de FIQ me parece lo más probable), así que deberías considerar si tu sistema hace esto.

Así es como yo los implementaría de manera que se resuelvan todos los problemas potenciales que señalé:

#define ARM_INT_KEY_TYPE            unsigned int
#define ARM_INT_LOCK(key_)   \
asm volatile(\
    "mrs %[key], cpsr\n\t"\
    "ands %[key], %[key], #0xC0\n\t"\
    "cpsid if\n\t" : [key]"=r"(key_) :: "memory", "cc" );
#define ARM_INT_UNLOCK(key_) asm volatile (\
    "tst %[key], #0x40\n\t"\
    "beq 0f\n\t"\
    "cpsie f\n\t"\
    "0: tst %[key], #0x80\n\t"\
    "beq 1f\n\t"\
    "cpsie i\n\t"
    "1:\n\t" :: [key]"r" (key_) : "memory", "cc")

Asegúrese de compilar con -mcpu=cortex-a9 porque al menos algunas versiones de GCC (como la mía) tienen por defecto una CPU ARM más antigua que no soporta cpsie y cpsid .

Utilicé ands en lugar de sólo and en ARM_INT_LOCK por lo que es una instrucción de 16 bits si se utiliza en código Thumb. El "cc" El clobber es necesario de todos modos, así que es estrictamente un beneficio de rendimiento/tamaño de código.

0 y 1 son etiquetas locales para que sirva de referencia.

Estos deben ser utilizables de la misma manera que sus versiones. El ARM_INT_LOCK es tan rápido/pequeño como el original. Desafortunadamente, no pude encontrar una manera de hacer ARM_INT_UNLOCK con seguridad en cualquier lugar cerca de las instrucciones.

Si su sistema tiene restricciones en cuanto a cuándo se desactivan las IRQs y FIQs, esto podría simplificarse. Por ejemplo, si siempre están deshabilitadas juntas, podrías combinarlas en una sola cbz + cpsie if así:

#define ARM_INT_UNLOCK(key_) asm volatile (\
    "cbz %[key], 0f\n\t"\
    "cpsie if\n\t"\
    "0:\n\t" :: [key]"r" (key_) : "memory", "cc")

Alternativamente, si no te importan los FIQs en absoluto, es similar a dejar de habilitarlos/deshabilitarlos completamente.

Si sabes que nada más cambia ninguno de los otros bits de estado en CPSR entre el bloqueo y el desbloqueo, entonces también podrías usar continue con algo muy similar a tu código original, excepto con ambos "memory" y "cc" de la que se benefician los dos. ARM_INT_LOCK y ARM_INT_UNLOCK

1voto

John Spitz Puntos 29

Para secciones críticas relativamente sencillas, puede utilizar las instrucciones LDREX y STREX.

https://stackoverflow.com/questions/51795537/critical-sections-in-arm http://infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dui0204f/Cihbghef.html

i-Ciencias.com

I-Ciencias es una comunidad de estudiantes y amantes de la ciencia en la que puedes resolver tus problemas y dudas.
Puedes consultar las preguntas de otros usuarios, hacer tus propias preguntas o resolver las de los demás.

Powered by:

X