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

15

Estoy transfiriendo 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 una falla que parece estar relacionada con una condición de carrera que debería evitarse mediante una sección crítica del código.

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

Además, ¿hay una 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 usa 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 "clave" es permitir secciones críticas anidadas, y estas se utilizan al principio y al final de las funciones para crear funciones reentrantes.

¡Gracias!

CodePoet
fuente
1
consulte infocenter.arm.com/help/index.jsp?topic=/com.arm.doc.dht0008a/… no lo haga en asm incrustado por cierto. haz que funcione como lo hace el artículo.
Jason Hu
No sé nada acerca de ARM, pero esperaría que para mutex (o cualquier función de sincronización entre hilos o procesos cruzados), debería usar el clobber de "memoria" para asegurarse de que a) todos los valores de memoria actualmente almacenados en caché en los registros se vacíen volver a la memoria antes de ejecutar el asm yb) cualquier valor en la memoria al que se acceda después de que el asm se vuelva a cargar. Tenga en cuenta que realizar una llamada (como recomienda HuStmpHrrr) debe realizar implícitamente este clobber por usted.
Además, aunque todavía no hablo ARM, sus restricciones para 'key_' no parecen correctas. Como usted dice que esto está destinado a ser utilizado para volver a entrar, declararlo como "= r" en la cerradura parece sospechoso. '=' significa que tiene la intención de sobrescribirlo, y el valor existente no es importante. Parece más probable que tenga la intención de usar '+' para indicar su intención de actualizar el valor existente. Y nuevamente para desbloquear, enumerarlo como entrada le dice a gcc que no tiene intención de cambiarlo, pero si no me equivoco, lo hace (cámbielo). Supongo que esto también debería aparecer como una salida '+'.
1
+1 para codificación en ensamblaje para un núcleo de tan alta especificación De todos modos, ¿podría esto estar relacionado con los modos de privilegio?
Dzarda
Estoy bastante seguro de que necesitará usarlo ldrexy strexhacerlo correctamente. Aquí hay una página web que le muestra cómo usar ldrexe streximplementar un spinlock.

Respuestas:

14

La parte más difícil de manejar una sección crítica sin un sistema operativo no es realmente crear el mutex, sino descubrir qué debería suceder si el código quiere usar un recurso que actualmente no está disponible. Las instrucciones de exclusiva de carga y exclusiva de tienda condicional hacen que sea bastante fácil crear una función de "intercambio" que, dado un puntero a un entero, almacenará atómicamente un nuevo valor pero devolverá lo que el entero apuntado había contenido:

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 ingresar fácilmente un mutex a través de 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 a menudo radica en el código "no se pudo obtener mutex". Si se produce una interrupción cuando un recurso protegido por mutex está ocupado, puede ser necesario que el código de manejo de interrupciones establezca un indicador y guarde cierta información para indicar lo que quería hacer, y luego tener un código similar que adquiera el código compruebe el mutex cada vez que va a liberar el mutex para ver si una interrupción quería hacer algo mientras se mantenía el mutex y, de ser así, realizar la acción en nombre de la interrupción.

Aunque es posible evitar problemas con las interrupciones que desean 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 por más tiempo del necesario.

Un compromiso útil puede ser usar un indicador como se describió anteriormente, pero tener el código de la línea principal que va a liberar las interrupciones de desactivación de mutex y verificar el indicador antes de hacerlo (volver a habilitar las interrupciones después de liberar el mutex). Tal enfoque no requiere dejar las interrupciones desactivadas por mucho tiempo, pero protegerá contra la posibilidad de que si el código de la línea principal prueba 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 él, puede ser reemplazado por otro código que adquiere y libera el mutex y actúa sobre el indicador de interrupción; si el código de la línea principal no prueba la bandera de la interrupción después de liberar el mutex,

En cualquier caso, lo más importante será tener un medio por el cual el código que intenta usar un recurso protegido por mutex cuando no está disponible tendrá un medio de repetir su intento una vez que se libera el recurso.

Super gato
fuente
7

Esta es una forma dura de hacer secciones críticas; deshabilitar interrupciones. Es posible que no funcione si su sistema tiene / maneja fallas de datos. También aumentará la latencia de interrupción. El irqflags.h Linux tiene algunas macros que se encargan de esto. Las instrucciones cpsiey cpsidpueden ser útiles; Sin embargo, no guardan el estado y no permitirán el anidamiento. cpsNo utiliza un registro.

Para la serie Cortex-A , ldrex/strexson más eficientes y pueden funcionar para formar un mutex para la sección crítica o pueden usarse con algoritmos sin bloqueo para deshacerse de la sección crítica.

En cierto sentido, ldrex/strexparece un ARMv5 swp. Sin embargo, son mucho más complejos de implementar en la práctica. Necesita una memoria caché que funcione y la memoria de destino de las ldrex/strexnecesidades debe estar en la memoria caché. La documentación de ARM en el ldrex/strexes bastante nebulosa, ya que quieren mecanismos para trabajar en CPU que no sean Cortex-A. Sin embargo, para el Cortex-A, el mecanismo para mantener la caché local de la CPU sincronizada con otras CPU es el mismo que se utiliza para implementar las ldrex/strexinstrucciones. Para la serie Cortex-A, la reserva granual (tamaño de la ldrex/strexmemoria reservada) es la misma que una línea de caché; También debe alinear la memoria con la línea de caché si tiene la intención de modificar varios valores, como con una lista doblemente vinculada.

Sospecho que hay algún error sutil.

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

