La respuesta a esto para C y C ++ es equivalente, por cierto: para evitar condiciones de carrera (a menudo llamadas “carreras de datos”).
Tenga en cuenta que la “programación del sistema integrado” es algo ortogonal a esto: lo que importa es la concurrencia / paralelismo. Sin embargo, muchos sistemas embebidos más pequeños no implican nada de eso y, para ellos, las condiciones de carrera o el uso de tipos atómicos simplemente no son un problema.
Veamos esto con cierto detalle, comenzando con el código para agregar en un int
simple almacenado en la memoria:
- ¿Es importante actualizar el firmware?
- ¿Cuál es el mejor instituto para capacitación en sistemas integrados en Bangalore con buenas ubicaciones?
- ¿Cómo pensar en C?
- ¿Dónde debo comenzar a aprender las pruebas unitarias para software embebido?
- ¿Qué puedo hacer si conozco C y 8051 integrados y electrónica?
void add_one (int * p) {
* p + = 1;
}
Compilaré esto con gcc para ARM64 con optimizaciones habilitadas y miraré la salida del ensamblaje. Puedes probar esto usando el explorador de compiladores de Matt Godbolt. La parte relevante se ve así:
Agrega uno:
ldr w1, [x0]
agregar w1, w1, 1
str w1, [x0]
jubilado
(Uso ARM como objetivo porque el problema es más claramente visible en el código de ensamblaje. El mismo problema existe en x86–64, pero tendría que profundizar en los detalles de la addl
porque realiza la operación con un solo addl
instrucción que es bastante opaca.)
Como puede ver, la adición se produce en tres pasos: (1) cargue los datos de la memoria en un registro, (2) incremente el registro y (3) almacene el registro nuevamente en la memoria.
Eso está perfectamente bien … hasta que decida ejecutar múltiples procesos simultáneamente con acceso a los mismos datos. Supongamos que en un punto el proceso A y B ejecutan el código anterior para el mismo valor de p
que apunta a un int
de valor 40 ,
con el proceso A solo una instrucción por delante del proceso B:
- El proceso A carga el valor 40 en su registro
- El proceso B carga el valor 40 en su registro
- Al mismo tiempo, el proceso A actualiza su registro a 41
- El proceso B actualiza su registro a 41
- Al mismo tiempo, el proceso A guarda 41 en
*p
- El proceso B guarda 41 en
*p
Es decir, aunque hubo dos llamadas a add_one
para un *p
que estaba en 40, el resultado final es *p
solo 41 (en lugar del 42 “correcto”). Esta es una “condición de carrera” típica, y puede abordarse con tipos atómicos.
Las condiciones de carrera pueden ocurrir con múltiples subprocesos que se ejecutan en un solo procesador y el sistema operativo puede suspender un proceso en cualquier momento y transferir el control a otro, o, más radicalmente, con múltiples procesos que se ejecutan en paralelo en múltiples núcleos. Ambos escenarios arrojan una llave inglesa en el código anterior, pero el último puede ser aún más complicado.
Ahora, déjame cambiar el código anterior para usar un tipo atómico. En C11 eso da:
vacío add_one (_Atomic int * p) {
* p + = 1;
}
o lo mismo en C ++:
void add_one (std :: atomic * p) {
* p + = 1;
}
El código generado es el mismo para C y C ++, pero diferente del original:
add_one (std :: atomic *):
.L2:
ldaxr w1, [x0]
agregar w1, w1, 1
stlxr w2, w1, [x0]
cbnz w2, .L2
jubilado
ARM utiliza un enfoque denominado “cargar-adquirir / almacenar-liberar” para las actualizaciones atómicas: la instrucción ldaxr
no solo carga el contenido de la dirección x0
(es decir, *p
) en el registro w1
, sino que también configura un “monitor” en la memoria que contiene *p
: Si algo intenta modificar esa memoria, el “monitor” levantará una bandera. La instrucción stlxr
intenta almacenar el valor de w1
en *p
(es decir, [x0]
), pero también consulta el monitor configurado con la instrucción ldaxr
y si se ldaxr
su ldaxr
, se ldaxr
el almacenamiento real y w2
será establecido en 1. Si la tienda tiene éxito, w2
se establece en cero. De cualquier manera, se libera el monitor. cbnz
es “rama condicional si no es cero”: por lo tanto, si la instrucción stlxr
cancela, se reinicia toda la secuencia.
Si trabaja a través de esa secuencia, se dará cuenta de que el código anterior garantiza que la “carga” y la “tienda” parecen ocurrir atómicamente. En este caso, la instrucción stlxr
también es en realidad una barrera de memoria , que garantiza que las operaciones atómicas en diferentes ubicaciones de memoria son secuencialmente consistentes, pero eso está un poco más allá de esta respuesta (una instrucción stxr
más relajada es un poco más eficiente si la atomicidad local es todo necesita; hay formas de generarlo usando C ++ estándar a través de las opciones de “orden de memoria” en el encabezado ). También es interesante notar que el “monitor” mencionado anteriormente no es súper preciso: los almacenes en la memoria que está cerca de
*p
también harán que stlxr
falle. Tales fallas espurias no afectan la corrección, pero disminuyen el rendimiento.
En x86–64 encontrará un enfoque diferente para el problema (usando modificadores de LOCK
), pero bajo el capó (es decir, a nivel de hardware) están involucradas técnicas algo similares.