Los novatos necesitan consejos sobre cómo escribir operaciones atómicas en LINUX
Acerca de las operaciones atómicas en Linux
02 de agosto de 2016
Operaciones atómicas: Es realizar una operación sin ser interrumpido.
Los problemas de funcionamiento atómico de Linux provienen de interrupciones, apropiación de procesos y ejecución concurrente de programas en sistemas SMP multinúcleo.
Las operaciones en secciones críticas se pueden bloquear para garantizar la atomicidad, mientras que las operaciones en variables globales o variables estáticas requieren operaciones de variables atómicas que dependen de la plataforma de hardware.
Por lo tanto, hay dos tipos de operaciones atómicas: una son bloqueos en varias secciones críticas y la otra son funciones que operan variables atómicas.
Para arm, una sola instrucción de ensamblaje es atómica, al igual que SMP multinúcleo. Debido a que hay arbitraje de bus, la CPU puede ocupar el bus sola hasta el final de la instrucción atómica. Los sistemas generalmente usan barreras de memoria (barrera de memoria), es decir, cuando un núcleo de CPU realiza una operación atómica, otros núcleos de CPU deben dejar de funcionar en la memoria o no funcionar en la memoria especificada, para evitar problemas de competencia de datos. Sin embargo, el proceso de carga del almacén de actualizaciones puede interrumpirse y adelantarse, por lo que el conjunto de instrucciones arm ha agregado instrucciones atómicas como ldrex/strex para implementar la carga del almacén de actualizaciones.
Sin embargo, para los programas Linux C/C++ (un C compilado en múltiples líneas de ensamblaje), la atomicidad no se puede garantizar debido a las razones mencionadas anteriormente, por lo que Linux proporciona un conjunto de funciones para operar variables globales o variables estáticas. . 1. Las operaciones atómicas enteras se definen en #include
void atomic_set(atomic_t *v,int i); //Establece el valor de la variable atómica v en iatomic_t v = ATOMIC_INIT(0); //Define la variable atómica v e inicializala en 0; Retornar El valor de la variable atómica v;void atomic_add(int i, atomic_t* v); //La variable atómica v aumenta en i;void atomic_sub(int i, atomic_t* v); la variable aumenta en 1 ;void atomic_dec(atomic_t* v);?int atomic_inc_and_test(atomic_t* v); //Primero incrementa en 1 y luego prueba si el valor es 0; si es 0, devuelve verdadero; de lo contrario, devuelve falso; int atomic_dec_and_test(atomic_t* v); int atomic_sub_and_test(int i, atomic_t* v); // Primero disminuye i y luego prueba si su valor es 0. Si es 0, devuelve verdadero; de lo contrario, devuelve falso nota: solo self; -incremento, sin operación de suma int atomic_add_return(int i, atomic_t* v); //Agrega i al valor de v y devuelve el nuevo valor; int atomic_sub_return(int i, atomic_t* v); v); //El valor de v es de Devuelve el nuevo valor después de incrementarlo en 1; int atomic_dec_return(atomic_t* v); , borrar, cambiar y probar void set_bit(int nr, volatile void* addr); // Establecer el bit nr-ésimo de la dirección addr. El llamado bit de configuración es escribir el bit en 1; volatile void* addr); ?//Borra el bit número n de la dirección addr, el llamado bit de borrado, es decir, escribe el bit como 0; void change_bit(int nr, volatile void* addr); el enésimo bit de la dirección addr; int test_bit(int nr, volatile void* addr); // Devuelve la dirección addr nr bit; int test_and_set_bit(int nr, volatile void* addr); // Prueba y configura el bit; el bit nr de addr no es 0, devuelve verdadero; si el bit nr de addr es 0, devuelve false; int test_and_clear_bit(int nr, volatile void* addr);// Prueba y borra el bit; volatile void* addr);// Pruebe e invierta el bit; la operación anterior equivale a ejecutar test_bit(nr, voidaddr) primero Luego ejecute xxx_bit(nr, voidaddr)
Un ejemplo simple: para tenga en cuenta que el dispositivo solo puede ser abierto mediante un proceso, evitando así la aparición de condiciones de carrera static atomic_t scull_available = ATOMIC_INIT(1);?//init Atomic está en la función scull_open y scull_close: int scull_open(struct inode *inode, archivo de estructura *filp){
struct scull_dev *dev; // información del dispositivo dev = container_of(inode->i_cdev, struct scull_dev, cdev; filp->private_data = dev;?// para otros métodos if(!atomic_dec_and_test(&scull_available)){ atomic_inc(&scull_available) ; return -EBUSY } return 0; // éxito?}int scull_release(struct inode *inode, struct file *filp){ atomic_inc(&scull_available return 0;}
Asume la implementación subyacente de atomic; variables Se implementa mediante una instrucción de ensamblaje y esta atomicidad debe garantizarse. Pero si la implementación de variables atómicas se compone de múltiples instrucciones, ¿habrá algún impacto en la intervención de SMP y las interrupciones? Cuando observé la implementación de las operaciones de variables atómicas de ARM, descubrí que se implementa mediante múltiples instrucciones de ensamblaje (ldrex/strex). Después de consultar otros libros y materiales, descubrí que las descripciones de estas dos instrucciones en la mayoría de los libros decían que admiten el acceso mutuamente excluyente a la memoria compartida de múltiples núcleos en sistemas SMP. Pero cuando se usa en el sistema UP, si ocurre una interrupción entre ldrex/strex y la misma variable atómica también es operada por ldrex/strex durante la interrupción, ¿habrá algún problema? Con respecto a este tema, miré detenidamente el código fuente de la variable atómica ARM del kernel y la explicación oficial de ARM de las funciones de ldrex/strex. El resumen es el siguiente:
1.
El código fuente de implementación de la variable atómica para la arquitectura ARM se encuentra en: arch/arm/include/asm/atomic.h
El código de implementación principal se divide en la implementación de la arquitectura por encima de ARMv6 (incluido v6) y la implementación de la versión ARMv6 por debajo de la realización.
La estructura principal del archivo es la siguiente:
#if?__LINUX_ARM_ARCH__?>=?6
......(a través del ldrex /implementación del ensamblado del comando strex)
#else?/*?ARM_ARCH_6?*/
#ifdef CONFIG_SMP
#error?SMP?not?supported?on ?CPU anteriores a ARMv6
#endif
......(implementado desactivando las interrupciones de la CPU en lenguaje C)
#endif?/* ?__LINUX_ARM_ARCH__?* /
......?
#ifndef CONFIG_GENERIC_ATOMIC64
......(átomos de 64 bits implementados mediante el ensamblaje de ldrexd /strexd instrucciones Acceso a variables)
#else?/*?!CONFIG_GENERIC_ATOMIC64?*/
#include?
#endif
#include?
Esta disposición se basa en la implementación de la versión del conjunto de instrucciones principales de ARM:
(1) En la arquitectura ARMv6 y superior (incluida la v6), hay CPU de múltiples núcleos. Para sincronizar datos y controlar la concurrencia entre múltiples núcleos, ARM agrega un mecanismo de monitores exclusivo (una máquina de estado simple) al acceso a la memoria. ) y agregó instrucciones ldrex/strex relevantes. Lea primero los siguientes materiales de referencia (la clave es comprender el monitor local y el monitor global):
1.2.2.?Monitores exclusivos
4.2.12.?LDREX?y? STREX
p>(2) Para arquitecturas anteriores a ARMv6, es imposible tener CPU de múltiples núcleos, por lo que para el acceso atómico a las variables, solo necesita desactivar la interrupción de la CPU para garantizar la atomicidad. ?
Para (2), es muy fácil de entender.
Pero en el caso (1), todavía tengo que analizar el código fuente antes de estar de acuerdo con este código. A continuación solo analizaré el código fuente atomic_add más representativo. Los principios de otras API son los mismos. . Si el lector no está familiarizado con el formato del ensamblado integrado en C, consulte "ARM GCC? Manual de ensamblaje integrado"
2 Análisis del código fuente atomic_add del kernel de la arquitectura ARM
/. *
*?ARMv6 UP y operaciones atómicas seguras SMP. Utilizamos carga exclusiva y
*?almacenamiento exclusivo para garantizar la atomicidad de estas operaciones. Podemos recorrer
*? para asegurarnos de que las variables se actualicen correctamente.
*/
static inline void atomic_add(int?i,?atomic_t?*v)
{
tmp largo sin firmar;
int?resultado;
__asm__ __volatile__("@ atomic_add\n"
"1: ldrex %0, [%3]\n" p>
" agregar %0, %0, %4\n"
" strex %1, %0, [%3]\n"
" teq % 1, #0\n"
" bne 1b"
:?"=&r"?(resultado),?"=&r"?(tmp),?"+Qo "?(v->contador)
:?"r"?(&v->contador),?"Ir"?(i)
:?"cc");
}
Análisis del código fuente:?
Nota: Según la sintaxis del ensamblado en línea, los datos correspondientes al resultado, tmp y &v->counter son colocado en Operar en registro. Si se produce un cambio de contexto, el mecanismo de conmutación realizará la protección del contexto del registro.
(1) ldrex %0, [%3]
Significa colocar los datos señalados por &v->counter en el resultado y (en el monitor local y en el monitor global monitor respectivamente) establece la bandera exclusiva.
(2) agregar %0, %0, %4
resultado = resultado + i
(3) strex %1, %0, [% 3]
Significa guardar el resultado en la memoria señalada por &v->counter. En este momento, los monitores exclusivos entrarán en juego y pondrán la bandera de si el guardado se realizó correctamente en tmp.
(4)?teq %1, #0
Prueba si strex tiene éxito (tmp == 0?)
(5) bne 1b p >
Si se descubre que Strex ha fallado, ejecute nuevamente desde (1).
A través del análisis anterior, podemos ver que la clave está en juzgar si la operación Strex es exitosa. Esto se debe al mecanismo de los monitores exclusivos de ARM y a las instrucciones ldrex/strex. A continuación se analiza el mecanismo de instrucción ldrex/strex a través de posibles situaciones. (Consulte 4.2.12. LDREX y STREX al leer)
1. En el sistema UP o SMP, ¿las variables no se comparten entre las CPU?
En este caso, sólo una CPU puede acceder a la variable y sólo el monitor local necesita atención.
Supongamos que cuando la CPU ejecuta (2), llega una interrupción y la misma variable atómica se opera usando ldrex/strex en la interrupción. La situación es como se muestra en la siguiente figura:
A: El procesador marca una dirección física, pero el acceso aún no se ha completado
B: El procesador vuelve a marcar la dirección física y el acceso aún no se ha completado (duplicar con A)
C: realiza la operación de almacenamiento, borra la marca anterior y devuelve 0 (operación exitosa)
D: no hay operación de almacenamiento ¿Se realizará y devolverá 1 (operación fallida)?
En otras palabras, la operación en la rutina de interrupción tendrá éxito y la operación interrumpida fallará y se volverá a intentar. ?
2. Las variables en el sistema SMP tienen acceso compartido entre CPU.
En este caso, se requiere acceso mutuamente exclusivo entre las dos CPU. En este caso, ldrex/strex. La instrucción prestará atención tanto al monitor local como al monitor global.
(i) Dos CPU acceden a la misma variable atómica al mismo tiempo (las instrucciones ldrex/strex prestarán atención al monitor global).
A: Marque la dirección física como CPU0 acceso exclusivo y borre cualquier indicador de acceso exclusivo de CPU0 a cualquier otra dirección física.
B: marque esta dirección física para acceso exclusivo por parte de CPU1 y borre cualquier indicador de acceso exclusivo por parte de CPU1 para cualquier otra dirección física.
C: No marcado para acceso exclusivo de CPU0, no se almacenará y devuelve 1 (operación fallida).
D: Se ha marcado como acceso exclusivo a CPU1, almacena y borra la marca de acceso exclusivo y devuelve 0 (operación exitosa).
En otras palabras, la CPU que realiza la operación ldrex tendrá éxito.
(ii) La misma CPU "anidada" accede a la misma variable atómica debido a interrupciones (las instrucciones ldrex/strex se centrarán en el monitor local)
R: Marque la dirección física como CPU0 tiene acceso exclusivo y borra cualquier indicador de acceso exclusivo de CPU0 a cualquier otra dirección física.
B: marque esta dirección física para acceso exclusivo por parte de CPU0 nuevamente y borre cualquier marca de acceso exclusivo por parte de CPU0 para cualquier otra dirección física.
C: Se ha marcado como acceso exclusivo CPU0, almacena y borra la marca de acceso exclusivo y devuelve 0 (operación exitosa).
D: No está marcado para acceso exclusivo de CPU0, no se almacenará y devuelve 1 (operación fallida).
En otras palabras, la operación en la rutina de interrupción tendrá éxito y la operación interrumpida fallará y se volverá a intentar.
(iii) Dos CPU acceden a la misma variable atómica al mismo tiempo, y al mismo tiempo, una CPU accede y cambia la variable atómica debido al "anidamiento" de interrupción (la instrucción ldrex/strex pagará atención tanto al monitor local como al monitor global)
p>Aunque para los humanos, esta situación es relativamente BT. Pero con una CPU de funcionamiento rápido, cosas como BT pueden suceder en cualquier momento.
R: Marque esta dirección física para acceso exclusivo por parte de CPU0 y borre cualquier marca de acceso exclusivo por parte de CPU0 para cualquier otra dirección física.
B: marque esta dirección física para acceso exclusivo por parte de CPU1 y borre cualquier indicador de acceso exclusivo por parte de CPU1 para cualquier otra dirección física.
C: marque esta dirección física nuevamente para acceso exclusivo por parte de CPU0 y borre cualquier marca de acceso exclusivo por parte de CPU0 para cualquier otra dirección física.
D: Se ha marcado como acceso exclusivo CPU0, almacena y borra la marca de acceso exclusivo y devuelve 0 (operación exitosa).
E: No está marcado para acceso exclusivo de CPU1, no se almacenará y devuelve 1 (operación fallida).
F: No marcado para acceso exclusivo de CPU0, no se almacenará y devuelve 1 (operación fallida).
Por supuesto, existen muchas otras posibilidades complejas, que también pueden analizarse mediante el mecanismo de las instrucciones ldrex/strex. Del análisis enumerado anteriormente, podemos ver que ldrex/strex puede garantizar la atomicidad del acceso bajo cualquier circunstancia (incluida la interrupción). Por lo tanto, se puede confiar en las operaciones atómicas en la arquitectura ARM del kernel.