Debe asegurarse de que la secuencia nunca se pueda adelantar . De lo contrario, puede obtener dos variables clave con interrupciones habilitadas y la liberación del bloqueo será incorrecta. Puede usar la swpinstrucción con la memoria de clave para garantizar la coherencia en el ARMv5, pero esta instrucción está en desuso en el Cortex-A a favor, ldrex/strexya que funciona mejor para los sistemas con múltiples CPU.

Todo esto depende de qué tipo de programación tenga su sistema. Parece que solo tienes líneas principales e interrupciones. A menudo necesita las primitivas de la sección crítica para tener algunos enganches al planificador dependiendo de los niveles (sistema / espacio de usuario / etc.) con los que desea que funcione la sección crítica.

Además, ¿hay una 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 CPU ARM y para sistemas operativos específicos.

ruido sin arte
fuente
2

Veo varios problemas potenciales con esas secciones críticas. Hay advertencias y soluciones a todos estos, pero como resumen:

  • No hay nada que impida que el compilador mueva el código a través de estas macros, por optimización u otras razones aleatorias.
  • Guardan y restauran algunas partes del estado del procesador que el compilador espera que el ensamblaje en línea deje solo (a menos que se indique lo contrario).
  • No hay nada que evite que ocurra una interrupción en el medio de la secuencia y que cambie el estado entre cuando se lee y cuando se escribe.

En primer lugar, definitivamente necesita algunas barreras de memoria del compilador . GCC implementa estos como clobbers . Básicamente, esta es una manera de decirle al compilador "No, no puede mover los accesos a la memoria a través de este ensamblaje en línea porque podría afectar el resultado de los accesos a la memoria". Específicamente, necesita ambos "memory"y "cc"clobbers, tanto en las macros de inicio como de fin. Esto evitará que otras cosas (como llamadas a funciones) se reordenen también en relación con el ensamblado en línea, porque el compilador sabe que pueden tener accesos a la memoria. He visto GCC para el estado de retención ARM en registros de códigos de condición a través del ensamblaje en línea con "memory"clobbers, por lo que definitivamente necesita el "cc"clobber.

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

Entre otras cosas, esto incluye los códigos de condición (donde cmpse almacenan los resultados de instrucciones como para que las instrucciones condicionales posteriores puedan actuar sobre el resultado). El compilador definitivamente se confundirá con esto. Esto se puede solucionar fácilmente utilizando el "cc"clobber como se mencionó anteriormente. Sin embargo, esto hará que el código falle cada vez, por lo que no suena como lo que está teniendo problemas. Sin embargo, es una bomba de tiempo, ya que modificar otro código aleatorio podría hacer que el compilador haga algo un poco diferente que se romperá con esto.

Esto también intentará guardar / restaurar los bits de TI, que se utilizan para implementar la ejecución condicional de Thumb . Tenga en cuenta que si nunca ejecuta el código Thumb, esto no importa. Nunca he descubierto cómo el ensamblaje en línea de GCC trata con los bits de TI, aparte de concluir que no lo hace, lo que significa que el compilador nunca debe colocar el ensamblaje en línea en un bloque de TI y siempre espera que el ensamblaje termine fuera de un bloque de TI. Nunca he visto a GCC generar código que viole estas suposiciones, y he realizado un ensamblaje en línea bastante complejo con una gran optimización, por lo 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 se clasifica como "arquitectónicamente impredecible", por lo 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á / restaurará (además de los que realmente deshabilitarán las interrupciones) son los bits de modo. Estos probablemente no cambiarán, por lo que probablemente no importará, pero si tiene algún código que cambie deliberadamente los modos, estas secciones de interrupción podrían causar problemas. Cambiar entre los modos privilegiado y de usuario es el único caso de hacer esto que esperaría.

En tercer lugar, no hay nada que impida que una interrupción cambie otras partes de CPSR entre MRSy MSRdentro ARM_INT_LOCK. Cualquiera de estos cambios podría sobrescribirse. En la mayoría de los sistemas razonables, las interrupciones asincrónicas no cambian el estado del código que están interrumpiendo (incluido 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 más probable), por lo que debe considerar si su sistema lo hace.

Así es como los implementaría de una manera que aborde 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 -mcpu=cortex-a9porque al menos algunas versiones de GCC (como la mía) tienen por defecto una CPU ARM más antigua que no admite cpsiey cpsid.

Usé en andslugar de solo anden, ARM_INT_LOCKasí que es una instrucción de 16 bits si se usa en el código Thumb. El "cc"clobber es necesario de todos modos, por lo que es estrictamente un beneficio de rendimiento / tamaño del código.

0y 1son etiquetas locales , para referencia.

Deben ser utilizables de la misma manera que sus versiones. El ARM_INT_LOCKes tan rápido / pequeño como el original. Desafortunadamente, no pude encontrar una manera de hacerlo de ARM_INT_UNLOCKmanera segura en tan solo unas pocas instrucciones.

Si su sistema tiene restricciones cuando IRQ y FIQ están deshabilitados, esto podría simplificarse. Por ejemplo, si siempre están deshabilitados juntos, puede combinarlos en uno cbz+ cpsie ifasí:

#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 le importan los FIQ, entonces es similar a simplemente dejar de habilitarlos / deshabilitarlos por completo.

Si sabe que nada más cambia ninguno de los otros bits de estado en CPSR entre el bloqueo y el desbloqueo, entonces también puede usar continuar con algo muy similar a su código original, excepto con ambos "memory"y "cc"clobbers en ambos ARM_INT_LOCKyARM_INT_UNLOCK

Brian Silverman
fuente