3135 lines
124 KiB
Plaintext
3135 lines
124 KiB
Plaintext
|
NOTE:
|
|||
|
This is a version of Documentation/memory-barriers.txt translated into
|
|||
|
Spanish by Carlos Bilbao <carlos.bilbao@amd.com>. If you find any
|
|||
|
difference between this document and the original file or a problem with
|
|||
|
the translation, please contact the maintainer of this file. Please also
|
|||
|
note that the purpose of this file is to be easier to read for non English
|
|||
|
(read: Spanish) speakers and is not intended as a fork. So if you have any
|
|||
|
comments or updates for this file please update the original English file
|
|||
|
first. The English version is definitive, and readers should look there if
|
|||
|
they have any doubt.
|
|||
|
|
|||
|
======================================
|
|||
|
BARRERAS DE MEMORIA EN EL KERNEL LINUX
|
|||
|
======================================
|
|||
|
|
|||
|
Documento original: David Howells <dhowells@redhat.com>
|
|||
|
Paul E. McKenney <paulmck@linux.ibm.com>
|
|||
|
Will Deacon <will.deacon@arm.com>
|
|||
|
Peter Zijlstra <peterz@infradead.org>
|
|||
|
|
|||
|
Traducido por: Carlos Bilbao <carlos.bilbao@amd.com>
|
|||
|
Nota: Si tiene alguna duda sobre la exactitud del contenido de esta
|
|||
|
traducción, la única referencia válida es la documentación oficial en
|
|||
|
inglés.
|
|||
|
|
|||
|
===========
|
|||
|
ADVERTENCIA
|
|||
|
===========
|
|||
|
|
|||
|
Este documento no es una especificación; es intencionalmente (por motivos
|
|||
|
de brevedad) y sin querer (por ser humanos) incompleta. Este documento
|
|||
|
pretende ser una guía para usar las diversas barreras de memoria
|
|||
|
proporcionadas por Linux, pero ante cualquier duda (y hay muchas) por favor
|
|||
|
pregunte. Algunas dudas pueden ser resueltas refiriéndose al modelo de
|
|||
|
consistencia de memoria formal y documentación en tools/memory-model/. Sin
|
|||
|
embargo, incluso este modelo debe ser visto como la opinión colectiva de
|
|||
|
sus maintainers en lugar de que como un oráculo infalible.
|
|||
|
|
|||
|
De nuevo, este documento no es una especificación de lo que Linux espera
|
|||
|
del hardware.
|
|||
|
|
|||
|
El propósito de este documento es doble:
|
|||
|
|
|||
|
(1) especificar la funcionalidad mínima en la que se puede confiar para
|
|||
|
cualquier barrera en concreto, y
|
|||
|
|
|||
|
(2) proporcionar una guía sobre cómo utilizar las barreras disponibles.
|
|||
|
|
|||
|
Tenga en cuenta que una arquitectura puede proporcionar más que el
|
|||
|
requisito mínimo para cualquier barrera en particular, pero si la
|
|||
|
arquitectura proporciona menos de eso, dicha arquitectura es incorrecta.
|
|||
|
|
|||
|
Tenga en cuenta también que es posible que una barrera no valga (sea no-op)
|
|||
|
para alguna arquitectura porque por la forma en que funcione dicha
|
|||
|
arquitectura, la barrera explícita resulte innecesaria en ese caso.
|
|||
|
|
|||
|
==========
|
|||
|
CONTENIDOS
|
|||
|
==========
|
|||
|
|
|||
|
(*) Modelo abstracto de acceso a memoria.
|
|||
|
|
|||
|
- Operaciones del dispositivo.
|
|||
|
- Garantías.
|
|||
|
|
|||
|
(*) ¿Qué son las barreras de memoria?
|
|||
|
|
|||
|
- Variedades de barrera de memoria.
|
|||
|
- ¿Qué no se puede asumir sobre las barreras de memoria?
|
|||
|
- Barreras de dirección-dependencia (históricas).
|
|||
|
- Dependencias de control.
|
|||
|
- Emparejamiento de barreras smp.
|
|||
|
- Ejemplos de secuencias de barrera de memoria.
|
|||
|
- Barreras de memoria de lectura frente a especulación de carga.
|
|||
|
- Atomicidad multicopia.
|
|||
|
|
|||
|
(*) Barreras explícitas del kernel.
|
|||
|
|
|||
|
- Barrera del compilador.
|
|||
|
- Barreras de memoria de la CPU.
|
|||
|
|
|||
|
(*) Barreras de memoria implícitas del kernel.
|
|||
|
|
|||
|
- Funciones de adquisición de cerrojo.
|
|||
|
- Funciones de desactivación de interrupciones.
|
|||
|
- Funciones de dormir y despertar.
|
|||
|
- Funciones varias.
|
|||
|
|
|||
|
(*) Efectos de barrera adquiriendo intra-CPU.
|
|||
|
|
|||
|
- Adquisición vs accesos a memoria.
|
|||
|
|
|||
|
(*) ¿Dónde se necesitan barreras de memoria?
|
|||
|
|
|||
|
- Interacción entre procesadores.
|
|||
|
- Operaciones atómicas.
|
|||
|
- Acceso a dispositivos.
|
|||
|
- Interrupciones.
|
|||
|
|
|||
|
(*) Efectos de barrera de E/S del kernel.
|
|||
|
|
|||
|
(*) Modelo de orden mínimo de ejecución asumido.
|
|||
|
|
|||
|
(*) Efectos de la memoria caché de la CPU.
|
|||
|
|
|||
|
- Coherencia de caché.
|
|||
|
- Coherencia de caché frente a DMA.
|
|||
|
- Coherencia de caché frente a MMIO.
|
|||
|
|
|||
|
(*) Cosas que hacen las CPU.
|
|||
|
|
|||
|
- Y luego está el Alfa.
|
|||
|
- Guests de máquinas virtuales.
|
|||
|
|
|||
|
(*) Ejemplos de usos.
|
|||
|
|
|||
|
- Buffers circulares.
|
|||
|
|
|||
|
(*) Referencias.
|
|||
|
|
|||
|
|
|||
|
====================================
|
|||
|
MODELO ABSTRACTO DE ACCESO A MEMORIA
|
|||
|
====================================
|
|||
|
|
|||
|
Considere el siguiente modelo abstracto del sistema:
|
|||
|
|
|||
|
: :
|
|||
|
: :
|
|||
|
: :
|
|||
|
+-------+ : +--------+ : +-------+
|
|||
|
| | : | | : | |
|
|||
|
| | : | | : | |
|
|||
|
| CPU 1 |<----->| Memoria|<----->| CPU 2 |
|
|||
|
| | : | | : | |
|
|||
|
| | : | | : | |
|
|||
|
+-------+ : +--------+ : +-------+
|
|||
|
^ : ^ : ^
|
|||
|
| : | : |
|
|||
|
| : | : |
|
|||
|
| : v : |
|
|||
|
| : +--------+ : |
|
|||
|
| : | | : |
|
|||
|
| : | Disposi| : |
|
|||
|
+---------->| tivo |<----------+
|
|||
|
: | | :
|
|||
|
: | | :
|
|||
|
: +--------+ :
|
|||
|
: :
|
|||
|
|
|||
|
Cada CPU ejecuta un programa que genera operaciones de acceso a la memoria.
|
|||
|
En la CPU abstracta, el orden de las operaciones de memoria es muy
|
|||
|
relajado, y una CPU en realidad puede realizar las operaciones de memoria
|
|||
|
en el orden que desee, siempre que la causalidad del programa parezca
|
|||
|
mantenerse. De manera similar, el compilador también puede organizar las
|
|||
|
instrucciones que emite en el orden que quiera, siempre que no afecte al
|
|||
|
funcionamiento aparente del programa.
|
|||
|
|
|||
|
Entonces, en el diagrama anterior, los efectos de las operaciones de
|
|||
|
memoria realizadas por un CPU son percibidos por el resto del sistema a
|
|||
|
medida que las operaciones cruzan la interfaz entre la CPU y el resto del
|
|||
|
sistema (las líneas discontinuas a puntos).
|
|||
|
|
|||
|
Por ejemplo, considere la siguiente secuencia de eventos:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
=============== ===============
|
|||
|
{ A == 1; B == 2 }
|
|||
|
A = 3; x = B;
|
|||
|
B = 4; y = A;
|
|||
|
|
|||
|
El conjunto de accesos visto por el sistema de memoria en el medio se puede
|
|||
|
organizar en 24 combinaciones diferentes (donde LOAD es cargar y STORE es
|
|||
|
guardar):
|
|||
|
|
|||
|
STORE A=3, STORE B=4, y=LOAD A->3, x=LOAD B->4
|
|||
|
STORE A=3, STORE B=4, x=LOAD B->4, y=LOAD A->3
|
|||
|
STORE A=3, y=LOAD A->3, STORE B=4, x=LOAD B->4
|
|||
|
STORE A=3, y=LOAD A->3, x=LOAD B->2, STORE B=4
|
|||
|
STORE A=3, x=LOAD B->2, STORE B=4, y=LOAD A->3
|
|||
|
STORE A=3, x=LOAD B->2, y=LOAD A->3, STORE B=4
|
|||
|
STORE B=4, STORE A=3, y=LOAD A->3, x=LOAD B->4
|
|||
|
STORE B=4, ...
|
|||
|
...
|
|||
|
|
|||
|
y por lo tanto puede resultar en cuatro combinaciones diferentes de
|
|||
|
valores:
|
|||
|
|
|||
|
x == 2, y == 1
|
|||
|
x == 2, y == 3
|
|||
|
x == 4, y == 1
|
|||
|
x == 4, y == 3
|
|||
|
|
|||
|
Además, los stores asignados por una CPU al sistema de memoria pueden no
|
|||
|
ser percibidos por los loads realizados por otra CPU en el mismo orden en
|
|||
|
que fueron realizados.
|
|||
|
|
|||
|
Como otro ejemplo, considere esta secuencia de eventos:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
=============== ===============
|
|||
|
{ A == 1, B == 2, C == 3, P == &A, Q == &C }
|
|||
|
B = 4; Q = P;
|
|||
|
P = &B; D = *Q;
|
|||
|
|
|||
|
Aquí hay una dependencia obvia de la dirección, ya que el valor cargado en
|
|||
|
D depende en la dirección recuperada de P por la CPU 2. Al final de la
|
|||
|
secuencia, cualquiera de los siguientes resultados son posibles:
|
|||
|
|
|||
|
(Q == &A) y (D == 1)
|
|||
|
(Q == &B) y (D == 2)
|
|||
|
(Q == &B) y (D == 4)
|
|||
|
|
|||
|
Tenga en cuenta que la CPU 2 nunca intentará cargar C en D porque la CPU
|
|||
|
cargará P en Q antes de emitir la carga de *Q.
|
|||
|
|
|||
|
OPERACIONES DEL DISPOSITIVO
|
|||
|
---------------------------
|
|||
|
|
|||
|
Algunos dispositivos presentan sus interfaces de control como colecciones
|
|||
|
de ubicaciones de memoria, pero el orden en que se accede a los registros
|
|||
|
de control es muy importante. Por ejemplo, imagine una tarjeta ethernet con
|
|||
|
un conjunto de registros a los que se accede a través de un registro de
|
|||
|
puerto de dirección (A) y un registro de datos del puerto (D). Para leer el
|
|||
|
registro interno 5, el siguiente código podría entonces ser usado:
|
|||
|
|
|||
|
*A = 5;
|
|||
|
x = *D;
|
|||
|
|
|||
|
pero esto podría aparecer como cualquiera de las siguientes dos secuencias:
|
|||
|
|
|||
|
STORE *A = 5, x = LOAD *D
|
|||
|
x = LOAD *D, STORE *A = 5
|
|||
|
|
|||
|
el segundo de las cuales casi con certeza resultará en mal funcionamiento,
|
|||
|
ya que se estableció la dirección _después_ de intentar leer el registro.
|
|||
|
|
|||
|
|
|||
|
GARANTÍAS
|
|||
|
---------
|
|||
|
|
|||
|
Hay algunas garantías mínimas que se pueden esperar de una CPU:
|
|||
|
|
|||
|
(*) En cualquier CPU dada, los accesos a la memoria dependiente se
|
|||
|
emitirán en orden, con respeto a sí mismo. Esto significa que para:
|
|||
|
|
|||
|
Q = READ_ONCE(P); D = READ_ONCE(*Q);
|
|||
|
|
|||
|
donde READ_ONCE() es LEER_UNA_VEZ(), la CPU emitirá las siguientes
|
|||
|
operaciones de memoria:
|
|||
|
|
|||
|
Q = LOAD P, D = LOAD *Q
|
|||
|
|
|||
|
y siempre en ese orden. Sin embargo, en DEC Alpha, READ_ONCE() también
|
|||
|
emite una instrucción de barrera de memoria, de modo que una CPU DEC
|
|||
|
Alpha, sin embargo emite las siguientes operaciones de memoria:
|
|||
|
|
|||
|
Q = LOAD P, MEMORY_BARRIER, D = LOAD *Q, MEMORY_BARRIER
|
|||
|
|
|||
|
Ya sea en DEC Alpha o no, READ_ONCE() también evita que el compilador
|
|||
|
haga cosas inapropiadas.
|
|||
|
|
|||
|
(*) Los loads y stores superpuestos dentro de una CPU en particular
|
|||
|
parecerán ser ordenados dentro de esa CPU. Esto significa que para:
|
|||
|
|
|||
|
a = READ_ONCE(*X); WRITE_ONCE(*X, b);
|
|||
|
|
|||
|
donde WRITE_ONCE() es ESCRIBIR_UNA_VEZ(), la CPU solo emitirá la
|
|||
|
siguiente secuencia de operaciones de memoria:
|
|||
|
|
|||
|
a = LOAD *X, STORE *X = b
|
|||
|
|
|||
|
Y para:
|
|||
|
|
|||
|
WRITE_ONCE(*X, c); d = READ_ONCE(*X);
|
|||
|
|
|||
|
la CPU solo emitirá:
|
|||
|
|
|||
|
STORE *X = c, d = LOAD *X
|
|||
|
|
|||
|
(Los loads y stores se superponen si están destinados a piezas
|
|||
|
superpuestas de memoria).
|
|||
|
|
|||
|
Y hay una serie de cosas que _deben_ o _no_ deben asumirse:
|
|||
|
|
|||
|
(*) _No_debe_ asumirse que el compilador hará lo que usted quiera
|
|||
|
con referencias de memoria que no están protegidas por READ_ONCE() y
|
|||
|
WRITE ONCE(). Sin ellos, el compilador tiene derecho a hacer todo tipo
|
|||
|
de transformaciones "creativas", que se tratan en la sección BARRERA
|
|||
|
DEL COMPILADOR.
|
|||
|
|
|||
|
(*) _No_debe_ suponerse que se emitirán loads y stores independientes
|
|||
|
en el orden dado. Esto significa que para:
|
|||
|
|
|||
|
X = *A; Y = *B; *D = Z;
|
|||
|
|
|||
|
podemos obtener cualquiera de las siguientes secuencias:
|
|||
|
|
|||
|
X = LOAD *A, Y = LOAD *B, STORE *D = Z
|
|||
|
X = LOAD *A, STORE *D = Z, Y = LOAD *B
|
|||
|
Y = LOAD *B, X = LOAD *A, STORE *D = Z
|
|||
|
Y = LOAD *B, STORE *D = Z, X = LOAD *A
|
|||
|
STORE *D = Z, X = LOAD *A, Y = LOAD *B
|
|||
|
STORE *D = Z, Y = LOAD *B, X = LOAD *A
|
|||
|
|
|||
|
(*) Se _debe_ suponer que los accesos de memoria superpuestos pueden
|
|||
|
fusionarse o ser descartados. Esto significa que para:
|
|||
|
|
|||
|
X = *A; Y = *(A + 4);
|
|||
|
|
|||
|
podemos obtener cualquiera de las siguientes secuencias:
|
|||
|
|
|||
|
X = LOAD *A; Y = LOAD *(A + 4);
|
|||
|
Y = LOAD *(A + 4); X = LOAD *A;
|
|||
|
{X, Y} = LOAD {*A, *(A + 4) };
|
|||
|
|
|||
|
Y para:
|
|||
|
|
|||
|
*A = X; *(A + 4) = Y;
|
|||
|
|
|||
|
podemos obtener cualquiera de:
|
|||
|
|
|||
|
STORE *A = X; STORE *(A + 4) = Y;
|
|||
|
STORE *(A + 4) = Y; STORE *A = X;
|
|||
|
STORE {*A, *(A + 4) } = {X, Y};
|
|||
|
|
|||
|
Y hay anti-garantías:
|
|||
|
|
|||
|
(*) Estas garantías no se aplican a los campos de bits, porque los
|
|||
|
compiladores a menudo generan código para modificarlos usando
|
|||
|
secuencias de lectura-modificación-escritura no atómica. No intente
|
|||
|
utilizar campos de bits para sincronizar algoritmos paralelos.
|
|||
|
|
|||
|
(*) Incluso en los casos en que los campos de bits están protegidos por
|
|||
|
cerrojos (o "cerrojos", o "locks"), todos los componentes en un campo
|
|||
|
de bits dado deben estar protegidos por un candado. Si dos campos en un
|
|||
|
campo de bits dado están protegidos por diferentes locks, las
|
|||
|
secuencias de lectura-modificación-escritura no atómicas del lock
|
|||
|
pueden causar una actualización a una campo para corromper el valor de
|
|||
|
un campo adyacente.
|
|||
|
|
|||
|
(*) Estas garantías se aplican solo a escalares correctamente alineados y
|
|||
|
dimensionados. De "tamaño adecuado" significa actualmente variables que
|
|||
|
son del mismo tamaño que "char", "short", "int" y "long".
|
|||
|
"Adecuadamente alineado" significa la alineación natural, por lo tanto,
|
|||
|
no hay restricciones para "char", alineación de dos bytes para "short",
|
|||
|
alineación de cuatro bytes para "int", y alineación de cuatro u ocho
|
|||
|
bytes para "long", en sistemas de 32 y 64 bits, respectivamente. Tenga
|
|||
|
en cuenta que estos garantías se introdujeron en el estándar C11, así
|
|||
|
que tenga cuidado cuando utilice compiladores anteriores a C11 (por
|
|||
|
ejemplo, gcc 4.6). La parte de la norma que contiene esta garantía es
|
|||
|
la Sección 3.14, que define "ubicación de memoria" de la siguiente
|
|||
|
manera:
|
|||
|
|
|||
|
ubicación de memoria
|
|||
|
ya sea un objeto de tipo escalar, o una secuencia máxima
|
|||
|
de campos de bits adyacentes, todos con ancho distinto de cero
|
|||
|
|
|||
|
NOTE 1: Dos hilos de ejecución pueden actualizar y acceder
|
|||
|
ubicaciones de memoria separadas sin interferir entre
|
|||
|
ellos.
|
|||
|
|
|||
|
NOTE 2: Un campo de bits y un miembro adyacente que no es un campo de
|
|||
|
bits están en ubicaciones de memoria separadas. Lo mismo sucede con
|
|||
|
dos campos de bits, si uno se declara dentro de un declaración de
|
|||
|
estructura anidada y el otro no, o si las dos están separados por una
|
|||
|
declaración de campo de bits de longitud cero, o si están separados por
|
|||
|
un miembro no declarado como campo de bits. No es seguro actualizar
|
|||
|
simultáneamente dos campos de bits en la misma estructura si entre
|
|||
|
todos los miembros declarados también hay campos de bits, sin importar
|
|||
|
cuál resulta ser el tamaño de estos campos de bits intermedios.
|
|||
|
|
|||
|
|
|||
|
==================================
|
|||
|
¿QUÉ SON LAS BARRERAS DE MEMORIA?
|
|||
|
==================================
|
|||
|
|
|||
|
Como se puede leer arriba, las operaciones independientes de memoria se
|
|||
|
realizan de manera efectiva en orden aleatorio, pero esto puede ser un
|
|||
|
problema para la interacción CPU-CPU y para la E/S ("I/O"). Lo que se
|
|||
|
requiere es alguna forma de intervenir para instruir al compilador y al
|
|||
|
CPU para restringir el orden.
|
|||
|
|
|||
|
Las barreras de memoria son este tipo de intervenciones. Imponen una
|
|||
|
percepción de orden parcial, sobre las operaciones de memoria a ambos lados
|
|||
|
de la barrera.
|
|||
|
|
|||
|
Tal cumplimiento es importante porque las CPUs y otros dispositivos en un
|
|||
|
sistema pueden usar una variedad de trucos para mejorar el rendimiento,
|
|||
|
incluido el reordenamiento, diferimiento y combinación de operaciones de
|
|||
|
memoria; cargas especulativas; predicción de "branches" especulativos y
|
|||
|
varios tipos de almacenamiento en caché. Las barreras de memoria se
|
|||
|
utilizan para anular o suprimir estos trucos, permitiendo que el código
|
|||
|
controle sensatamente la interacción de múltiples CPU y/o dispositivos.
|
|||
|
|
|||
|
|
|||
|
VARIEDADES DE BARRERA DE MEMORIA
|
|||
|
---------------------------------
|
|||
|
|
|||
|
Las barreras de memoria vienen en cuatro variedades básicas:
|
|||
|
|
|||
|
(1) Barreras de memoria al escribir o almacenar (Write or store memory
|
|||
|
barriers).
|
|||
|
|
|||
|
Una barrera de memoria de escritura garantiza que todas las
|
|||
|
operaciones de STORE especificadas antes de que la barrera aparezca
|
|||
|
suceden antes de todas las operaciones STORE especificadas después
|
|||
|
de la barrera, con respecto a los otros componentes del sistema.
|
|||
|
|
|||
|
Una barrera de escritura es un orden parcial solo en los stores; No
|
|||
|
es requerido que tenga ningún efecto sobre los loads.
|
|||
|
|
|||
|
Se puede considerar que una CPU envía una secuencia de operaciones de
|
|||
|
store al sistema de memoria a medida que pasa el tiempo. Todos los
|
|||
|
stores _antes_ de una barrera de escritura ocurrirán _antes_ de todos
|
|||
|
los stores después de la barrera de escritura.
|
|||
|
|
|||
|
[!] Tenga en cuenta que las barreras de escritura normalmente deben
|
|||
|
combinarse con read o barreras de address-dependency barriers
|
|||
|
(dependencia de dirección); consulte la subsección
|
|||
|
"Emparejamiento de barreras smp".
|
|||
|
|
|||
|
|
|||
|
(2) Barrera de dependencia de dirección (histórico).
|
|||
|
|
|||
|
Una barrera de dependencia de dirección es una forma más débil de
|
|||
|
barrera de lectura. En el caso de que se realicen dos loads de manera
|
|||
|
que la segunda dependa del resultado de la primera (por ejemplo: el
|
|||
|
primer load recupera la dirección a la que se dirigirá el segundo
|
|||
|
load), una barrera de dependencia de dirección sería necesaria para
|
|||
|
asegurarse de que el objetivo de la segunda carga esté actualizado
|
|||
|
después de acceder a la dirección obtenida por la primera carga.
|
|||
|
|
|||
|
Una barrera de dependencia de direcciones es una ordenación parcial en
|
|||
|
laods de direcciones interdependientes; no se requiere que tenga
|
|||
|
ningún efecto en los stores, ya sean cargas de memoria o cargas
|
|||
|
de memoria superpuestas.
|
|||
|
|
|||
|
Como se mencionó en (1), las otras CPU en el sistema pueden verse como
|
|||
|
secuencias de stores en el sistema de memoria que la considerada CPU
|
|||
|
puede percibir. Una barrera de dependencia de dirección emitida por
|
|||
|
la CPU en cuestión garantiza que para cualquier carga que la preceda,
|
|||
|
si esa carga toca alguna secuencia de stores de otra CPU, entonces
|
|||
|
en el momento en que la barrera se complete, los efectos de todos los
|
|||
|
stores antes del cambio del load serán perceptibles por cualquier
|
|||
|
carga emitida después la barrera de la dependencia de la dirección.
|
|||
|
|
|||
|
Consulte la subsección "Ejemplos de secuencias de barrera de memoria"
|
|||
|
para ver los diagramas mostrando las restricciones de orden.
|
|||
|
|
|||
|
[!] Tenga en cuenta que la primera carga realmente tiene que tener una
|
|||
|
dependencia de _dirección_ y no es una dependencia de control. Si la
|
|||
|
dirección para la segunda carga depende de la primera carga, pero la
|
|||
|
dependencia es a través de un condicional en lugar de -en realidad-
|
|||
|
cargando la dirección en sí, entonces es una dependencia de _control_
|
|||
|
y se requiere una barrera de lectura completa o superior. Consulte la
|
|||
|
subsección "Dependencias de control" para más información.
|
|||
|
|
|||
|
[!] Tenga en cuenta que las barreras de dependencia de dirección
|
|||
|
normalmente deben combinarse con barreras de escritura; consulte la
|
|||
|
subsección "Emparejamiento de barreras smp".
|
|||
|
|
|||
|
[!] Desde el kernel v5.9, se eliminó la API del kernel para barreras
|
|||
|
de memoria de direcciones explícitas. Hoy en día, las APIs para marcar
|
|||
|
cargas de variables compartidas, como READ_ONCE() y rcu_dereference(),
|
|||
|
proporcionan barreras de dependencia de dirección implícitas.
|
|||
|
|
|||
|
(3) Barreras de memoria al leer o cargar (Read or load memory
|
|||
|
barriers).
|
|||
|
|
|||
|
Una barrera de lectura es una barrera de dependencia de direcciones,
|
|||
|
más una garantía de que todas las operaciones de LOAD especificadas
|
|||
|
antes de la barrera parecerán ocurrir antes de todas las operaciones
|
|||
|
de LOAD especificadas después de la barrera con respecto a los demás
|
|||
|
componentes del sistema.
|
|||
|
|
|||
|
Una barrera de lectura es un orden parcial solo en cargas; no es
|
|||
|
necesario que tenga ningún efecto en los stores.
|
|||
|
|
|||
|
Las barreras de memoria de lectura implican barreras de dependencia de
|
|||
|
direcciones, y por tanto puede sustituirlas por estas.
|
|||
|
|
|||
|
[!] Tenga en mente que las barreras de lectura normalmente deben
|
|||
|
combinarse con barreras de escritura; consulte la subsección
|
|||
|
"Emparejamiento de barreras smp".
|
|||
|
|
|||
|
(4) Barreras de memoria generales
|
|||
|
|
|||
|
Una barrera de memoria general proporciona la garantía de que todas
|
|||
|
las operaciones LOAD y STORE especificadas antes de que la barrera
|
|||
|
aparezca suceden antes de que todas las operaciones LOAD y STORE
|
|||
|
especificadas después de la barrera con respecto a los demás
|
|||
|
componentes del sistema.
|
|||
|
|
|||
|
Una barrera de memoria general es un orden parcial tanto en
|
|||
|
operaciones de carga como de almacenamiento.
|
|||
|
|
|||
|
Las barreras de memoria generales implican barreras de memoria tanto
|
|||
|
de lectura como de escritura, de modo que pueden sustituir a
|
|||
|
cualquiera.
|
|||
|
|
|||
|
Y un par de variedades implícitas:
|
|||
|
|
|||
|
(5) ACQUIRE (de adquisición).
|
|||
|
|
|||
|
Esto actúa como una barrera permeable unidireccional. Garantiza que
|
|||
|
toda las operaciones de memoria después de la operación ACQUIRE
|
|||
|
parezcan suceder después de la ACQUIRE con respecto a los demás
|
|||
|
componentes del sistema. Las operaciones ACQUIRE incluyen operaciones
|
|||
|
LOCK y smp_load_acquire(), y operaciones smp_cond_load_acquire().
|
|||
|
|
|||
|
Las operaciones de memoria que ocurren antes de una operación ACQUIRE
|
|||
|
pueden parecer suceder después de que se complete.
|
|||
|
|
|||
|
Una operación ACQUIRE casi siempre debe estar emparejada con una
|
|||
|
operación RELEASE (de liberación).
|
|||
|
|
|||
|
|
|||
|
(6) Operaciones RELEASE (de liberación).
|
|||
|
|
|||
|
Esto también actúa como una barrera permeable unidireccional.
|
|||
|
Garantiza que todas las operaciones de memoria antes de la operación
|
|||
|
RELEASE parecerán ocurrir antes de la operación RELEASE con respecto a
|
|||
|
los demás componentes del sistema. Las operaciones de RELEASE incluyen
|
|||
|
operaciones de UNLOCK y operaciones smp_store_release().
|
|||
|
|
|||
|
Las operaciones de memoria que ocurren después de una operación
|
|||
|
RELEASE pueden parecer suceder antes de que se complete.
|
|||
|
|
|||
|
El uso de las operaciones ACQUIRE y RELEASE generalmente excluye la
|
|||
|
necesidad de otros tipos de barrera de memoria. Además, un par
|
|||
|
RELEASE+ACQUIRE NO garantiza actuar como una barrera de memoria
|
|||
|
completa. Sin embargo, después de un ACQUIRE de una variable dada,
|
|||
|
todos los accesos a la memoria que preceden a cualquier anterior
|
|||
|
RELEASE en esa misma variable están garantizados como visibles. En
|
|||
|
otras palabras, dentro de la sección crítica de una variable dada,
|
|||
|
todos los accesos de todas las secciones críticas anteriores para esa
|
|||
|
variable habrán terminado de forma garantizada.
|
|||
|
|
|||
|
Esto significa que ACQUIRE actúa como una operación mínima de
|
|||
|
"adquisición" y RELEASE actúa como una operación mínima de
|
|||
|
"liberación".
|
|||
|
|
|||
|
Un subconjunto de las operaciones atómicas descritas en atomic_t.txt
|
|||
|
contiene variantes de ACQUIRE y RELEASE, además de definiciones
|
|||
|
completamente ordenadas o relajadas (sin barrera semántica). Para
|
|||
|
composiciones atómicas que realizan tanto un load como store, la semántica
|
|||
|
ACQUIRE se aplica solo a la carga y la semántica RELEASE se aplica sólo a
|
|||
|
la parte de la operación del store.
|
|||
|
|
|||
|
Las barreras de memoria solo son necesarias cuando existe la posibilidad de
|
|||
|
interacción entre dos CPU o entre una CPU y un dispositivo. Si se puede
|
|||
|
garantizar que no habrá tal interacción en ninguna pieza de código en
|
|||
|
particular, entonces las barreras de memoria son innecesarias en ese
|
|||
|
fragmento de código.
|
|||
|
|
|||
|
Tenga en cuenta que estas son las garantías _mínimas_. Diferentes
|
|||
|
arquitecturas pueden proporcionar garantías más sustanciales, pero no se
|
|||
|
puede confiar en estas fuera de esa arquitectura en específico.
|
|||
|
|
|||
|
|
|||
|
¿QUÉ NO SE PUEDE ASUMIR SOBRE LAS BARRERAS DE LA MEMORIA?
|
|||
|
---------------------------------------------------------
|
|||
|
|
|||
|
Hay ciertas cosas que las barreras de memoria del kernel Linux no
|
|||
|
garantizan:
|
|||
|
|
|||
|
(*) No hay garantía de que ninguno de los accesos a la memoria
|
|||
|
especificados antes de una barrera de memoria estará _completo_ al
|
|||
|
completarse una instrucción de barrera de memoria; se puede considerar
|
|||
|
que la barrera dibuja una línea en la cola de acceso del CPU que no
|
|||
|
pueden cruzar los accesos del tipo correspondiente.
|
|||
|
|
|||
|
(*) No hay garantía de que la emisión de una barrera de memoria en una CPU
|
|||
|
tenga cualquier efecto directo en otra CPU o cualquier otro hardware
|
|||
|
en el sistema. El efecto indirecto será el orden en que la segunda CPU
|
|||
|
ve los efectos de los primeros accesos que ocurren de la CPU, pero lea
|
|||
|
el siguiente argumento:
|
|||
|
|
|||
|
(*) No hay garantía de que una CPU vea el orden correcto de los efectos
|
|||
|
de los accesos de una segunda CPU, incluso _si_ la segunda CPU usa una
|
|||
|
barrera de memoria, a menos que la primera CPU _también_ use una
|
|||
|
barrera de memoria coincidente (vea el subapartado "Emparejamiento de
|
|||
|
barrera SMP").
|
|||
|
|
|||
|
(*) No hay garantía de que alguna pieza intermedia fuera del hardware[*]
|
|||
|
del CPU no reordenará los accesos a la memoria. Los mecanismos de
|
|||
|
coherencia de caché del CPU deben propagar los efectos indirectos de
|
|||
|
una barrera de memoria entre las CPU, pero es posible que no lo hagan
|
|||
|
en orden.
|
|||
|
|
|||
|
[*] Para obtener información sobre bus mastering DMA y coherencia, lea:
|
|||
|
|
|||
|
Documentation/driver-api/pci/pci.rst
|
|||
|
Documentation/core-api/dma-api-howto.rst
|
|||
|
Documentation/core-api/dma-api.rst
|
|||
|
|
|||
|
|
|||
|
BARRERA DE DEPENDENCIA DE DIRECCIÓN (HISTÓRICO)
|
|||
|
-----------------------------------------------
|
|||
|
|
|||
|
A partir de la versión 4.15 del kernel Linux, se agregó un smp_mb() a
|
|||
|
READ_ONCE() para DEC Alpha, lo que significa que las únicas personas que
|
|||
|
necesitan prestar atención a esta sección son aquellas que trabajan en el
|
|||
|
código específico de la arquitectura DEC Alpha y aquellas que trabajan en
|
|||
|
READ_ONCE() por dentro. Para aquellos que lo necesitan, y para aquellos que
|
|||
|
estén interesados desde un punto de vista histórico, aquí está la historia
|
|||
|
de las barreras de dependencia de dirección.
|
|||
|
|
|||
|
[!] Si bien las dependencias de direcciones se observan tanto en carga a
|
|||
|
carga como en relaciones de carga a store, las barreras de dependencia de
|
|||
|
dirección no son necesarias para situaciones de carga a store.
|
|||
|
|
|||
|
El requisito de las barreras de dependencia de dirección es un poco sutil,
|
|||
|
y no siempre es obvio que sean necesarias. Para ilustrar, considere la
|
|||
|
siguiente secuencia de eventos:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
=============== ===============
|
|||
|
{ A == 1, B == 2, C == 3, P == &A, Q == &C }
|
|||
|
B = 4;
|
|||
|
<barrera de escritura>
|
|||
|
WRITE_ONCE(P, &B);
|
|||
|
Q = READ_ONCE_OLD(P);
|
|||
|
D = *Q;
|
|||
|
|
|||
|
[!] READ_ONCE_OLD() corresponde a READ_ONCE() del kernel anterior a 4.15,
|
|||
|
que no implica una barrera de dependencia de direcciones.
|
|||
|
|
|||
|
Hay una clara dependencia de dirección aquí, y parecería que al final de
|
|||
|
la secuencia, Q debe ser &A o &B, y que:
|
|||
|
|
|||
|
(Q == &A) implica (D == 1)
|
|||
|
(Q == &B) implica (D == 4)
|
|||
|
|
|||
|
¡Pero! La percepción de la CPU 2 de P puede actualizarse _antes_ de su
|
|||
|
percepción de B, por lo tanto dando lugar a la siguiente situación:
|
|||
|
|
|||
|
(Q == &B) y (D == 2) ????
|
|||
|
|
|||
|
Si bien esto puede parecer una falla en el mantenimiento de la coherencia
|
|||
|
o la causalidad, no lo es, y este comportamiento se puede observar en
|
|||
|
ciertas CPU reales (como DEC Alfa).
|
|||
|
|
|||
|
Para lidiar con esto, READ_ONCE() proporciona una barrera de dependencia
|
|||
|
de dirección implícita desde el lanzamiento del kernel v4.15:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
=============== ===============
|
|||
|
{ A == 1, B == 2, C == 3, P == &A, Q == &C }
|
|||
|
B = 4;
|
|||
|
<barrera de escritura>
|
|||
|
WRITE_ONCE(P, &B);
|
|||
|
Q = READ_ONCE(P);
|
|||
|
<barrera de dependencia de dirección implícita>
|
|||
|
D = *Q;
|
|||
|
|
|||
|
Esto refuerza la ocurrencia de una de las dos implicaciones, y previene la
|
|||
|
tercera posibilidad de surgir.
|
|||
|
|
|||
|
|
|||
|
[!] Tenga en cuenta que esta situación extremadamente contraria a la
|
|||
|
intuición surge más fácilmente en máquinas con cachés divididos, de modo
|
|||
|
que, por ejemplo, un banco de caché procesa líneas de caché pares y el otro
|
|||
|
banco procesa líneas impares de caché. El puntero P podría almacenarse en
|
|||
|
una línea de caché impar y la variable B podría almacenarse en una línea de
|
|||
|
caché con número par. Entonces, si el banco de números pares de la memoria
|
|||
|
caché de la CPU de lectura está extremadamente ocupado mientras que el
|
|||
|
banco impar está inactivo, uno podría ver el nuevo valor del puntero P
|
|||
|
(&B), pero el antiguo valor de la variable B (2).
|
|||
|
|
|||
|
|
|||
|
No se requiere una barrera de dependencia de dirección para ordenar
|
|||
|
escrituras dependientes porque las CPU que admite el kernel Linux no
|
|||
|
escriben hasta que están seguros (1) de que la escritura realmente
|
|||
|
sucederá, (2) de la ubicación de la escritura, y (3) del valor a escribir.
|
|||
|
Pero, por favor, lea atentamente la sección "DEPENDENCIAS DEL CONTROL" y el
|
|||
|
archivo Documentation/RCU/rcu_dereference.rst: el compilador puede romperse
|
|||
|
y romper dependencias en muchas formas altamente creativas.
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
=============== ===============
|
|||
|
{ A == 1, B == 2, C = 3, P == &A, Q == &C }
|
|||
|
B = 4;
|
|||
|
<barrera de escritura>
|
|||
|
WRITE_ONCE(P, &B);
|
|||
|
Q = READ_ONCE_OLD(P);
|
|||
|
WRITE_ONCE(*Q, 5);
|
|||
|
|
|||
|
Por lo tanto, no se requiere ninguna barrera de dependencia de direcciones
|
|||
|
para ordenar la lectura en Q con el load en *Q. En otras palabras, este
|
|||
|
resultado está prohibido, incluso sin una barrera de dependencia de
|
|||
|
dirección implícita del READ_ONCE() moderno:
|
|||
|
|
|||
|
(Q == &B) && (B == 4)
|
|||
|
|
|||
|
Tenga en cuenta que este patrón debe ser raro. Después de todo, el objetivo
|
|||
|
del orden de dependencia es -prevenir- escrituras en la estructura de
|
|||
|
datos, junto con los costosos errores de caché asociados con tales
|
|||
|
escrituras. Este patrón se puede utilizar para registrar raras condiciones
|
|||
|
de error y similares, y el orden natural de las CPUs evita que se pierdan
|
|||
|
tales registros.
|
|||
|
|
|||
|
|
|||
|
Tenga en cuenta que el orden proporcionado por una dependencia de dirección
|
|||
|
es local para la CPU que lo contiene. Lea la sección sobre "Atomicidad
|
|||
|
multicopia" para más información.
|
|||
|
|
|||
|
|
|||
|
La barrera de dependencia de dirección es muy importante para el sistema
|
|||
|
RCU, por ejemplo. Vea rcu_assign_pointer() y rcu_dereference() en
|
|||
|
include/linux/rcupdate.h. Esto permite que el objetivo actual de un puntero
|
|||
|
RCU sea reemplazado con un nuevo objetivo modificado, sin que el objetivo
|
|||
|
del reemplazo parezca estar inicializado de manera incompleta.
|
|||
|
|
|||
|
Consulte también la subsección sobre "Coherencia de caché" para obtener un
|
|||
|
ejemplo más completo.
|
|||
|
|
|||
|
DEPENDENCIAS DE CONTROL
|
|||
|
-----------------------
|
|||
|
|
|||
|
Las dependencias de control pueden ser un poco complicadas porque los
|
|||
|
compiladores actuales no las entienden. El propósito de esta sección es
|
|||
|
ayudarle a prevenir que la ignorancia del compilador rompa su código.
|
|||
|
|
|||
|
Una dependencia de control load-load (de carga a carga) requiere una
|
|||
|
barrera de memoria de lectura completa, no simplemente una barrera
|
|||
|
(implícita) de dependencia de direcciones para que funcione correctamente.
|
|||
|
Considere el siguiente fragmento de código:
|
|||
|
|
|||
|
q = READ_ONCE(a);
|
|||
|
<barrera implícita de dependencia de direcciones>
|
|||
|
if (q) {
|
|||
|
/* BUG: No hay dependencia de dirección!!! */
|
|||
|
p = READ_ONCE(b);
|
|||
|
}
|
|||
|
|
|||
|
Esto no tendrá el efecto deseado porque no hay una dependencia de dirección
|
|||
|
real, sino más bien una dependencia de control que la CPU puede
|
|||
|
cortocircuitar al intentar predecir el resultado por adelantado, para que
|
|||
|
otras CPU vean la carga de b como si hubiera ocurrido antes que la carga de
|
|||
|
a. En cuyo caso lo que realmente se requiere es:
|
|||
|
|
|||
|
q = READ_ONCE(a);
|
|||
|
if (q) {
|
|||
|
<barrera de lectura>
|
|||
|
p = READ_ONCE(b);
|
|||
|
}
|
|||
|
|
|||
|
Sin embargo, los stores no se especulan. Esto significa que ordenar -es-
|
|||
|
provisto para dependencias de control de load-store, como en el siguiente
|
|||
|
ejemplo:
|
|||
|
|
|||
|
q = READ_ONCE(a);
|
|||
|
if (q) {
|
|||
|
WRITE_ONCE(b, 1);
|
|||
|
}
|
|||
|
|
|||
|
Las dependencias de control se emparejan normalmente con otros tipos de
|
|||
|
barreras. Dicho esto, tenga en cuenta que ni READ_ONCE() ni WRITE_ONCE()
|
|||
|
son opcionales! Sin READ_ONCE(), el compilador podría combinar la carga de
|
|||
|
'a' con otras cargas de 'a'. Sin WRITE_ONCE(), el compilador podría
|
|||
|
combinar el store de 'b' con otros stores de 'b'. Cualquiera de estos casos
|
|||
|
puede dar lugar a efectos en el orden muy contrarios a la intuición.
|
|||
|
|
|||
|
Peor aún, si el compilador puede probar (decir) que el valor de la
|
|||
|
variable 'a' siempre es distinta de cero, estaría dentro de sus derechos
|
|||
|
para optimizar el ejemplo original eliminando la declaración "if", como:
|
|||
|
|
|||
|
q = a;
|
|||
|
b = 1; /* BUG: Compilador y CPU pueden ambos reordernar!!! */
|
|||
|
|
|||
|
Así que no deje de lado READ_ONCE().
|
|||
|
|
|||
|
Es tentador tratar de hacer cumplir el orden en stores idénticos en ambos
|
|||
|
caminos del "if" de la siguiente manera:
|
|||
|
|
|||
|
q = READ_ONCE(a);
|
|||
|
if (q) {
|
|||
|
barrier();
|
|||
|
WRITE_ONCE(b, 1);
|
|||
|
hacer_algo();
|
|||
|
} else {
|
|||
|
barrier();
|
|||
|
WRITE_ONCE(b, 1);
|
|||
|
hacer_otra_cosa();
|
|||
|
}
|
|||
|
|
|||
|
Desafortunadamente, los compiladores actuales transformarán esto de la
|
|||
|
siguiente manera en casos de alto nivel de optimización:
|
|||
|
|
|||
|
q = READ_ONCE(a);
|
|||
|
barrier();
|
|||
|
WRITE_ONCE(b, 1); /* BUG: No hay orden en load de a!!! */
|
|||
|
if (q) {
|
|||
|
/* WRITE_ONCE(b, 1); -- movido arriba, BUG!!! */
|
|||
|
hacer_algo();
|
|||
|
} else {
|
|||
|
/* WRITE_ONCE(b, 1); -- movido arriba, BUG!!! */
|
|||
|
hacer_otra_cosa();
|
|||
|
}
|
|||
|
|
|||
|
Ahora no hay condicional entre la carga de 'a' y el store de 'b', lo que
|
|||
|
significa que la CPU está en su derecho de reordenarlos: El condicional es
|
|||
|
absolutamente necesario y debe estar presente en el código ensamblador
|
|||
|
incluso después de que se hayan aplicado todas las optimizaciones del
|
|||
|
compilador. Por lo tanto, si necesita ordenar en este ejemplo, necesita
|
|||
|
explícitamente barreras de memoria, por ejemplo, smp_store_release():
|
|||
|
|
|||
|
|
|||
|
q = READ_ONCE(a);
|
|||
|
if (q) {
|
|||
|
smp_store_release(&b, 1);
|
|||
|
hacer_algo();
|
|||
|
} else {
|
|||
|
smp_store_release(&b, 1);
|
|||
|
hacer_otra_cosa();
|
|||
|
}
|
|||
|
|
|||
|
Por el contrario, sin barreras de memoria explícita, el control de un if
|
|||
|
con dos opciones está garantizado solo cuando los stores difieren, por
|
|||
|
ejemplo:
|
|||
|
|
|||
|
q = READ_ONCE(a);
|
|||
|
if (q) {
|
|||
|
WRITE_ONCE(b, 1);
|
|||
|
hacer_algo();
|
|||
|
} else {
|
|||
|
WRITE_ONCE(b, 2);
|
|||
|
hacer_otra_cosa();
|
|||
|
}
|
|||
|
|
|||
|
Aún se requiere el inicial READ_ONCE() para evitar que el compilador toque
|
|||
|
el valor de 'a'.
|
|||
|
|
|||
|
Además, debe tener cuidado con lo que hace con la variable local 'q', de lo
|
|||
|
contrario, el compilador podría adivinar el valor y volver a eliminar el
|
|||
|
necesario condicional. Por ejemplo:
|
|||
|
|
|||
|
q = READ_ONCE(a);
|
|||
|
if (q % MAX) {
|
|||
|
WRITE_ONCE(b, 1);
|
|||
|
hacer_algo();
|
|||
|
} else {
|
|||
|
WRITE_ONCE(b, 2);
|
|||
|
hacer_otra_cosa();
|
|||
|
}
|
|||
|
|
|||
|
Si MAX se define como 1, entonces el compilador sabe que (q % MAX) es igual
|
|||
|
a cero, en cuyo caso el compilador tiene derecho a transformar el código
|
|||
|
anterior en el siguiente:
|
|||
|
|
|||
|
q = READ_ONCE(a);
|
|||
|
WRITE_ONCE(b, 2);
|
|||
|
hacer_otra_cosa();
|
|||
|
|
|||
|
Dada esta transformación, la CPU no está obligada a respetar el orden entre
|
|||
|
la carga de la variable 'a' y el store de la variable 'b'. Es tentador
|
|||
|
agregar una barrier(), pero esto no ayuda. El condicional se ha ido, y la
|
|||
|
barrera no lo traerá de vuelta. Por lo tanto, si confia en este orden, debe
|
|||
|
asegurarse de que MAX sea mayor que uno, tal vez de la siguiente manera:
|
|||
|
|
|||
|
q = READ_ONCE(a);
|
|||
|
BUILD_BUG_ON(MAX <= 1); /* Orden de carga de a con store de b */
|
|||
|
if (q % MAX) {
|
|||
|
WRITE_ONCE(b, 1);
|
|||
|
hacer_algo();
|
|||
|
} else {
|
|||
|
WRITE_ONCE(b, 2);
|
|||
|
hacer_otra_cosa();
|
|||
|
}
|
|||
|
|
|||
|
Tenga en cuenta una vez más que los stores de 'b' difieren. Si fueran
|
|||
|
idénticos, como se señaló anteriormente, el compilador podría sacar ese
|
|||
|
store fuera de la declaración 'if'.
|
|||
|
|
|||
|
También debe tener cuidado de no confiar demasiado en el cortocircuito
|
|||
|
de la evaluación booleana. Considere este ejemplo:
|
|||
|
|
|||
|
q = READ_ONCE(a);
|
|||
|
if (q || 1 > 0)
|
|||
|
WRITE_ONCE(b, 1);
|
|||
|
|
|||
|
Debido a que la primera condición no puede fallar y la segunda condición es
|
|||
|
siempre cierta, el compilador puede transformar este ejemplo de la
|
|||
|
siguiente manera, rompiendo la dependencia del control:
|
|||
|
|
|||
|
q = READ_ONCE(a);
|
|||
|
WRITE_ONCE(b, 1);
|
|||
|
|
|||
|
Este ejemplo subraya la necesidad de asegurarse de que el compilador no
|
|||
|
pueda adivinar su código. Más generalmente, aunque READ_ONCE() fuerza
|
|||
|
al compilador para emitir código para una carga dada, no fuerza al
|
|||
|
compilador para usar los resultados.
|
|||
|
|
|||
|
Además, las dependencias de control se aplican solo a la cláusula then y
|
|||
|
la cláusula else de la sentencia if en cuestión. En particular, no se
|
|||
|
aplica necesariamente al código que sigue a la declaración if:
|
|||
|
|
|||
|
q = READ_ONCE(a);
|
|||
|
if (q) {
|
|||
|
WRITE_ONCE(b, 1);
|
|||
|
} else {
|
|||
|
WRITE_ONCE(b, 2);
|
|||
|
}
|
|||
|
WRITE_ONCE(c, 1); /* BUG: No hay orden para la lectura de 'a'. */
|
|||
|
|
|||
|
Es tentador argumentar que, de hecho, existe un orden porque el compilador
|
|||
|
no puede reordenar accesos volátiles y tampoco puede reordenar escrituras
|
|||
|
en 'b' con la condición. Desafortunadamente para esta línea de
|
|||
|
razonamiento, el compilador podría compilar las dos escrituras en 'b' como
|
|||
|
instrucciones de movimiento condicional, como en este fantástico idioma
|
|||
|
pseudo-ensamblador:
|
|||
|
|
|||
|
ld r1,a
|
|||
|
cmp r1,$0
|
|||
|
cmov,ne r4,$1
|
|||
|
cmov,eq r4,$2
|
|||
|
st r4,b
|
|||
|
st $1,c
|
|||
|
|
|||
|
Una CPU débilmente ordenada no tendría dependencia de ningún tipo entre la
|
|||
|
carga de 'a' y el store de 'c'. Las dependencias de control se extenderían
|
|||
|
solo al par de instrucciones cmov y el store dependiente de ellas. En
|
|||
|
resumen, las dependencias de control se aplican solo a los stores en la
|
|||
|
cláusula then y la cláusula else de la sentencia if en cuestión (incluidas
|
|||
|
las funciones invocado por esas dos cláusulas), no al código que sigue a
|
|||
|
esa declaración if.
|
|||
|
|
|||
|
|
|||
|
Tenga muy en cuenta que el orden proporcionado por una dependencia de
|
|||
|
control es local a la CPU que lo contiene. Vea el apartado de "Atomicidad
|
|||
|
multicopia" para más información.
|
|||
|
|
|||
|
|
|||
|
En resumen:
|
|||
|
|
|||
|
(*) Las dependencias de control pueden ordenar cargas anteriores para
|
|||
|
stores posteriores. Sin embargo, no garantizan ningún otro tipo de
|
|||
|
orden: No cargas previas contra cargas posteriores, ni
|
|||
|
almacenamientos previos y luego nada. Si necesita tales formas de
|
|||
|
orden, use smp_rmb(), smp_wmb() o, en el caso de stores anteriores y
|
|||
|
cargas posteriores, smp_mb().
|
|||
|
|
|||
|
(*) Si ambos caminos de la declaración "if" comienzan con stores
|
|||
|
idénticos de la misma variable, entonces esos stores deben ser
|
|||
|
ordenados, ya sea precediéndoles a ambos con smp_mb() o usando
|
|||
|
smp_store_release() para realizar el store. Tenga en cuenta que -no-
|
|||
|
es suficiente usar barrier() al comienzo de cada caso de la
|
|||
|
declaración "if" porque, como se muestra en el ejemplo anterior, la
|
|||
|
optimización de los compiladores puede destruir la dependencia de
|
|||
|
control respetando al pie de la letra la ley de barrier().
|
|||
|
|
|||
|
(*) Las dependencias de control requieren al menos un condicional en
|
|||
|
tiempo de ejecución entre la carga anterior y el almacenamiento
|
|||
|
posterior, y este condicional debe implicar la carga previa. Si el
|
|||
|
compilador es capaz de optimizar el condicional y quitarlo, también
|
|||
|
habrá optimizado el ordenar. El uso cuidadoso de READ_ONCE() y
|
|||
|
WRITE_ONCE() puede ayudar a preservar el necesario condicional.
|
|||
|
|
|||
|
(*) Las dependencias de control requieren que el compilador evite
|
|||
|
reordenar las dependencia hasta su inexistencia. El uso cuidadoso de
|
|||
|
READ_ONCE() o atomic{,64}_read() puede ayudarle a preservar la
|
|||
|
dependencia de control. Consulte la sección BARRERA DEL COMPILADOR
|
|||
|
para obtener más información al respecto.
|
|||
|
|
|||
|
(*) Las dependencias de control se aplican solo a la cláusula then y la
|
|||
|
cláusula else de la sentencia "if" que contiene la dependencia de
|
|||
|
control, incluyendo cualquier función a la que llamen dichas dos
|
|||
|
cláusulas. Las dependencias de control no se aplican al código que
|
|||
|
sigue a la instrucción if que contiene la dependencia de control.
|
|||
|
|
|||
|
(*) Las dependencias de control se emparejan normalmente con otros tipos
|
|||
|
de barreras.
|
|||
|
|
|||
|
(*) Las dependencias de control no proporcionan atomicidad multicopia. Si
|
|||
|
usted necesita todas las CPU para ver un store dado al mismo tiempo,
|
|||
|
emplee smp_mb().
|
|||
|
|
|||
|
(*) Los compiladores no entienden las dependencias de control. Por lo
|
|||
|
tanto es su trabajo asegurarse de que no rompan su código.
|
|||
|
|
|||
|
|
|||
|
EMPAREJAMIENTO DE BARRERAS SMP
|
|||
|
------------------------------
|
|||
|
|
|||
|
Cuando se trata de interacciones CPU-CPU, ciertos tipos de barrera de
|
|||
|
memoria deben estar siempre emparejados. La falta del apropiado
|
|||
|
emparejamiento es casi seguro un error.
|
|||
|
|
|||
|
Las barreras generales se emparejan entre sí, aunque también se emparejan
|
|||
|
con la mayoría de otro tipo de barreras, aunque sin atomicidad multicopia.
|
|||
|
Una barrera de adquisición se empareja con una barrera de liberación, pero
|
|||
|
ambas también pueden emparejarse con otras barreras, incluidas, por
|
|||
|
supuesto, las barreras generales. Una barrera de escritura se empareja con
|
|||
|
una barrera de dependencia de dirección, una dependencia de control, una
|
|||
|
barrera de adquisición, una barrera de liberación, una barrera de lectura
|
|||
|
o una barrera general. Del mismo modo, una barrera de lectura se empareja
|
|||
|
con una de dependencia de control o barrera de dependencia de dirección con
|
|||
|
una barrera de escritura, una barrera de adquisición, una barrera de
|
|||
|
liberación o una barrera general:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
=============== ===============
|
|||
|
WRITE_ONCE(a, 1);
|
|||
|
<barrera de escritura>
|
|||
|
WRITE_ONCE(b, 2); x = READ_ONCE(b);
|
|||
|
<barrera de lectura>
|
|||
|
y = READ_ONCE(a);
|
|||
|
|
|||
|
O bien:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
=============== ===============================
|
|||
|
a = 1;
|
|||
|
<barrera de escritura>
|
|||
|
WRITE_ONCE(b, &a); x = READ_ONCE(b);
|
|||
|
<barrera de dependencia de dirección implícita>
|
|||
|
y = *x;
|
|||
|
|
|||
|
O incluso:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
=============== ===============================
|
|||
|
r1 = READ_ONCE(y);
|
|||
|
<barrera general>
|
|||
|
WRITE_ONCE(x, 1); if (r2 = READ_ONCE(x)) {
|
|||
|
<barrera de control implícita>
|
|||
|
WRITE_ONCE(y, 1);
|
|||
|
}
|
|||
|
|
|||
|
assert(r1 == 0 || r2 == 0);
|
|||
|
|
|||
|
Básicamente, la barrera de lectura siempre tiene que estar ahí, aunque
|
|||
|
puede ser del tipo "más débil".
|
|||
|
|
|||
|
[!] Tenga en cuenta que normalmente se esperaría que los stores antes de la
|
|||
|
barrera de escritura se hagan coincidir con los stores después de la
|
|||
|
barrera de lectura o la barrera de dependencia de dirección, y viceversa:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
=================== ===================
|
|||
|
WRITE_ONCE(a, 1); }---- --->{ v = READ_ONCE(c);
|
|||
|
WRITE_ONCE(b, 2); } \ / { w = READ_ONCE(d);
|
|||
|
<barrera de escritura> \ <barrera de lectura>
|
|||
|
WRITE_ONCE(c, 3); } / \ { x = READ_ONCE(a);
|
|||
|
WRITE_ONCE(d, 4); }---- --->{ y = READ_ONCE(b);
|
|||
|
|
|||
|
|
|||
|
EJEMPLOS DE SECUENCIAS DE BARRERA DE MEMORIA
|
|||
|
--------------------------------------------
|
|||
|
|
|||
|
En primer lugar, las barreras de escritura actúan como orden parcial en las
|
|||
|
operaciones de store. Considere la siguiente secuencia de eventos:
|
|||
|
|
|||
|
CPU 1
|
|||
|
=======================
|
|||
|
STORE A = 1
|
|||
|
STORE B = 2
|
|||
|
STORE C = 3
|
|||
|
<barrera de escritura>
|
|||
|
STORE D = 4
|
|||
|
STORE E = 5
|
|||
|
|
|||
|
Esta secuencia de eventos es finalizado para con el sistema de coherencia
|
|||
|
de memoria en un orden que el resto del sistema podría percibir como el
|
|||
|
conjunto desordenado { STORE A, STORE B, STORE C} todo ocurriendo antes del
|
|||
|
conjunto desordenado { STORE D, STORE E}:
|
|||
|
|
|||
|
|
|||
|
+-------+ : :
|
|||
|
| | +------+
|
|||
|
| |------>| C=3 | } /\
|
|||
|
| | : +------+ }----- \ -----> Eventos perceptibles para
|
|||
|
| | : | A=1 | } \/ el resto del sistema
|
|||
|
| | : +------+ }
|
|||
|
| CPU 1 | : | B=2 | }
|
|||
|
| | +------+ }
|
|||
|
| | wwwwwwwwwwwwwwww } <--- En este momento la barrera de
|
|||
|
| | +------+ } escritura requiere que todos los
|
|||
|
| | : | E=5 | } stores anteriores a la barrera
|
|||
|
| | : +------+ } sean confirmados antes de que otros
|
|||
|
| |------>| D=4 | } store puedan suceder
|
|||
|
| | +------+
|
|||
|
+-------+ : :
|
|||
|
|
|
|||
|
| Secuencia por la cual los stores son confirmados al
|
|||
|
| sistema de memoria por parte del CPU 1
|
|||
|
V
|
|||
|
|
|||
|
En segundo lugar, las barreras de dependencia de dirección actúan como
|
|||
|
órdenes parciales sobre la dirección de cargas dependientes. Considere la
|
|||
|
siguiente secuencia de eventos:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
======================= =======================
|
|||
|
{ B = 7; X = 9; Y = 8; C = &Y }
|
|||
|
STORE A = 1
|
|||
|
STORE B = 2
|
|||
|
<barrera de escritura>
|
|||
|
STORE C = &B LOAD X
|
|||
|
STORE D = 4 LOAD C (consigue &B)
|
|||
|
LOAD *C (lee B)
|
|||
|
|
|||
|
Sin intervención, la CPU 2 puede percibir los eventos en la CPU 1 en orden
|
|||
|
aleatorio a efectos prácticos, a pesar de la barrera de escritura emitida
|
|||
|
por la CPU 1:
|
|||
|
|
|||
|
+-------+ : : : :
|
|||
|
| | +------+ +-------+ | Secuencia de
|
|||
|
| |------>| B=2 |----- --->| Y->8 | | actualizado de
|
|||
|
| | : +------+ \ +-------+ | percepción en CPU 2
|
|||
|
| CPU 1 | : | A=1 | \ --->| C->&Y | V
|
|||
|
| | +------+ | +-------+
|
|||
|
| | wwwwwwwwwwwwwwww | : :
|
|||
|
| | +------+ | : :
|
|||
|
| | : | C=&B |--- | : : +-------+
|
|||
|
| | : +------+ \ | +-------+ | |
|
|||
|
| |------>| D=4 | ----------->| C->&B |------>| |
|
|||
|
| | +------+ | +-------+ | |
|
|||
|
+-------+ : : | : : | |
|
|||
|
| : : | |
|
|||
|
| : : | CPU 2 |
|
|||
|
| +-------+ | |
|
|||
|
Percepción de B ---> | | B->7 |------>| |
|
|||
|
aparentemente incorrecta! | +-------+ | |
|
|||
|
| : : | |
|
|||
|
| +-------+ | |
|
|||
|
La carga de X frena ---> \ | X->9 |------>| |
|
|||
|
el mantenimiento de \ +-------+ | |
|
|||
|
la coherencia de B ----->| B->2 | +-------+
|
|||
|
+-------+
|
|||
|
: :
|
|||
|
|
|||
|
|
|||
|
En el ejemplo anterior, la CPU 2 percibe que B es 7, a pesar de la carga de
|
|||
|
*C (que sería B) viniendo después del LOAD de C.
|
|||
|
|
|||
|
Sin embargo, si se colocara una barrera de dependencia de dirección entre
|
|||
|
la carga de C y la carga de *C (es decir: B) en la CPU 2:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
======================= =======================
|
|||
|
{ B = 7; X = 9; Y = 8; C = &Y }
|
|||
|
STORE A = 1
|
|||
|
STORE B = 2
|
|||
|
<barrera de escritura>
|
|||
|
STORE C = &B LOAD X
|
|||
|
STORE D = 4 LOAD C (consigue &B)
|
|||
|
<barrera de dependencia de dirección>
|
|||
|
LOAD *C (reads B)
|
|||
|
|
|||
|
entonces ocurrirá lo siguiente:
|
|||
|
|
|||
|
+-------+ : : : :
|
|||
|
| | +------+ +-------+
|
|||
|
| |------>| B=2 |----- --->| Y->8 |
|
|||
|
| | : +------+ \ +-------+
|
|||
|
| CPU 1 | : | A=1 | \ --->| C->&Y |
|
|||
|
| | +------+ | +-------+
|
|||
|
| | wwwwwwwwwwwwwwww | : :
|
|||
|
| | +------+ | : :
|
|||
|
| | : | C=&B |--- | : : +-------+
|
|||
|
| | : +------+ \ | +-------+ | |
|
|||
|
| |------>| D=4 | ----------->| C->&B |------>| |
|
|||
|
| | +------+ | +-------+ | |
|
|||
|
+-------+ : : | : : | |
|
|||
|
| : : | |
|
|||
|
| : : | CPU 2 |
|
|||
|
| +-------+ | |
|
|||
|
| | X->9 |------>| |
|
|||
|
| +-------+ | |
|
|||
|
Se asegura de que ---> \ aaaaaaaaaaaaaaaaa | |
|
|||
|
los efectos anteriores al \ +-------+ | |
|
|||
|
store de C sean percibidos ----->| B->2 |------>| |
|
|||
|
por los siguientes loads +-------+ | |
|
|||
|
: : +-------+
|
|||
|
|
|||
|
|
|||
|
Y en tercer lugar, una barrera de lectura actúa como un orden parcial sobre
|
|||
|
las cargas. Considere la siguiente secuencia de eventos:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
======================= =======================
|
|||
|
{ A = 0, B = 9 }
|
|||
|
STORE A=1
|
|||
|
<barrera de escritura>
|
|||
|
STORE B=2
|
|||
|
LOAD B
|
|||
|
LOAD A
|
|||
|
|
|||
|
Sin intervención, la CPU 2 puede elegir percibir los eventos en la CPU 1 en
|
|||
|
algún orden aleatorio a efectos prácticos, a pesar de la barrera de
|
|||
|
escritura emitida por la CPU 1:
|
|||
|
|
|||
|
+-------+ : : : :
|
|||
|
| | +------+ +-------+
|
|||
|
| |------>| A=1 |------ --->| A->0 |
|
|||
|
| | +------+ \ +-------+
|
|||
|
| CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 |
|
|||
|
| | +------+ | +-------+
|
|||
|
| |------>| B=2 |--- | : :
|
|||
|
| | +------+ \ | : : +-------+
|
|||
|
+-------+ : : \ | +-------+ | |
|
|||
|
---------->| B->2 |------>| |
|
|||
|
| +-------+ | CPU 2 |
|
|||
|
| | A->0 |------>| |
|
|||
|
| +-------+ | |
|
|||
|
| : : +-------+
|
|||
|
\ : :
|
|||
|
\ +-------+
|
|||
|
---->| A->1 |
|
|||
|
+-------+
|
|||
|
: :
|
|||
|
|
|||
|
Sin embargo, si se colocara una barrera de lectura entre la carga de B y la
|
|||
|
carga de A en la CPU 2:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
======================= =======================
|
|||
|
{ A = 0, B = 9 }
|
|||
|
STORE A=1
|
|||
|
<barrera de escritura>
|
|||
|
STORE B=2
|
|||
|
LOAD B
|
|||
|
<barrera de lectura>
|
|||
|
LOAD A
|
|||
|
|
|||
|
entonces el orden parcial impuesto por la CPU 1 será percibido
|
|||
|
correctamente por la CPU 2:
|
|||
|
|
|||
|
+-------+ : : : :
|
|||
|
| | +------+ +-------+
|
|||
|
| |------>| A=1 |------ --->| A->0 |
|
|||
|
| | +------+ \ +-------+
|
|||
|
| CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 |
|
|||
|
| | +------+ | +-------+
|
|||
|
| |------>| B=2 |--- | : :
|
|||
|
| | +------+ \ | : : +-------+
|
|||
|
+-------+ : : \ | +-------+ | |
|
|||
|
---------->| B->2 |------>| |
|
|||
|
| +-------+ | CPU 2 |
|
|||
|
| : : | |
|
|||
|
| : : | |
|
|||
|
En este punto la barrera ----> \ rrrrrrrrrrrrrrrrr | |
|
|||
|
de lectura consigue que \ +-------+ | |
|
|||
|
todos los efectos anteriores ---->| A->1 |------>| |
|
|||
|
al almacenamiento de B sean +-------+ | |
|
|||
|
perceptibles por la CPU 2 : : +-------+
|
|||
|
|
|||
|
|
|||
|
Para ilustrar esto de manera más completa, considere lo que podría pasar si
|
|||
|
el código contenía una carga de A a cada lado de la barrera de lectura:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
======================= =======================
|
|||
|
{ A = 0, B = 9 }
|
|||
|
STORE A=1
|
|||
|
<barrera de escritura>
|
|||
|
STORE B=2
|
|||
|
LOAD B
|
|||
|
LOAD A [primer load de A]
|
|||
|
<rbarrera de lectura>
|
|||
|
LOAD A [segundo load de A]
|
|||
|
|
|||
|
Aunque las dos cargas de A ocurren después de la carga de B, ambas pueden
|
|||
|
obtener diferentes valores:
|
|||
|
|
|||
|
+-------+ : : : :
|
|||
|
| | +------+ +-------+
|
|||
|
| |------>| A=1 |------ --->| A->0 |
|
|||
|
| | +------+ \ +-------+
|
|||
|
| CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 |
|
|||
|
| | +------+ | +-------+
|
|||
|
| |------>| B=2 |--- | : :
|
|||
|
| | +------+ \ | : : +-------+
|
|||
|
+-------+ : : \ | +-------+ | |
|
|||
|
---------->| B->2 |------>| |
|
|||
|
| +-------+ | CPU 2 |
|
|||
|
| : : | |
|
|||
|
| : : | |
|
|||
|
| +-------+ | |
|
|||
|
| | A->0 |------>| 1st |
|
|||
|
| +-------+ | |
|
|||
|
En este punto la barrera ----> \ rrrrrrrrrrrrrrrrr | |
|
|||
|
de lectura consigue que \ +-------+ | |
|
|||
|
todos los efectos anteriores ---->| A->1 |------>| |
|
|||
|
al almacenamiento de B sean +-------+ | |
|
|||
|
perceptibles por la CPU 2 : : +-------+
|
|||
|
|
|||
|
Pero puede ser que la actualización a A desde la CPU 1 se vuelva
|
|||
|
perceptible para la CPU 2 antes de que la barrera de lectura se complete de
|
|||
|
todos modos:
|
|||
|
|
|||
|
+-------+ : : : :
|
|||
|
| | +------+ +-------+
|
|||
|
| |------>| A=1 |------ --->| A->0 |
|
|||
|
| | +------+ \ +-------+
|
|||
|
| CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 |
|
|||
|
| | +------+ | +-------+
|
|||
|
| |------>| B=2 |--- | : :
|
|||
|
| | +------+ \ | : : +-------+
|
|||
|
+-------+ : : \ | +-------+ | |
|
|||
|
---------->| B->2 |------>| |
|
|||
|
| +-------+ | CPU 2 |
|
|||
|
| : : | |
|
|||
|
\ : : | |
|
|||
|
\ +-------+ | |
|
|||
|
---->| A->1 |------>| 1st |
|
|||
|
+-------+ | |
|
|||
|
rrrrrrrrrrrrrrrrr | |
|
|||
|
+-------+ | |
|
|||
|
| A->1 |------>| 2nd |
|
|||
|
+-------+ | |
|
|||
|
: : +-------+
|
|||
|
|
|||
|
La garantía es que la segunda carga siempre dará como resultado A == 1 si
|
|||
|
la carga de B resultó en B == 2. No existe tal garantía para la primera
|
|||
|
carga de A; esto puede dar como resultado A == 0 o A == 1.
|
|||
|
|
|||
|
|
|||
|
BARRERAS DE MEMORIA DE LECTURA FRENTE A ESPECULACIÓN DE CARGA
|
|||
|
-------------------------------------------------------------
|
|||
|
|
|||
|
Muchas CPU especulan con las cargas: es decir, ven que necesitarán cargar
|
|||
|
un elemento de la memoria, y encuentran un momento en el que no están
|
|||
|
usando el bus para ningún otra carga, y también en la carga por adelantado,
|
|||
|
aunque en realidad no lo hayan llegado a ese punto en el flujo de ejecución
|
|||
|
de instrucciones todavía. Esto permite que la instrucción de carga real
|
|||
|
potencialmente complete de inmediato, porque la CPU ya tiene el valor a
|
|||
|
mano.
|
|||
|
|
|||
|
Puede resultar que la CPU en realidad no necesitara el valor, tal vez
|
|||
|
porque una condición eludió la carga, en cuyo caso puede descartar el valor
|
|||
|
o simplemente almacenar en caché para su uso posterior.
|
|||
|
|
|||
|
Considere:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
======================= =======================
|
|||
|
LOAD B
|
|||
|
DIVIDE } Instrucciones de división
|
|||
|
DIVIDE } tardan mucho en terminar
|
|||
|
LOAD A
|
|||
|
|
|||
|
donde DIVIDE es DIVIDIR. Que podría aparecer como esto:
|
|||
|
|
|||
|
: : +-------+
|
|||
|
+-------+ | |
|
|||
|
--->| B->2 |------>| |
|
|||
|
+-------+ | CPU 2 |
|
|||
|
: :DIVIDE | |
|
|||
|
+-------+ | |
|
|||
|
La CPU ocupada con la división ---> --->| A->0 |~~~~ | |
|
|||
|
especula sobre el LOAD de A +-------+ ~ | |
|
|||
|
: : ~ | |
|
|||
|
: :DIVIDE | |
|
|||
|
: : ~ | |
|
|||
|
Una vez completadas las divisiones --> : : ~-->| |
|
|||
|
la CPU puede realizar el : : | |
|
|||
|
LOAD con efecto inmediato : : +-------+
|
|||
|
|
|||
|
|
|||
|
Colocando una barrera de lectura o una barrera de dependencia de dirección
|
|||
|
justo antes de la segundo carga:
|
|||
|
|
|||
|
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
======================= =======================
|
|||
|
LOAD B
|
|||
|
DIVIDE
|
|||
|
DIVIDE
|
|||
|
<rbarrera de lectura>
|
|||
|
LOAD A
|
|||
|
|
|||
|
obligará a reconsiderar cualquier valor obtenido especulativamente en una
|
|||
|
medida dependiente del tipo de barrera utilizada. Si no se hizo ningún
|
|||
|
cambio en la ubicación de memoria especulada, entonces el valor especulado
|
|||
|
solo se usará:
|
|||
|
|
|||
|
: : +-------+
|
|||
|
+-------+ | |
|
|||
|
--->| B->2 |------>| |
|
|||
|
+-------+ | CPU 2 |
|
|||
|
: :DIVIDE | |
|
|||
|
+-------+ | |
|
|||
|
La CPU ocupada con la división ---> --->| A->0 |~~~~ | |
|
|||
|
especula sobre el LOAD de A +-------+ ~ | |
|
|||
|
: : ~ | |
|
|||
|
: :DIVIDE | |
|
|||
|
: : ~ | |
|
|||
|
: : ~ | |
|
|||
|
rrrrrrrrrrrrrrrr~ | |
|
|||
|
: : ~ | |
|
|||
|
: : ~-->| |
|
|||
|
: : | |
|
|||
|
: : +-------+
|
|||
|
|
|||
|
|
|||
|
pero si había una actualización o una invalidación de otra CPU pendiente,
|
|||
|
entonces la especulación será cancelada y el valor recargado:
|
|||
|
|
|||
|
: : +-------+
|
|||
|
+-------+ | |
|
|||
|
--->| B->2 |------>| |
|
|||
|
+-------+ | CPU 2 |
|
|||
|
: :DIVIDE | |
|
|||
|
+-------+ | |
|
|||
|
La CPU ocupada con la división ---> --->| A->0 |~~~~ | |
|
|||
|
especula sobre el LOAD de A +-------+ ~ | |
|
|||
|
: : ~ | |
|
|||
|
: :DIVIDE | |
|
|||
|
: : ~ | |
|
|||
|
: : ~ | |
|
|||
|
rrrrrrrrrrrrrrrrr | |
|
|||
|
+-------+ | |
|
|||
|
La especulación es descartada ---> --->| A->1 |------>| |
|
|||
|
y un valor actualizado +-------+ | |
|
|||
|
es conseguido : : +-------+
|
|||
|
|
|||
|
ATOMICIDAD MULTICOPIA
|
|||
|
---------------------
|
|||
|
|
|||
|
La atomicidad multicopia es una noción profundamente intuitiva sobre el
|
|||
|
orden que no es siempre proporcionada por los sistemas informáticos reales,
|
|||
|
a saber, que un determinada store se vuelve visible al mismo tiempo para
|
|||
|
todos las CPU o, alternativamente, que todas las CPU acuerdan el orden en
|
|||
|
que todos los stores se vuelven visibles. Sin embargo, el soporte para
|
|||
|
atomicidad multicopia completa descartaría valiosas optimizaciones
|
|||
|
hardware, por lo que una versión más débil conocida como ``otra atomicidad
|
|||
|
multicopia'' en cambio, solo garantiza que un store dado se vuelva visible
|
|||
|
al mismo tiempo en todas las -otras- CPUs. El resto de este documento
|
|||
|
discute esta versión más débil, pero por brevedad lo llamaremos simplemente
|
|||
|
``atomicidad multicopia''.
|
|||
|
|
|||
|
El siguiente ejemplo demuestra la atomicidad multicopia:
|
|||
|
|
|||
|
CPU 1 CPU 2 CPU 3
|
|||
|
======================= ======================= =======================
|
|||
|
{ X = 0, Y = 0 }
|
|||
|
STORE X=1 r1=LOAD X (reads 1) LOAD Y (reads 1)
|
|||
|
<barrera general> <barrera de lectura>
|
|||
|
STORE Y=r1 LOAD X
|
|||
|
|
|||
|
Suponga que la carga de la CPU 2 desde X devuelve 1, que luego almacena en
|
|||
|
Y, y la carga de la CPU 3 desde Y devuelve 1. Esto indica que el store de
|
|||
|
la CPU 1 a X precede a la carga de la CPU 2 desde X y el store de esa CPU 2
|
|||
|
a Y precede la carga de la CPU 3 desde Y. Además, las barreras de memoria
|
|||
|
garantizan que la CPU 2 ejecuta su carga antes que su almacenamiento, y la
|
|||
|
CPU 3 carga desde Y antes de cargar desde X. La pregunta entonces es
|
|||
|
"¿Puede la carga de la CPU 3 desde X devolver 0?"
|
|||
|
|
|||
|
Debido a que la carga de la CPU 3 desde X en cierto sentido viene después
|
|||
|
de la carga de la CPU 2, es natural esperar que la carga de la CPU 3 desde
|
|||
|
X deba devolver 1. Esta expectativa se deriva de la atomicidad multicopia:
|
|||
|
si una carga que se ejecuta en la CPU B sigue una carga de la misma
|
|||
|
variable que se ejecuta en la CPU A (y la CPU A no almacenó originalmente
|
|||
|
el valor que leyó), entonces en sistemas atómicos multicopia, la carga de
|
|||
|
la CPU B debe devolver el mismo valor que hizo la carga de la CPU A o algún
|
|||
|
valor posterior. Sin embargo, el kernel Linux no requiere que los sistemas
|
|||
|
sean atómicos multicopia.
|
|||
|
|
|||
|
El uso de una barrera de memoria general en el ejemplo anterior compensa
|
|||
|
cualquier falta de atomicidad multicopia. En el ejemplo, si la carga de la
|
|||
|
CPU 2 de X devuelve 1 y la carga de la CPU 3 de Y devuelve 1, entonces la
|
|||
|
carga de la CPU 3 desde X debe de hecho también devolver 1.
|
|||
|
|
|||
|
Sin embargo, las dependencias, las barreras de lectura y las barreras de
|
|||
|
escritura no siempre son capaces de compensar la atomicidad no multicopia.
|
|||
|
Por ejemplo, supongamos que la barrera general de la CPU 2 se elimina del
|
|||
|
ejemplo anterior, dejando solo la dependencia de datos que se muestra a
|
|||
|
continuación:
|
|||
|
|
|||
|
CPU 1 CPU 2 CPU 3
|
|||
|
======================= ======================= =======================
|
|||
|
{ X = 0, Y = 0 }
|
|||
|
STORE X=1 r1=LOAD X (escribe 1) LOAD Y (lee 1)
|
|||
|
<dependencia de datos> <barrera de lectura>
|
|||
|
STORE Y=r1 LOAD X (lee 0)
|
|||
|
|
|||
|
Esta sustitución permite que la atomicidad no multicopia se desenfrene: en
|
|||
|
este ejemplo, es perfectamente legal que la carga de la CPU 2 desde X
|
|||
|
devuelva 1, la carga de la CPU 3 desde Y devuelva 1, y su carga desde X
|
|||
|
tenga valor 0.
|
|||
|
|
|||
|
El punto clave es que aunque la dependencia de datos de la CPU 2 ordena su
|
|||
|
carga y store, no garantiza ordenar el store de la CPU 1. De forma que, si
|
|||
|
este ejemplo se ejecuta en un sistema atómico no multicopia donde las CPU 1
|
|||
|
y 2 comparten un buffer de almacenamiento o un nivel de caché, la CPU 2
|
|||
|
podría tener acceso anticipado de escritura a CPU 1. Por lo tanto, se
|
|||
|
requieren barreras generales para garantizar que todas las CPU acurden el
|
|||
|
orden combinado de accesos múltiples.
|
|||
|
|
|||
|
Las barreras generales pueden compensar no solo la atomicidad no
|
|||
|
multicopia, pero también pueden generar orden adicional que puede asegurar
|
|||
|
que -todas- las CPU percibirán el mismo orden de -todas- las operaciones.
|
|||
|
Por el contrario, una cadena de parejas de liberación-adquisición no
|
|||
|
proporciona este orden adicional, lo que significa que solo se garantiza
|
|||
|
que las CPU de la cadena estén de acuerdo en el orden combinado de los
|
|||
|
accesos. Por ejemplo, cambiando a código C en deferencia al fantasma de
|
|||
|
Herman Hollerith:
|
|||
|
|
|||
|
int u, v, x, y, z;
|
|||
|
|
|||
|
void cpu0(void)
|
|||
|
{
|
|||
|
r0 = smp_load_acquire(&x);
|
|||
|
WRITE_ONCE(u, 1);
|
|||
|
smp_store_release(&y, 1);
|
|||
|
}
|
|||
|
|
|||
|
void cpu1(void)
|
|||
|
{
|
|||
|
r1 = smp_load_acquire(&y);
|
|||
|
r4 = READ_ONCE(v);
|
|||
|
r5 = READ_ONCE(u);
|
|||
|
smp_store_release(&z, 1);
|
|||
|
}
|
|||
|
|
|||
|
void cpu2(void)
|
|||
|
{
|
|||
|
r2 = smp_load_acquire(&z);
|
|||
|
smp_store_release(&x, 1);
|
|||
|
}
|
|||
|
|
|||
|
void cpu3(void)
|
|||
|
{
|
|||
|
WRITE_ONCE(v, 1);
|
|||
|
smp_mb();
|
|||
|
r3 = READ_ONCE(u);
|
|||
|
}
|
|||
|
|
|||
|
Dado que cpu0(), cpu1() y cpu2() participan en una cadena de parejas
|
|||
|
smp_store_release()/smp_load_acquire(), el siguiente resultado estaría
|
|||
|
prohibido:
|
|||
|
|
|||
|
r0 == 1 && r1 == 1 && r2 == 1
|
|||
|
|
|||
|
Además, debido a la relación liberación-adquisición entre cpu0() y cpu1(),
|
|||
|
cpu1() debe ver las escrituras de cpu0(), de modo que el siguiente
|
|||
|
resultado estaría prohibido:
|
|||
|
|
|||
|
r1 == 1 && r5 == 0
|
|||
|
|
|||
|
Sin embargo, el orden proporcionado por una cadena de
|
|||
|
liberación-adquisición es local a las CPU que participan en esa cadena y no
|
|||
|
se aplica a cpu3(), al menos aparte de los stores. Por lo tanto, es posible
|
|||
|
el siguiente resultado:
|
|||
|
|
|||
|
r0 == 0 && r1 == 1 && r2 == 1 && r3 == 0 && r4 == 0
|
|||
|
|
|||
|
Por otro lado, también el siguiente resultado es posible:
|
|||
|
|
|||
|
r0 == 0 && r1 == 1 && r2 == 1 && r3 == 0 && r4 == 0 && r5 == 1
|
|||
|
|
|||
|
Aunque cpu0(), cpu1() y cpu2() verán sus respectivas lecturas y escrituras
|
|||
|
en orden, las CPU que no participan en la cadena de liberación-adquisición
|
|||
|
pueden estar en desacuerdo con el orden. Este desacuerdo se debe al hecho
|
|||
|
de que las instrucciones de barrera de memoria débiles utilizadas para
|
|||
|
implementar smp_load_acquire() y smp_store_release() no son necesarios para
|
|||
|
ordenar stores anteriores contra cargas posteriores en todos los casos.
|
|||
|
Esto significa que cpu3() puede ver el store de cpu0() suceder -después- de
|
|||
|
la carga de cpu1() desde v, aunque tanto cpu0() como cpu1() están de
|
|||
|
acuerdo en que estas dos operaciones ocurrieron en el orden previsto.
|
|||
|
|
|||
|
Sin embargo, tenga en cuenta que smp_load_acquire() no es mágico. En
|
|||
|
particular, simplemente lee de su argumento en orden. Es decir, -no-
|
|||
|
asegura que se leerá cualquier valor en particular. Por lo tanto, los
|
|||
|
siguiente resultados son posibles:
|
|||
|
|
|||
|
r0 == 0 && r1 == 0 && r2 == 0 && r5 == 0
|
|||
|
|
|||
|
Tenga en cuenta que este resultado puede ocurrir incluso en un mítico
|
|||
|
sistema, consistente en secuencia, donde nunca se reordena nada.
|
|||
|
|
|||
|
Para reiterar, si su código requiere un orden completo de todas las
|
|||
|
operaciones, utilice barreras generales en todo momento.
|
|||
|
|
|||
|
|
|||
|
==============================
|
|||
|
BARRERAS EXPLÍCITAS DEL KERNEL
|
|||
|
==============================
|
|||
|
|
|||
|
El kernel Linux tiene una variedad de diferentes barreras que actúan a
|
|||
|
diferentes niveles:
|
|||
|
|
|||
|
(*) Barrera del compilador.
|
|||
|
|
|||
|
(*) Barreras de memoria de la CPU.
|
|||
|
|
|||
|
|
|||
|
BARRERA DEL COMPILADOR
|
|||
|
-----------------------
|
|||
|
|
|||
|
El kernel de Linux tiene una función de barrera del compilador explícita
|
|||
|
que evita que el el compilador mueva los accesos a la memoria de cualquier
|
|||
|
lado al otro:
|
|||
|
|
|||
|
barrier();
|
|||
|
|
|||
|
Esta es una barrera general: no hay variantes de barrier() para casos de
|
|||
|
lectura-lectura o escritura-escritura. Sin embargo, READ_ONCE() y
|
|||
|
WRITE_ONCE() pueden ser considerado como formas débiles de barrier() que
|
|||
|
afectan solo específicos accesos marcados por READ_ONCE() o WRITE_ONCE().
|
|||
|
|
|||
|
La función barrier() produce los siguientes efectos:
|
|||
|
|
|||
|
(*) Evita que el compilador reordene los accesos tras barrier() para
|
|||
|
preceder a cualquier acceso que preceda a barrier(). Un ejemplo de uso
|
|||
|
de esta propiedad es facilitar la comunicación entre código del
|
|||
|
interrupt-handler (encargo de gestionar interrupciones) y el código
|
|||
|
que fue interrumpido.
|
|||
|
|
|||
|
(*) Dentro de un bucle ("loop"), obliga al compilador a cargar las
|
|||
|
variables utilizadas en ese loop condicional en cada paso a través de
|
|||
|
ese loop.
|
|||
|
|
|||
|
Las funciones READ_ONCE() y WRITE_ONCE() pueden evitar cualquier cantidad
|
|||
|
de optimizaciones que, si bien son perfectamente seguras en código de un
|
|||
|
solo subproceso, pueden resultar fatales en código concurrente. Aquí hay
|
|||
|
algunos ejemplos de tal tipo de optimizaciones:
|
|||
|
|
|||
|
(*) El compilador está en su derecho de reordenar cargas y stores de la
|
|||
|
misma variable, y en algunos casos, la CPU está dentro de su
|
|||
|
derecho de reordenar cargas a la misma variable. Esto significa que
|
|||
|
el siguiente código:
|
|||
|
|
|||
|
a[0] = x;
|
|||
|
a[1] = x;
|
|||
|
|
|||
|
Podría resultar en un valor más antiguo de x almacenado en a[1] que en
|
|||
|
a[0]. Evite que tanto el compilador como la CPU hagan esto de la
|
|||
|
siguiente manera:
|
|||
|
|
|||
|
a[0] = READ_ONCE(x);
|
|||
|
a[1] = READ_ONCE(x);
|
|||
|
|
|||
|
En resumen, READ_ONCE() y WRITE_ONCE() proporcionan coherencia de
|
|||
|
caché para accesos desde múltiples CPUs a una sola variable.
|
|||
|
|
|||
|
(*) El compilador tiene derecho a juntar cargas sucesivas de la misma
|
|||
|
variable. Tal fusión puede hacer que el compilador "optimice" el
|
|||
|
siguiente código:
|
|||
|
|
|||
|
while (tmp = a)
|
|||
|
hacer_algo_con(tmp);
|
|||
|
|
|||
|
en el siguiente código, que, aunque en cierto sentido es legítimo
|
|||
|
para un código de un solo subproceso, es casi seguro que no es lo
|
|||
|
que el desarrollador pretendía:
|
|||
|
|
|||
|
if (tmp = a)
|
|||
|
for (;;)
|
|||
|
hacer_algo_con(tmp);
|
|||
|
|
|||
|
Use READ_ONCE() para evitar que el compilador le haga esto:
|
|||
|
|
|||
|
while (tmp = READ_ONCE(a))
|
|||
|
hacer_algo_con(tmp);
|
|||
|
|
|||
|
(*) El compilador tiene derecho a recargar una variable, por ejemplo,
|
|||
|
en los casos en que la alta presión de los registros impida que el
|
|||
|
compilador mantenga todos los datos de interés en registros. El
|
|||
|
compilador podría por lo tanto, optimizar la variable 'tmp' de nuestro
|
|||
|
ejemplo anterior:
|
|||
|
|
|||
|
while (tmp = a)
|
|||
|
hacer_algo_con(tmp);
|
|||
|
|
|||
|
Esto podría resultar en el siguiente código, que es perfectamente
|
|||
|
seguro en código de subproceso único, pero puede ser fatal en código
|
|||
|
concurrente:
|
|||
|
|
|||
|
while (a)
|
|||
|
hacer_algo_con(a);
|
|||
|
|
|||
|
Por ejemplo, la versión optimizada de este código podría resultar en
|
|||
|
pasar un cero a hacer_algo_con() en el caso de que la variable a sea
|
|||
|
modificada por alguna otra CPU, entre la instrucción "while" y la
|
|||
|
llamada a hacer_algo_con().
|
|||
|
|
|||
|
De nuevo, use READ_ONCE() para evitar que el compilador haga esto:
|
|||
|
|
|||
|
while (tmp = READ_ONCE(a))
|
|||
|
hacer_algo_con(tmp);
|
|||
|
|
|||
|
Tenga en cuenta que si el compilador se queda sin registros, podría
|
|||
|
guardar tmp en la pila ("stack"). El overhead (coste en eficiencia) de
|
|||
|
este guardado y posterior restauración es por lo que los compiladores
|
|||
|
recargan las variables. Hacerlo es perfectamente seguro para código de
|
|||
|
subproceso único, por lo que debe informar al compilador sobre los
|
|||
|
casos donde no sea seguro.
|
|||
|
|
|||
|
(*) El compilador está en su derecho de omitir una carga por completo si
|
|||
|
sabe cual será su valor. Por ejemplo, si el compilador puede probar
|
|||
|
que el valor de la variable 'a' siempre es cero, puede optimizar este
|
|||
|
código:
|
|||
|
|
|||
|
while (tmp = a)
|
|||
|
hacer_algo_con(tmp);
|
|||
|
|
|||
|
En esto:
|
|||
|
|
|||
|
do { } while (0);
|
|||
|
|
|||
|
Esta transformación es una victoria para un código de un solo
|
|||
|
subproceso, porque se deshace de una carga y un branch. El problema es
|
|||
|
que el compilador llevará a cabo su prueba asumiendo que la CPU actual
|
|||
|
es la única actualizando la variable 'a'. Si la variable 'a' es
|
|||
|
compartida, entonces la prueba del compilador será errónea. Use
|
|||
|
READ_ONCE() para decirle al compilador que no sabe tanto como cree:
|
|||
|
|
|||
|
while (tmp = READ_ONCE(a))
|
|||
|
hacer_algo_con(tmp);
|
|||
|
|
|||
|
Pero, por favor, tenga en cuenta que el compilador también está
|
|||
|
observando de cerca lo que usted hace con el valor después de
|
|||
|
READ_ONCE(). Por ejemplo, suponga que Ud. hace lo siguiente y MAX es
|
|||
|
una macro de preprocesador con el valor 1:
|
|||
|
|
|||
|
while ((tmp = READ_ONCE(a)) % MAX)
|
|||
|
hacer_algo_con(tmp);
|
|||
|
|
|||
|
Entonces el compilador sabe que el resultado del operador "%" aplicado
|
|||
|
a MAX siempre será cero, nuevamente permitiendo que el compilador
|
|||
|
optimice el código hasta su casi inexistencia. (Aún se cargará desde
|
|||
|
la variable 'a'.)
|
|||
|
|
|||
|
(*) De manera similar, el compilador tiene derecho a omitir un store por
|
|||
|
completo si sabe que la variable ya tiene el valor almacenado.
|
|||
|
Nuevamente, el compilador asume que la CPU actual es la única que
|
|||
|
almacena la variable, lo que puede hacer que el compilador haga
|
|||
|
algo incorrecto para las variables compartidas. Por ejemplo, suponga
|
|||
|
que tiene lo siguiente:
|
|||
|
|
|||
|
a = 0;
|
|||
|
... Código que no almacena la variable a ...
|
|||
|
a = 0;
|
|||
|
|
|||
|
El compilador observa que el valor de la variable 'a' ya es cero, por
|
|||
|
lo que bien podría omitir el segundo store. Esto supondría una fatal
|
|||
|
sorpresa, si alguna otra CPU hubiera almacenado la variable 'a'
|
|||
|
mientras tanto.
|
|||
|
|
|||
|
Use WRITE_ONCE() para evitar que el compilador haga este tipo de
|
|||
|
suposición equivocada:
|
|||
|
|
|||
|
WRITE_ONCE(a, 0);
|
|||
|
... Código que no almacena la variable a ...
|
|||
|
WRITE_ONCE(a, 0);
|
|||
|
|
|||
|
(*) El compilador tiene derecho a reordenar los accesos a memoria a menos
|
|||
|
que le diga que no. Por ejemplo, considere la siguiente interacción
|
|||
|
entre el código de nivel de proceso y un controlador de interrupción:
|
|||
|
|
|||
|
void nivel_de_procesamiento(void)
|
|||
|
{
|
|||
|
msg = ACQUIRE_mensaje();
|
|||
|
flag = true;
|
|||
|
}
|
|||
|
|
|||
|
void controlador_interrupcion(void)
|
|||
|
{
|
|||
|
if (flag)
|
|||
|
procesar_mensaje(msg);
|
|||
|
}
|
|||
|
|
|||
|
No hay nada que impida que el compilador transforme
|
|||
|
nivel_de_procesamiento() a lo siguiente, que de hecho, bien podría ser
|
|||
|
una victoria para código de un solo subproceso:
|
|||
|
|
|||
|
void nivel_de_procesamiento(void)
|
|||
|
{
|
|||
|
flag = true;
|
|||
|
msg = ACQUIRE_mensaje();
|
|||
|
}
|
|||
|
|
|||
|
Si la interrupción ocurre entre estas dos declaraciones, entonces
|
|||
|
controlador_interrupcion() podría recibir un mensaje ilegible. Use
|
|||
|
READ_ONCE() para evitar esto de la siguiente manera:
|
|||
|
|
|||
|
void nivel_de_procesamiento(void)
|
|||
|
{
|
|||
|
WRITE_ONCE(msg, ACQUIRE_mensaje());
|
|||
|
WRITE_ONCE(flag, true);
|
|||
|
}
|
|||
|
|
|||
|
void controlador_interrupcion(void)
|
|||
|
{
|
|||
|
if (READ_ONCE(flag))
|
|||
|
procesar_mensaje(READ_ONCE(msg));
|
|||
|
}
|
|||
|
|
|||
|
Tenga en cuenta que los envoltorios ("wrappers") READ_ONCE() y
|
|||
|
WRITE_ONCE() en controlador_interrupcion() son necesarios si este
|
|||
|
controlador de interrupciones puede ser interrumpido por algo que
|
|||
|
también accede a 'flag' y 'msg', por ejemplo, una interrupción anidada
|
|||
|
o un NMI. De lo contrario, READ_ONCE() y WRITE_ONCE() no son
|
|||
|
necesarios en controlador_interrupcion() aparte de con fines de
|
|||
|
documentación. (Tenga también en cuenta que las interrupciones
|
|||
|
anidadas no ocurren típicamente en los kernels Linux modernos, de
|
|||
|
hecho, si un controlador de interrupciones regresa con interrupciones
|
|||
|
habilitadas, obtendrá un WARN_ONCE().)
|
|||
|
|
|||
|
Debe suponer que el compilador puede mover READ_ONCE() y WRITE_ONCE()
|
|||
|
a código que no contiene READ_ONCE(), WRITE_ONCE(), barrier(), o
|
|||
|
primitivas similares.
|
|||
|
|
|||
|
Este efecto también podría lograrse usando barrier(), pero READ_ONCE()
|
|||
|
y WRITE_ONCE() son más selectivos: Con READ_ONCE() y WRITE_ONCE(), el
|
|||
|
compilador solo necesita olvidar el contenido de ubicaciones de
|
|||
|
memoria indicadas, mientras que con barrier() el compilador debe
|
|||
|
descartar el valor de todas las ubicaciones de memoria que tiene
|
|||
|
actualmente almacenadas en caché, en cualquier registro de la máquina.
|
|||
|
Por supuesto, el compilador también debe respetar el orden en que
|
|||
|
ocurren READ_ONCE() y WRITE_ONCE(), aunque la CPU, efectivamente, no
|
|||
|
necesita hacerlo.
|
|||
|
|
|||
|
(*) El compilador tiene derecho a inventar stores para una variable,
|
|||
|
como en el siguiente ejemplo:
|
|||
|
|
|||
|
if (a)
|
|||
|
b = a;
|
|||
|
else
|
|||
|
b = 42;
|
|||
|
|
|||
|
El compilador podría ahorrar un branch al optimizar esto de la
|
|||
|
siguiente manera:
|
|||
|
|
|||
|
b = 42;
|
|||
|
if (a)
|
|||
|
b = a;
|
|||
|
|
|||
|
En el código de un solo subproceso, esto no solo es seguro, sino que
|
|||
|
también ahorra un branch. Desafortunadamente, en código concurrente,
|
|||
|
esta optimización podría causar que alguna otra CPU vea un valor falso
|
|||
|
de 42, incluso si la variable 'a' nunca fue cero, al cargar la
|
|||
|
variable 'b'. Use WRITE_ONCE() para evitar esto de la siguiente
|
|||
|
manera:
|
|||
|
|
|||
|
if (a)
|
|||
|
WRITE_ONCE(b, a);
|
|||
|
else
|
|||
|
WRITE_ONCE(b, 42);
|
|||
|
|
|||
|
El compilador también puede inventar cargas. Estos casos suelen ser
|
|||
|
menos perjudiciales, pero pueden dar como resultado "bouncing" de la
|
|||
|
línea de caché y, por lo tanto, bajo rendimiento y escalabilidad.
|
|||
|
Utilice READ_ONCE() para evitar cargas inventadas.
|
|||
|
|
|||
|
(*) Para ubicaciones de memoria alineadas cuyo tamaño les permita
|
|||
|
acceder con una sola instrucción de referencia de memoria, evite el
|
|||
|
"desgarro de la carga" (load tearing) y "desgarro del store" (store
|
|||
|
tearing), en el que un solo gran acceso es reemplazado por múltiples
|
|||
|
accesos menores. Por ejemplo, dada una arquitectura que tiene
|
|||
|
instrucciones de almacenamiento de 16 bits con campos inmediatos de 7
|
|||
|
bits, el compilador podría tener la tentación de usar dos
|
|||
|
instrucciones inmediatas de almacenamiento de 16 bits para implementar
|
|||
|
el siguiente store de 32 bits:
|
|||
|
|
|||
|
p = 0x00010002;
|
|||
|
|
|||
|
Tenga en cuenta que GCC realmente usa este tipo de optimización, lo
|
|||
|
cual no es sorprendente dado que probablemente costaría más de dos
|
|||
|
instrucciones el construir la constante y luego almacenarla. Por lo
|
|||
|
tanto, esta optimización puede ser una victoria en un código de un
|
|||
|
solo subproceso. De hecho, un error reciente (desde que se solucionó)
|
|||
|
hizo que GCC usara incorrectamente esta optimización en un store
|
|||
|
volátil. En ausencia de tales errores, el uso de WRITE_ONCE() evita el
|
|||
|
desgarro del store en el siguiente ejemplo:
|
|||
|
|
|||
|
struct __attribute__((__packed__)) foo {
|
|||
|
short a;
|
|||
|
int b;
|
|||
|
short c;
|
|||
|
};
|
|||
|
struct foo foo1, foo2;
|
|||
|
...
|
|||
|
|
|||
|
foo2.a = foo1.a;
|
|||
|
foo2.b = foo1.b;
|
|||
|
foo2.c = foo1.c;
|
|||
|
|
|||
|
Debido a que no hay envoltorios READ_ONCE() o WRITE_ONCE() y no
|
|||
|
hay markings volátiles, el compilador estaría en su derecho de
|
|||
|
implementar estas tres declaraciones de asignación como un par de
|
|||
|
cargas de 32 bits, seguido de un par de stores de 32 bits. Esto
|
|||
|
resultaría en una carga con desgarro en 'foo1.b' y store del desgarro
|
|||
|
en 'foo2.b'. READ_ONCE() y WRITE_ONCE() nuevamente evitan el desgarro
|
|||
|
en este ejemplo:
|
|||
|
|
|||
|
foo2.a = foo1.a;
|
|||
|
WRITE_ONCE(foo2.b, READ_ONCE(foo1.b));
|
|||
|
foo2.c = foo1.c;
|
|||
|
|
|||
|
Aparte de esto, nunca es necesario usar READ_ONCE() y WRITE_ONCE() en una
|
|||
|
variable que se ha marcado como volátil. Por ejemplo, dado que 'jiffies'
|
|||
|
está marcado como volátil, nunca es necesario usar READ_ONCE(jiffies). La
|
|||
|
razón de esto es que READ_ONCE() y WRITE_ONCE() se implementan como
|
|||
|
conversiones volátiles, lo que no tiene efecto cuando su argumento ya está
|
|||
|
marcado como volátil.
|
|||
|
|
|||
|
Tenga en cuenta que estas barreras del compilador no tienen un efecto
|
|||
|
directo en la CPU, que luego puede reordenar las cosas como quiera.
|
|||
|
|
|||
|
|
|||
|
BARRERAS DE MEMORIA DE LA CPU
|
|||
|
-----------------------------
|
|||
|
|
|||
|
El kernel de Linux tiene siete barreras básicas de memoria de CPU:
|
|||
|
|
|||
|
TIPO OBLIGATORIO SMP CONDICIONAL
|
|||
|
======================= =============== ===============
|
|||
|
GENERAL mb() smp_mb()
|
|||
|
WRITE wmb() smp_wmb()
|
|||
|
READ rmb() smp_rmb()
|
|||
|
DEPEDENCIA DE DIRECCIÓN READ_ONCE()
|
|||
|
|
|||
|
|
|||
|
Todas las barreras de memoria, excepto las barreras de dependencia de
|
|||
|
direcciones, implican una barrera del compilador. Las dependencias de
|
|||
|
direcciones no imponen ningún orden de compilación adicional.
|
|||
|
|
|||
|
Además: en el caso de las dependencias de direcciones, se esperaría que el
|
|||
|
compilador emita las cargas en el orden correcto (por ejemplo, `a[b]`
|
|||
|
tendría que cargar el valor de b antes de cargar a[b]), sin embargo, no hay
|
|||
|
garantía alguna en la especificación de C sobre que el compilador no puede
|
|||
|
especular el valor de b (por ejemplo, es igual a 1) y carga a[b] antes que
|
|||
|
b (ej. tmp = a[1]; if (b != 1) tmp = a[b]; ). También existe el problema de
|
|||
|
que un compilador vuelva a cargar b después de haber cargado a[b], teniendo
|
|||
|
así una copia más nueva de b que a[b]. Aún no se ha conseguido un consenso
|
|||
|
acerca de estos problemas, sin embargo, el macro READ_ONCE() es un buen
|
|||
|
lugar para empezar a buscar.
|
|||
|
|
|||
|
Las barreras de memoria SMP se reducen a barreras de compilador cuando se
|
|||
|
compila a monoprocesador, porque se supone que una CPU parecerá ser
|
|||
|
auto-consistente, y ordenará correctamente los accesos superpuestos
|
|||
|
respecto a sí misma. Sin embargo, consulte la subsección "Guests de
|
|||
|
máquinas virtuales" mas adelante.
|
|||
|
|
|||
|
[!] Tenga en cuenta que las barreras de memoria SMP _deben_ usarse para
|
|||
|
controlar el orden de referencias a memoria compartida en sistemas SMP,
|
|||
|
aunque el uso de bloqueo en su lugar sea suficiente.
|
|||
|
|
|||
|
Las barreras obligatorias no deben usarse para controlar los efectos de
|
|||
|
SMP, ya que dichas barreras imponen una sobrecarga innecesaria en los
|
|||
|
sistemas SMP y UP. Se pueden, sin embargo, usar para controlar los efectos
|
|||
|
MMIO en los accesos a través de ventanas E/S de memoria relajada. Estas
|
|||
|
barreras son necesarias incluso en sistemas que no son SMP, ya que afectan
|
|||
|
al orden en que las operaciones de memoria aparecen en un dispositivo, al
|
|||
|
prohibir tanto al compilador como a la CPU que sean reordenados.
|
|||
|
|
|||
|
|
|||
|
Hay algunas funciones de barrera más avanzadas:
|
|||
|
|
|||
|
(*) smp_store_mb(var, valor)
|
|||
|
|
|||
|
Asigna el valor a la variable y luego inserta una barrera de memoria
|
|||
|
completa después de ella. No se garantiza insertar nada más que una
|
|||
|
barrera del compilador en una compilación UP.
|
|||
|
|
|||
|
|
|||
|
(*) smp_mb__before_atomic();
|
|||
|
(*) smp_mb__after_atomic();
|
|||
|
|
|||
|
Estos se pueden usar con funciones RMW atómicas que no implican
|
|||
|
barreras de memoria, pero donde el código necesita una barrera de
|
|||
|
memoria. Ejemplos de funciones RMW atómicas que no implican una
|
|||
|
barrera de memoria son, por ejemplo, agregar, restar, operaciones
|
|||
|
condicionales (fallidas), funciones _relaxed, pero no atomic_read o
|
|||
|
atomic_set. Un ejemplo común donde se puede requerir una barrera es
|
|||
|
cuando se usan operaciones atómicas como referencia de contador.
|
|||
|
|
|||
|
Estos también se utilizan para funciones atómicas RMW bitop que no
|
|||
|
implican una barrera de memoria (como set_bit y clear_bit).
|
|||
|
|
|||
|
Como ejemplo, considere una pieza de código que marca un objeto como
|
|||
|
muerto y luego disminuye el contador de referencias del objeto:
|
|||
|
|
|||
|
obj->dead = 1;
|
|||
|
smp_mb__before_atomic();
|
|||
|
atomic_dec(&obj->ref_count);
|
|||
|
|
|||
|
Esto asegura que la marca de muerte en el objeto se perciba como
|
|||
|
fijada *antes* de que disminuya el contador de referencia.
|
|||
|
|
|||
|
Consulte Documentation/atomic_{t,bitops}.txt para obtener más
|
|||
|
información.
|
|||
|
|
|||
|
|
|||
|
(*) dma_wmb();
|
|||
|
(*) dma_rmb();
|
|||
|
(*) dma_mb();
|
|||
|
|
|||
|
Estos son usados con memoria consistente para garantizar el orden de
|
|||
|
escrituras o lecturas de memoria compartida accesible tanto para la
|
|||
|
CPU como para un dispositivo compatible con DMA.
|
|||
|
|
|||
|
Por ejemplo, considere un controlador de dispositivo que comparte
|
|||
|
memoria con otro dispositivo y usa un valor de estado del descriptor
|
|||
|
para indicar si el descriptor pertenece al dispositivo o a la CPU, y
|
|||
|
un "doorbell" (timbre, punto de acceso) para avisarle cuando haya
|
|||
|
nuevos descriptores disponibles:
|
|||
|
|
|||
|
if (desc->status != DEVICE_OWN) {
|
|||
|
/* no leer los datos hasta que tengamos el descriptor */
|
|||
|
dma_rmb();
|
|||
|
|
|||
|
/* leer/modificar datos */
|
|||
|
read_data = desc->data;
|
|||
|
desc->data = write_data;
|
|||
|
|
|||
|
/* flush de modificaciones antes de la actualización de estado */
|
|||
|
dma_wmb();
|
|||
|
|
|||
|
/* asignar propiedad */
|
|||
|
desc->status = DEVICE_OWN;
|
|||
|
|
|||
|
/* notificar al dispositivo de nuevos descriptores */
|
|||
|
writel(DESC_NOTIFY, doorbell);
|
|||
|
}
|
|||
|
|
|||
|
El dma_rmb() nos permite garantizar que el dispositivo ha liberado su
|
|||
|
propiedad antes de que leamos los datos del descriptor, y el dma_wmb()
|
|||
|
permite garantizar que los datos se escriben en el descriptor antes de
|
|||
|
que el dispositivo pueda ver que ahora tiene la propiedad. El dma_mb()
|
|||
|
implica tanto un dma_rmb() como un dma_wmb(). Tenga en cuenta que, al
|
|||
|
usar writel(), no se necesita un wmb() anterior para garantizar que
|
|||
|
las escrituras de la memoria caché coherente se hayan completado antes
|
|||
|
escribiendo a la región MMIO. El writel_relaxed() más barato no
|
|||
|
proporciona esta garantía y no debe utilizarse aquí.
|
|||
|
|
|||
|
Consulte la subsección "Efectos de barrera de E/S del kernel" para
|
|||
|
obtener más información sobre accesorios de E/S relajados y el archivo
|
|||
|
Documentation/core-api/dma-api.rst para más información sobre memoria
|
|||
|
consistente.
|
|||
|
|
|||
|
(*) pmem_wmb();
|
|||
|
|
|||
|
Es es para uso con memoria persistente para garantizar que los stores
|
|||
|
para los que las modificaciones se escriben en el almacenamiento
|
|||
|
persistente llegaron a dominio de durabilidad de la plataforma.
|
|||
|
|
|||
|
Por ejemplo, después de una escritura no temporal en la región pmem,
|
|||
|
usamos pmem_wmb() para garantizar que los stores hayan alcanzado el
|
|||
|
dominio de durabilidad de la plataforma. Esto garantiza que los stores
|
|||
|
han actualizado el almacenamiento persistente antes de cualquier
|
|||
|
acceso a datos o transferencia de datos causada por instrucciones
|
|||
|
posteriores. Esto es además del orden realizado por wmb().
|
|||
|
|
|||
|
Para la carga desde memoria persistente, las barreras de memoria de
|
|||
|
lectura existentes son suficientes para garantizar el orden de
|
|||
|
lectura.
|
|||
|
|
|||
|
(*) io_stop_wc();
|
|||
|
|
|||
|
Para accesos a memoria con atributos de combinación de escritura (por
|
|||
|
ejemplo, los devueltos por ioremap_wc(), la CPU puede esperar a que
|
|||
|
los accesos anteriores se junten con posteriores. io_stop_wc() se
|
|||
|
puede utilizar para evitar la combinación de accesos a memoria de
|
|||
|
de escritura antes de esta macro, con los posteriores, cuando dicha
|
|||
|
espera tenga implicaciones en el rendimiento.
|
|||
|
|
|||
|
=========================================
|
|||
|
BARRERAS DE MEMORIA IMPLÍCITAS DEL KERNEL
|
|||
|
=========================================
|
|||
|
|
|||
|
Algunas de las otras funciones en el kernel Linux implican barreras de
|
|||
|
memoria, entre estas encontramos funciones de bloqueo y planificación
|
|||
|
("scheduling").
|
|||
|
|
|||
|
Esta especificación es una garantía _mínima_; cualquier arquitectura
|
|||
|
particular puede proporcionar garantías más sustanciales, pero no se puede
|
|||
|
confiar en estas fuera de código específico de arquitectura.
|
|||
|
|
|||
|
|
|||
|
FUNCIONES DE ADQUISICIÓN DE CERROJO
|
|||
|
-----------------------------------
|
|||
|
|
|||
|
El kernel Linux tiene una serie de abstracciones de bloqueo:
|
|||
|
|
|||
|
(*) spin locks (cerrojos en loop)
|
|||
|
(*) R/W spin lock (cerrojos de escritura/lectura)
|
|||
|
(*) mutex
|
|||
|
(*) semáforos
|
|||
|
(*) R/W semáforos
|
|||
|
|
|||
|
En todos los casos existen variantes de las operaciones "ACQUIRE" y
|
|||
|
"RELEASE" para cada uno de ellos. Todas estas operaciones implican ciertas
|
|||
|
barreras:
|
|||
|
|
|||
|
(1) Implicaciones de la operación ACQUIRE:
|
|||
|
|
|||
|
Las operaciones de memoria emitidas después del ACQUIRE se completarán
|
|||
|
después de que la operación ACQUIRE haya finalizado.
|
|||
|
|
|||
|
Las operaciones de memoria emitidas antes de ACQUIRE pueden
|
|||
|
completarse después que la operación ACQUIRE se ha completado.
|
|||
|
|
|||
|
(2) Implicaciones de la operación RELEASE:
|
|||
|
|
|||
|
Las operaciones de memoria emitidas antes de la RELEASE se
|
|||
|
completarán antes de que la operación de RELEASE se haya completado.
|
|||
|
|
|||
|
Las operaciones de memoria emitidas después de la RELEASE pueden
|
|||
|
completarse antes de que la operación de RELEASE se haya completado.
|
|||
|
|
|||
|
(3) Implicación de ACQUIRE vs ACQUIRE:
|
|||
|
|
|||
|
Todas las operaciones ACQUIRE emitidas antes de otra operación
|
|||
|
ACQUIRE serán completadas antes de esa operación ACQUIRE.
|
|||
|
|
|||
|
(4) Implicación de ACQUIRE vs RELEASE:
|
|||
|
|
|||
|
Todas las operaciones ACQUIRE emitidas antes de una operación RELEASE
|
|||
|
serán completadas antes de la operación RELEASE.
|
|||
|
|
|||
|
(5) Implicación de ACQUIRE condicional fallido:
|
|||
|
|
|||
|
Ciertas variantes de bloqueo de la operación ACQUIRE pueden fallar, ya
|
|||
|
sea debido a no poder obtener el bloqueo de inmediato, o debido a que
|
|||
|
recibieron una señal de desbloqueo mientras dormían esperando que el
|
|||
|
cerrojo estuviera disponible. Los fallos en cerrojos no implican
|
|||
|
ningún tipo de barrera.
|
|||
|
|
|||
|
[!] Nota: una de las consecuencias de que los cerrojos en ACQUIRE y RELEASE
|
|||
|
sean barreras unidireccionales, es que los efectos de las instrucciones
|
|||
|
fuera de una sección crítica pueden filtrarse al interior de la sección
|
|||
|
crítica.
|
|||
|
|
|||
|
No se puede suponer que un ACQUIRE seguido de una RELEASE sea una barrera
|
|||
|
de memoria completa dado que es posible que un acceso anterior a ACQUIRE
|
|||
|
suceda después del ACQUIRE, y un acceso posterior a la RELEASE suceda antes
|
|||
|
del RELEASE, y los dos accesos puedan entonces cruzarse:
|
|||
|
|
|||
|
*A = a;
|
|||
|
ACQUIRE M
|
|||
|
RELEASE M
|
|||
|
*B = b;
|
|||
|
|
|||
|
puede ocurrir como:
|
|||
|
|
|||
|
ACQUIRE M, STORE *B, STORE *A, RELEASE M
|
|||
|
|
|||
|
Cuando ACQUIRE y RELEASE son bloqueo de adquisición y liberación,
|
|||
|
respectivamente, este mismo orden puede ocurrir si el cerrojo ACQUIRE y
|
|||
|
RELEASE son para la misma variable de bloqueo, pero solo desde la
|
|||
|
perspectiva de otra CPU que no tiene ese bloqueo. En resumen, un ACQUIRE
|
|||
|
seguido de un RELEASE NO puede entenderse como una barrera de memoria
|
|||
|
completa.
|
|||
|
|
|||
|
De manera similar, el caso inverso de un RELEASE seguido de un ACQUIRE no
|
|||
|
implica una barrera de memoria completa. Por lo tanto, la ejecución de la
|
|||
|
CPU de los tramos críticos correspondientes a la RELEASE y la ACQUIRE
|
|||
|
pueden cruzarse, de modo que:
|
|||
|
|
|||
|
*A = a;
|
|||
|
RELEASE M
|
|||
|
ACQUIRE N
|
|||
|
*B = b;
|
|||
|
|
|||
|
puede ocurrir como:
|
|||
|
|
|||
|
ACQUIRE N, STORE *B, STORE *A, RELEASE M
|
|||
|
|
|||
|
Podría parecer que este nuevo orden podría introducir un punto muerto.
|
|||
|
Sin embargo, esto no puede suceder porque si tal punto muerto amenazara
|
|||
|
con suceder, el RELEASE simplemente se completaría, evitando así el
|
|||
|
interbloqueo ("deadlock", punto muerto).
|
|||
|
|
|||
|
¿Por qué funciona esto?
|
|||
|
|
|||
|
Un punto clave es que solo estamos hablando de la CPU re-haciendo el
|
|||
|
orden, no el compilador. Si el compilador (o, ya puestos, el
|
|||
|
desarrollador) cambió las operaciones, un deadlock -podría- ocurrir.
|
|||
|
|
|||
|
Pero supongamos que la CPU reordenó las operaciones. En este caso, el
|
|||
|
desbloqueo precede al bloqueo en el código ensamblador. La CPU
|
|||
|
simplemente eligió intentar ejecutar primero la última operación de
|
|||
|
bloqueo. Si hay un interbloqueo, esta operación de bloqueo simplemente
|
|||
|
esperará (o tratará de dormir, pero hablaremos de eso más adelante). La
|
|||
|
CPU eventualmente ejecutará la operación de desbloqueo (que precedió a la
|
|||
|
operación de bloqueo en el código ensamblador), lo que desenmascará el
|
|||
|
potencial punto muerto, permitiendo que la operación de bloqueo tenga
|
|||
|
éxito.
|
|||
|
|
|||
|
Pero, ¿y si el cerrojo es un cerrojo que duerme ("sleeplock")? En tal
|
|||
|
caso, el código intentará entrar al scheduler, donde eventualmente
|
|||
|
encontrará una barrera de memoria, que forzará la operación de desbloqueo
|
|||
|
anterior para completar, nuevamente desentrañando el punto muerto. Podría
|
|||
|
haber una carrera de desbloqueo del sueño ("sleep-unlock race"), pero la
|
|||
|
primitiva de bloqueo necesita resolver tales carreras correctamente en
|
|||
|
cualquier caso.
|
|||
|
|
|||
|
Es posible que los cerrojos y los semáforos no proporcionen ninguna
|
|||
|
garantía de orden en sistemas compilados en UP, por lo que no se puede
|
|||
|
contar con tal situación para lograr realmente nada en absoluto,
|
|||
|
especialmente con respecto a los accesos de E/S, a menos que se combinen
|
|||
|
con operaciones de inhabilitación de interrupciones.
|
|||
|
|
|||
|
Consulte también la sección "Efectos de barrera adquiriendo intra-CPU".
|
|||
|
|
|||
|
|
|||
|
Como ejemplo, considere lo siguiente:
|
|||
|
|
|||
|
*A = a;
|
|||
|
*B = b;
|
|||
|
ACQUIRE
|
|||
|
*C = c;
|
|||
|
*D = d;
|
|||
|
RELEASE
|
|||
|
*E = e;
|
|||
|
*F = f;
|
|||
|
|
|||
|
La siguiente secuencia de eventos es aceptable:
|
|||
|
|
|||
|
ACQUIRE, {*F,*A}, *E, {*C,*D}, *B, RELEASE
|
|||
|
|
|||
|
[+] Tenga en cuenta que {*F,*A} indica un acceso combinado.
|
|||
|
|
|||
|
Pero ninguno de los siguientes lo son:
|
|||
|
|
|||
|
{*F,*A}, *B, ACQUIRE, *C, *D, RELEASE, *E
|
|||
|
*A, *B, *C, ACQUIRE, *D, RELEASE, *E, *F
|
|||
|
*A, *B, ACQUIRE, *C, RELEASE, *D, *E, *F
|
|||
|
*B, ACQUIRE, *C, *D, RELEASE, {*F,*A}, *E
|
|||
|
|
|||
|
|
|||
|
|
|||
|
FUNCIONES DE DESACTIVACIÓN DE INTERRUPCIONES
|
|||
|
--------------------------------------------
|
|||
|
|
|||
|
Las funciones que deshabilitan interrupciones (equivalentes a ACQUIRE) y
|
|||
|
habilitan interrupciones (equivalentes a RELEASE) actuarán únicamente como
|
|||
|
barrera del compilador. Por consiguiente, si la memoria o la E/S requieren
|
|||
|
barreras en tal situación, deben ser provistas por algún otro medio.
|
|||
|
|
|||
|
|
|||
|
FUNCIONES DE DORMIR Y DESPERTAR
|
|||
|
-------------------------------
|
|||
|
|
|||
|
Dormir y despertar son eventos marcados ("flagged") en los datos globales
|
|||
|
que se pueden ver como una interacción entre dos piezas de datos: el estado
|
|||
|
de la task (hilo, proceso, tarea) que espera el evento y los datos globales
|
|||
|
utilizados para indicar el evento. Para asegurarse de que estos parezcan
|
|||
|
suceder en el orden correcto, las primitivas para comenzar el proceso de ir
|
|||
|
a dormir, y las primitivas para iniciar un despertar implican ciertas
|
|||
|
barreras.
|
|||
|
|
|||
|
En primer lugar, el agente durmiente normalmente sigue algo similar a esta
|
|||
|
secuencia de eventos:
|
|||
|
|
|||
|
for (;;) {
|
|||
|
set_current_state(TASK_UNINTERRUPTIBLE);
|
|||
|
if (evento_indicado)
|
|||
|
break;
|
|||
|
schedule(); // planificar
|
|||
|
}
|
|||
|
|
|||
|
Una barrera de memoria general se obtiene automáticamente mediante
|
|||
|
set_current_state() después de haber alterado el estado de la tarea:
|
|||
|
|
|||
|
CPU 1
|
|||
|
===============================
|
|||
|
set_current_state(); // hacer_estado_actual()
|
|||
|
smp_store_mb();
|
|||
|
STORE current->state
|
|||
|
<barrera general>
|
|||
|
LOAD evento_indicado
|
|||
|
|
|||
|
set_current_state() puede estar envuelto por:
|
|||
|
|
|||
|
prepare_to_wait(); // preparese_para_esperar();
|
|||
|
prepare_to_wait_exclusive(); // prepararse_para_solo_esperar();
|
|||
|
|
|||
|
que por lo tanto también implican una barrera de memoria general después de
|
|||
|
establecer el estado. Toda la secuencia anterior está disponible en varias
|
|||
|
formas, todas las cuales obtienen la barrera de memoria en el lugar
|
|||
|
correcto:
|
|||
|
|
|||
|
wait_event();
|
|||
|
wait_event_interruptible();
|
|||
|
wait_event_interruptible_exclusive();
|
|||
|
wait_event_interruptible_timeout();
|
|||
|
wait_event_killable();
|
|||
|
wait_event_timeout();
|
|||
|
wait_on_bit();
|
|||
|
wait_on_bit_lock();
|
|||
|
|
|||
|
|
|||
|
En segundo lugar, el código que realiza una activación normalmente se
|
|||
|
asemeja a algo como esto:
|
|||
|
|
|||
|
evento_indicado = 1;
|
|||
|
wake_up(&event_wait_queue); // despertar
|
|||
|
|
|||
|
o:
|
|||
|
|
|||
|
evento_indicado = 1;
|
|||
|
wake_up_process(event_daemon); // despertar proceso
|
|||
|
|
|||
|
wake_up() ejecuta una barrera de memoria general si despierta algo. Si no
|
|||
|
despierta nada, entonces una barrera de memoria puede o no ser ejecutada;
|
|||
|
no debe confiar en ello. La barrera se produce antes del acceso al estado
|
|||
|
de la tarea. En particular, se encuentra entre el STORE para indicar el
|
|||
|
evento y el STORE para configurar TASK_RUNNING (hilo ejecutando):
|
|||
|
|
|||
|
CPU 1 (Durmiente) CPU 2 (Despertadora)
|
|||
|
=============================== ===============================
|
|||
|
set_current_state(); STORE evento_indicado
|
|||
|
smp_store_mb(); wake_up();
|
|||
|
STORE current->state ...
|
|||
|
<barrera general> <barrera general>
|
|||
|
LOAD evento_indicado if ((LOAD task->state) & TASK_NORMAL)
|
|||
|
STORE task->state
|
|||
|
|
|||
|
donde "task" es el subproceso que se está despertando y es igual al
|
|||
|
"current" (hilo actual) de la CPU 1.
|
|||
|
|
|||
|
Para reiterar, se garantiza la ejecución de una barrera de memoria general
|
|||
|
mediante wake_up() si algo está realmente despierto, pero de lo contrario
|
|||
|
no existe tal garantía. Para entender esto, considere la siguiente
|
|||
|
secuencia de eventos, donde X e Y son ambos cero inicialmente:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
=============================== ===============================
|
|||
|
X = 1; Y = 1;
|
|||
|
smp_mb(); wake_up();
|
|||
|
LOAD Y LOAD X
|
|||
|
|
|||
|
Si ocurre una reactivación ("wakeup"), una (al menos) de las dos cargas
|
|||
|
debe ver 1. Si, por otro lado, no ocurre una reactivación, ambas cargas
|
|||
|
pueden ver 0.
|
|||
|
|
|||
|
wake_up_process() siempre ejecuta una barrera de memoria general. La
|
|||
|
barrera, de nuevo, ocurre antes de que se acceda al estado del hilo. En
|
|||
|
particular, si wake_up(), en el fragmento anterior, fuera reemplazado por
|
|||
|
una llamada a wake_up_process(), las dos cargas verían 1, garantizado.
|
|||
|
|
|||
|
Las funciones de activación disponibles incluyen:
|
|||
|
|
|||
|
complete();
|
|||
|
wake_up();
|
|||
|
wake_up_all();
|
|||
|
wake_up_bit();
|
|||
|
wake_up_interruptible();
|
|||
|
wake_up_interruptible_all();
|
|||
|
wake_up_interruptible_nr();
|
|||
|
wake_up_interruptible_poll();
|
|||
|
wake_up_interruptible_sync();
|
|||
|
wake_up_interruptible_sync_poll();
|
|||
|
wake_up_locked();
|
|||
|
wake_up_locked_poll();
|
|||
|
wake_up_nr();
|
|||
|
wake_up_poll();
|
|||
|
wake_up_process();
|
|||
|
|
|||
|
En términos de orden de la memoria, todas estas funciones proporcionan las
|
|||
|
mismas garantías que un wake_up() (o más fuertes).
|
|||
|
|
|||
|
[!] Tenga en cuenta que las barreras de la memoria implicadas por el
|
|||
|
durmiente y el despierto _no_ ordenan varios stores antes del despertar con
|
|||
|
respecto a cargas de los valores guardados después de que el durmiente haya
|
|||
|
llamado a set_current_state(). Por ejemplo, si el durmiente hace:
|
|||
|
|
|||
|
set_current_state(TASK_INTERRUPTIBLE);
|
|||
|
if (evento_indicado)
|
|||
|
break;
|
|||
|
__set_current_state(TASK_RUNNING);
|
|||
|
hacer_algo(my_data);
|
|||
|
|
|||
|
y el que despierta hace:
|
|||
|
|
|||
|
my_data = valor;
|
|||
|
evento_indicado = 1;
|
|||
|
wake_up(&event_wait_queue);
|
|||
|
|
|||
|
no existe garantía de que el cambio a event_indicated sea percibido por
|
|||
|
el durmiente de manera que venga después del cambio a my_data. En tal
|
|||
|
circunstancia, el código en ambos lados debe sacar sus propias barreras de
|
|||
|
memoria entre los separados accesos a datos. Por lo tanto, el durmiente
|
|||
|
anterior debería hacer:
|
|||
|
|
|||
|
set_current_state(TASK_INTERRUPTIBLE);
|
|||
|
if (evento_indicado) {
|
|||
|
smp_rmb();
|
|||
|
hacer_algo(my_data);
|
|||
|
}
|
|||
|
|
|||
|
y el que despierta debería hacer:
|
|||
|
|
|||
|
my_data = value;
|
|||
|
smp_wmb();
|
|||
|
evento_indicado = 1;
|
|||
|
wake_up(&event_wait_queue);
|
|||
|
|
|||
|
FUNCIONES VARIAS
|
|||
|
----------------
|
|||
|
|
|||
|
Otras funciones que implican barreras:
|
|||
|
|
|||
|
(*) schedule() y similares implican barreras completas de memoria.
|
|||
|
|
|||
|
|
|||
|
========================================
|
|||
|
EFECTOS DE BARRERA ADQUIRIENDO INTRA-CPU
|
|||
|
========================================
|
|||
|
|
|||
|
En los sistemas SMP, las primitivas de bloqueo proveen una forma más
|
|||
|
sustancial de barrera: una que afecta el orden de acceso a la memoria en
|
|||
|
otras CPU, dentro del contexto de conflicto en cualquier bloqueo en
|
|||
|
particular.
|
|||
|
|
|||
|
|
|||
|
ADQUISICIÓN VS ACCESOS A MEMORIA
|
|||
|
--------------------------------
|
|||
|
|
|||
|
Considere lo siguiente: el sistema tiene un par de spinlocks (M) y (Q), y
|
|||
|
tres CPU; entonces la siguiente secuencia de eventos debería ocurrir:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
=============================== ===============================
|
|||
|
WRITE_ONCE(*A, a); WRITE_ONCE(*E, e);
|
|||
|
ACQUIRE M ACQUIRE Q
|
|||
|
WRITE_ONCE(*B, b); WRITE_ONCE(*F, f);
|
|||
|
WRITE_ONCE(*C, c); WRITE_ONCE(*G, g);
|
|||
|
RELEASE M RELEASE Q
|
|||
|
WRITE_ONCE(*D, d); WRITE_ONCE(*H, h);
|
|||
|
|
|||
|
Entonces no hay garantía sobre en qué orden verá la CPU 3 los accesos a *A
|
|||
|
hasta que *H ocurra, además de las restricciones impuestas por los bloqueos
|
|||
|
separados en las distintas CPUs. Podría, por ejemplo, ver:
|
|||
|
|
|||
|
*E, ACQUIRE M, ACQUIRE Q, *G, *C, *F, *A, *B, RELEASE Q, *D, *H, RELEASE M
|
|||
|
|
|||
|
Pero no verá ninguno de:
|
|||
|
|
|||
|
*B, *C or *D preceding ACQUIRE M
|
|||
|
*A, *B or *C following RELEASE M
|
|||
|
*F, *G or *H preceding ACQUIRE Q
|
|||
|
*E, *F or *G following RELEASE Q
|
|||
|
|
|||
|
========================================
|
|||
|
¿DÓNDE SE NECESITAN BARRERAS DE MEMORIA?
|
|||
|
========================================
|
|||
|
|
|||
|
Bajo operación normal, el re-ordenamiento de una operación de memoria
|
|||
|
generalmente no va a suponer un problema, ya que para una pieza de código
|
|||
|
lineal de un solo subproceso seguirá pareciendo que funciona correctamente,
|
|||
|
incluso si está en un kernel SMP. Existen, sin embargo, cuatro
|
|||
|
circunstancias en las que reordenar definitivamente _podría_ ser un
|
|||
|
problema:
|
|||
|
|
|||
|
(*) Interacción entre procesadores.
|
|||
|
|
|||
|
(*) Operaciones atómicas.
|
|||
|
|
|||
|
(*) Acceso a dispositivos.
|
|||
|
|
|||
|
(*) Interrupciones.
|
|||
|
|
|||
|
|
|||
|
INTERACCIÓN ENTRE PROCESADORES
|
|||
|
------------------------------
|
|||
|
|
|||
|
Cuando se da un sistema con más de un procesador, más de una CPU en el
|
|||
|
sistema puede estar trabajando en el mismo conjunto de datos al mismo
|
|||
|
tiempo. Esto puede causar problemas de sincronización, y la forma habitual
|
|||
|
de tratar con estos es utilizar cerrojos. Sin embargo, los cerrojos son
|
|||
|
bastante caros, por lo que puede ser preferible operar sin el uso de un
|
|||
|
cerrojo a ser posible. En cuyo caso, es posible que las operaciones que
|
|||
|
afectan a ambas CPU deban ordenarse cuidadosamente para evitar un
|
|||
|
funcionamiento incorrecto.
|
|||
|
|
|||
|
Considere, por ejemplo, la ruta lenta del semáforo R/W. Aquí hay un proceso
|
|||
|
de espera en cola del semáforo, en virtud de que tiene una parte de su pila
|
|||
|
vinculada a la lista de procesos en espera del semáforo:
|
|||
|
|
|||
|
struct rw_semaphore {
|
|||
|
...
|
|||
|
spinlock_t lock;
|
|||
|
struct list_head waiters;
|
|||
|
};
|
|||
|
|
|||
|
struct rwsem_waiter {
|
|||
|
struct list_head list;
|
|||
|
struct task_struct *task;
|
|||
|
};
|
|||
|
|
|||
|
Para despertar a un proceso que espera ("waiter") en particular, las
|
|||
|
funciones up_read() o up_write() tienen que:
|
|||
|
|
|||
|
(1) leer el siguiente puntero del registro de este proceso que espera,
|
|||
|
para saber dónde está el registro del siguiente waiter;
|
|||
|
|
|||
|
(2) leer el puntero a la estructura de tareas del waiter;
|
|||
|
|
|||
|
(3) borrar el puntero de la tarea para decirle al waiter que se le ha dado
|
|||
|
el semáforo;
|
|||
|
|
|||
|
(4) llamar a wake_up_process() en la tarea; y
|
|||
|
|
|||
|
(5) liberar la referencia retenida en la estructura de tareas del waiter.
|
|||
|
|
|||
|
En otras palabras, tiene que realizar esta secuencia de eventos:
|
|||
|
|
|||
|
LOAD waiter->list.next;
|
|||
|
LOAD waiter->task;
|
|||
|
STORE waiter->task;
|
|||
|
CALL wakeup
|
|||
|
RELEASE task
|
|||
|
|
|||
|
y si alguno de estos pasos ocurre fuera de orden, entonces todo puede que
|
|||
|
funcione defectuosamente.
|
|||
|
|
|||
|
Una vez que se ha puesto en cola y soltado el bloqueo de semáforo, el
|
|||
|
proceso que espera no consigue el candado de nuevo; en cambio, solo espera
|
|||
|
a que se borre su puntero de tarea antes de continuar. Dado que el registro
|
|||
|
está en la pila del proceso que espera, esto significa que si el puntero de
|
|||
|
la tarea se borra _antes_ de que se lea el siguiente puntero de la lista,
|
|||
|
otra CPU podría comenzar a procesar el proceso que espera y podría romper
|
|||
|
el stack del proceso que espera antes de que la función up*() tenga la
|
|||
|
oportunidad de leer el puntero que sigue.
|
|||
|
|
|||
|
Considere entonces lo que podría suceder con la secuencia de eventos
|
|||
|
anterior:
|
|||
|
|
|||
|
CPU 1 CPU 2
|
|||
|
=============================== ===============================
|
|||
|
down_xxx()
|
|||
|
Poner waiter en la "queue" (cola)
|
|||
|
Dormir
|
|||
|
up_yyy()
|
|||
|
LOAD waiter->task;
|
|||
|
STORE waiter->task;
|
|||
|
Despertado por otro evento
|
|||
|
<preempt>
|
|||
|
Reanudar el procesamiento
|
|||
|
down_xxx() regresa
|
|||
|
llamada a foo()
|
|||
|
foo() estropea *waiter
|
|||
|
</preempt>
|
|||
|
LOAD waiter->list.next;
|
|||
|
--- OOPS ---
|
|||
|
|
|||
|
Esto podría solucionarse usando el bloqueo de semáforo, pero luego la
|
|||
|
función down_xxx() tiene que obtener innecesariamente el spinlock
|
|||
|
nuevamente, después de ser despertado el hilo.
|
|||
|
|
|||
|
La forma de lidiar con esto es insertar una barrera de memoria SMP general:
|
|||
|
|
|||
|
LOAD waiter->list.next;
|
|||
|
LOAD waiter->task;
|
|||
|
smp_mb();
|
|||
|
STORE waiter->task;
|
|||
|
CALL wakeup
|
|||
|
RELEASE task
|
|||
|
|
|||
|
En este caso, la barrera garantiza que todos los accesos a memoria antes de
|
|||
|
la barrera parecerán suceder antes de todos los accesos a memoria después
|
|||
|
de dicha barrera con respecto a las demás CPU del sistema. _No_ garantiza
|
|||
|
que todos los accesos a memoria antes de la barrera se completarán en el
|
|||
|
momento en que la instrucción de la barrera en sí se complete.
|
|||
|
|
|||
|
En un sistema UP, donde esto no sería un problema, la función smp_mb() es
|
|||
|
solo una barrera del compilador, asegurándose así de que el compilador
|
|||
|
emita las instrucciones en el orden correcto sin realmente intervenir en la
|
|||
|
CPU. Como solo hay un CPU, la lógica de orden de dependencias de esa CPU se
|
|||
|
encargará de todo lo demás.
|
|||
|
|
|||
|
|
|||
|
OPERACIONES ATÓMICAS
|
|||
|
--------------------
|
|||
|
|
|||
|
Si bien son, técnicamente, consideraciones de interacción entre
|
|||
|
procesadores, las operaciones atómicas se destacan especialmente porque
|
|||
|
algunas de ellas implican barreras de memoria completa y algunas otras no,
|
|||
|
pero se confía mucho en ellos en su conjunto a lo largo del kernel.
|
|||
|
|
|||
|
Consulte Documentation/atomic_t.txt para obtener más información.
|
|||
|
|
|||
|
|
|||
|
ACCESO A DISPOSITIVOS
|
|||
|
---------------------
|
|||
|
|
|||
|
Un driver puede ser interrumpido por su propia rutina de servicio de
|
|||
|
interrupción y, por lo tanto, las dos partes del driver pueden interferir
|
|||
|
con los intentos de controlar o acceder al dispositivo.
|
|||
|
|
|||
|
Esto puede aliviarse, al menos en parte, desactivando las interrupciones
|
|||
|
locales (una forma de bloqueo), de modo que las operaciones críticas sean
|
|||
|
todas contenidas dentro la sección de interrupción desactivada en el
|
|||
|
controlador. Mientras la interrupción del driver está ejecutando la rutina,
|
|||
|
es posible que el "core" del controlador no se ejecute en la misma CPU y no
|
|||
|
se permita que su interrupción vuelva a ocurrir hasta que la interrupción
|
|||
|
actual haya sido resuelta, por lo tanto, el controlador de interrupción no
|
|||
|
necesita bloquearse contra esto.
|
|||
|
|
|||
|
Sin embargo, considere un driver que estaba hablando con una tarjeta
|
|||
|
ethernet que tiene un registro de direcciones y un registro de datos. Si
|
|||
|
el core de ese controlador habla con la tarjeta estando en desactivación de
|
|||
|
interrupción y luego se invoca el controlador de interrupción del
|
|||
|
controlador:
|
|||
|
|
|||
|
IRQ LOCALES DESACTIVADAS
|
|||
|
writew(ADDR, 3);
|
|||
|
writew(DATA, y);
|
|||
|
IRQ LOCALES ACTIVADAS
|
|||
|
<interrupción>
|
|||
|
writew(ADDR, 4);
|
|||
|
q = readw(DATA);
|
|||
|
</interrupción>
|
|||
|
|
|||
|
El almacenamiento en el registro de datos puede ocurrir después del segundo
|
|||
|
almacenamiento en el registro de direcciones si las reglas de orden son lo
|
|||
|
suficientemente relajadas:
|
|||
|
|
|||
|
STORE *ADDR = 3, STORE *ADDR = 4, STORE *DATA = y, q = LOAD *DATA
|
|||
|
|
|||
|
Si se relajan las reglas de orden, se debe asumir que los accesos
|
|||
|
realizados dentro de la sección con interrupción deshabilitada pueden
|
|||
|
filtrarse fuera de esta y pueden intercalarse con accesos realizados en una
|
|||
|
interrupción - y viceversa - a menos que se utilicenn barreras implícita o
|
|||
|
explícitas.
|
|||
|
|
|||
|
Normalmente, esto no será un problema porque los accesos de E/S realizados
|
|||
|
dentro de tales secciones incluirán operaciones de carga síncronas en
|
|||
|
registros E/S estrictamente ordenados, que forman barreras de E/S
|
|||
|
implícitas.
|
|||
|
|
|||
|
|
|||
|
Una situación similar puede ocurrir entre una rutina de interrupción y dos
|
|||
|
rutinas ejecutándose en separadas CPU que se comunican entre sí. Si tal
|
|||
|
caso es probable, entonces se deben usar bloqueos de desactivación de
|
|||
|
interrupciones para garantizar el orden.
|
|||
|
|
|||
|
|
|||
|
=====================================
|
|||
|
Efectos de barrera de E/S del kernel
|
|||
|
=====================================
|
|||
|
|
|||
|
La interfaz con periféricos a través de accesos de E/S es profundamente
|
|||
|
específica para cada arquitectura y dispositivo. Por lo tanto, los drivers
|
|||
|
que son inherentemente no portátiles pueden depender de comportamientos
|
|||
|
específicos de sus sistemas de destino, con el fin de lograr la
|
|||
|
sincronización de la manera más ligera posible. Para drivers que deseen ser
|
|||
|
portátiles entre múltiples arquitecturas e implementaciones de bus, el
|
|||
|
kernel ofrece una serie de funciones de acceso que proporcionan varios
|
|||
|
grados de garantías de orden:
|
|||
|
|
|||
|
(*) readX(), writeX():
|
|||
|
|
|||
|
Las funciones de acceso MMIO readX() y writeX() usan un puntero al
|
|||
|
periférico al que se accede como un parámetro __iomem *. para punteros
|
|||
|
asignados los atributos de E/S predeterminados (por ejemplo, los
|
|||
|
devueltos por ioremap()), las garantías de orden son las siguientes:
|
|||
|
|
|||
|
1. Se ordenan todos los accesos readX() y writeX() a un mismo periférico
|
|||
|
entre estos. Esto asegura que los registros de acceso MMIO por el
|
|||
|
mismo subproceso de la CPU a un dispositivo en particular llegarán en
|
|||
|
el orden del programa.
|
|||
|
|
|||
|
2. Se ordena un writeX() emitido por un subproceso de CPU que contiene un
|
|||
|
spinlock antes de un writeX() al mismo periférico desde otro
|
|||
|
subproceso de CPU, si emitido después de una adquisición posterior del
|
|||
|
mismo spinlock. Esto garantiza que ese registro MMIO escribe en un
|
|||
|
dispositivo en particular, mientras que se obtiene un spinlock en un
|
|||
|
orden consistente con las adquisiciones del cerrojo.
|
|||
|
|
|||
|
3. Un writeX() por un subproceso de la CPU al periférico primero esperará
|
|||
|
a la finalización de todas las escrituras anteriores en la memoria
|
|||
|
emitidas por, o bien propagadas por, el mismo subproceso. Esto asegura
|
|||
|
que las escrituras de la CPU a un búfer DMA de salida asignadas por
|
|||
|
dma_alloc_coherent() serán visibles para un motor ("engine") DMA
|
|||
|
cuando la CPU escriba en sus registros de control MMIO, para activar
|
|||
|
la transferencia.
|
|||
|
|
|||
|
4. Un readX() de un subproceso del CPU, desde el periférico, se
|
|||
|
completará antes de que cualquier lectura subsiguiente de memoria por
|
|||
|
el mismo subproceso pueda comenzar. Esto asegura que las lecturas de
|
|||
|
la CPU desde un búfer DMA entrantes asignadas por
|
|||
|
dma_alloc_coherent(), no verán datos obsoletos después de leer el
|
|||
|
registro de estado MMIO del motor DMA, para establecer que la
|
|||
|
transferencia DMA se haya completado.
|
|||
|
|
|||
|
5. Un readX() por un subproceso del CPU, desde el periférico, se
|
|||
|
completará antes de que cualquier bucle delay() subsiguiente pueda
|
|||
|
comenzar a ejecutarse en el mismo subproceso. Esto asegura que dos
|
|||
|
escrituras del CPU a registros MMIO en un periférico llegarán al menos
|
|||
|
con 1us de diferencia, si la primera escritura se lee inmediatamente
|
|||
|
de vuelta con readX() y se llama a udelay(1) antes del segundo
|
|||
|
writeX():
|
|||
|
|
|||
|
writel(42, DEVICE_REGISTER_0); // Llega al dispositivo ...
|
|||
|
readl(DEVICE_REGISTER_0);
|
|||
|
udelay(1);
|
|||
|
writel(42, DEVICE_REGISTER_1); // al menos 1us antes de esto....
|
|||
|
|
|||
|
Las propiedades de orden de los punteros __iomem obtenidos con valores de
|
|||
|
atributos que no sean los valores por defecto (por ejemplo, los devueltos
|
|||
|
por ioremap_wc()) son específicos de la arquitectura subyacente y, por lo
|
|||
|
tanto, las garantías enumeradas anteriormente no pueden por lo general ser
|
|||
|
aseguradas para accesos a este tipo de "mappings" (asignaciones).
|
|||
|
|
|||
|
(*) readX_relaxed(), writeX_relaxed():
|
|||
|
|
|||
|
Son similares a readX() y writeX(), pero proporcionan una garantía de
|
|||
|
orden de memoria más débil. Específicamente, no garantizan orden con
|
|||
|
respecto al bloqueo, los accesos normales a la memoria o los bucles
|
|||
|
delay() (es decir, los puntos 2-5 arriba) pero todavía se garantiza que
|
|||
|
se ordenarán con respecto a otros accesos desde el mismo hilo de la CPU,
|
|||
|
al mismo periférico, cuando se opera en punteros __iomem asignados con el
|
|||
|
valor predeterminado para los atributos de E/S.
|
|||
|
|
|||
|
(*) readsX(), writesX():
|
|||
|
|
|||
|
Los puntos de entrada readsX() y writesX() MMIO están diseñados para
|
|||
|
acceder FIFOs mapeados en memoria y basados en registros que residen en
|
|||
|
periféricos, que no son capaces de realizar DMA. Por tanto, sólo
|
|||
|
proporcionan garantías de orden readX_relaxed() y writeX_relaxed(), como
|
|||
|
se documentó anteriormente.
|
|||
|
|
|||
|
(*) inX(), outX():
|
|||
|
|
|||
|
Los puntos de entrada inX() y outX() están destinados a acceder a mapas
|
|||
|
de puertos "legacy" (antiguos) de periféricos de E/S, que pueden requerir
|
|||
|
instrucciones especiales en algunas arquitecturas (especialmente, en
|
|||
|
x86). El número de puerto del periférico que se está accedido se pasa
|
|||
|
como un argumento.
|
|||
|
|
|||
|
Dado que muchas arquitecturas de CPU acceden finalmente a estos
|
|||
|
periféricos a través de un mapeo interno de memoria virtual, las
|
|||
|
garantías de orden portátiles proporcionadas por inX() y outX() son las
|
|||
|
mismas que las proporcionadas por readX() y writeX(), respectivamente, al
|
|||
|
acceder a una asignación con los valores de atributos de E/S
|
|||
|
predeterminados (los que haya por defecto).
|
|||
|
|
|||
|
Los drivers de dispositivos pueden esperar que outX() emita una
|
|||
|
transacción de escritura no publicada, que espera una respuesta de
|
|||
|
finalización del periférico de E/S antes de regresar. Esto no está
|
|||
|
garantizado por todas las arquitecturas y por lo tanto no forma parte de
|
|||
|
la semántica de orden portátil.
|
|||
|
|
|||
|
(*) insX(), outsX():
|
|||
|
|
|||
|
Como arriba, los puntos de entrada insX() y outsX() proporcionan el mismo
|
|||
|
orden garantizado por readsX() y writesX() respectivamente, al acceder a
|
|||
|
un mapping con los atributos de E/S predeterminados.
|
|||
|
|
|||
|
(*) ioreadX(), iowriteX():
|
|||
|
|
|||
|
Estos funcionarán adecuadamente para el tipo de acceso que realmente están
|
|||
|
haciendo, ya sea inX()/outX() o readX()/writeX().
|
|||
|
|
|||
|
Con la excepción de los puntos de entrada (insX(), outsX(), readsX() y
|
|||
|
writesX()), todo lo anterior supone que el periférico subyacente es
|
|||
|
little-endian y, por lo tanto, realizará operaciones de intercambio de
|
|||
|
bytes en arquitecturas big-endian.
|
|||
|
|
|||
|
|
|||
|
===========================================
|
|||
|
MODELO DE ORDEN MÍNIMO DE EJECUCIÓN ASUMIDO
|
|||
|
===========================================
|
|||
|
|
|||
|
Debe suponerse que la CPU conceptual está débilmente ordenada, pero que
|
|||
|
mantiene la apariencia de causalidad del programa con respecto a sí misma.
|
|||
|
Algunas CPU (como i386 o x86_64) están más limitadas que otras (como
|
|||
|
powerpc o frv), por lo que el caso más relajado (es decir, DEC Alpha) se
|
|||
|
debe asumir fuera de código específico de arquitectura.
|
|||
|
|
|||
|
Esto significa que se debe considerar que la CPU ejecutará su flujo de
|
|||
|
instrucciones en el orden que se quiera - o incluso en paralelo - siempre
|
|||
|
que si una instrucción en el flujo depende de una instrucción anterior,
|
|||
|
entonces dicha instrucción anterior debe ser lo suficientemente completa[*]
|
|||
|
antes de que la posterior instrucción puede proceder; en otras palabras:
|
|||
|
siempre que la apariencia de causalidad se mantenga.
|
|||
|
|
|||
|
[*] Algunas instrucciones tienen más de un efecto, como cambiar el
|
|||
|
código de condición, cambio de registros o cambio de memoria - y
|
|||
|
distintas instrucciones pueden depender de diferentes efectos.
|
|||
|
|
|||
|
Una CPU puede también descartar cualquier secuencia de instrucciones que
|
|||
|
termine sin tener efecto final. Por ejemplo, si dos instrucciones
|
|||
|
adyacentes cargan un valor inmediato en el mismo registro, la primera puede
|
|||
|
descartarse.
|
|||
|
|
|||
|
|
|||
|
De manera similar, se debe suponer que el compilador podría reordenar la
|
|||
|
corriente de instrucciones de la manera que crea conveniente, nuevamente
|
|||
|
siempre que la apariencia de causalidad se mantenga.
|
|||
|
|
|||
|
|
|||
|
=====================================
|
|||
|
EFECTOS DE LA MEMORIA CACHÉ DE LA CPU
|
|||
|
=====================================
|
|||
|
|
|||
|
La forma en que se perciben las operaciones de memoria caché en todo el
|
|||
|
sistema se ve afectada, hasta cierto punto, por los cachés que se
|
|||
|
encuentran entre las CPU y la memoria, y por el sistema de coherencia en
|
|||
|
memoria que mantiene la consistencia de estado en el sistema.
|
|||
|
|
|||
|
En cuanto a la forma en que una CPU interactúa con otra parte del sistema a
|
|||
|
través del caché, el sistema de memoria tiene que incluir los cachés de la
|
|||
|
CPU y barreras de memoria, que en su mayor parte actúan en la interfaz
|
|||
|
entre la CPU y su caché (las barreras de memoria lógicamente actúan sobre
|
|||
|
la línea de puntos en el siguiente diagrama):
|
|||
|
|
|||
|
<--- CPU ---> : <----------- Memoria ----------->
|
|||
|
:
|
|||
|
+--------+ +--------+ : +--------+ +-----------+
|
|||
|
| Core | | Cola | : | Cache | | | +---------+
|
|||
|
| CPU | | de | : | CPU | | | | |
|
|||
|
| |--->| acceso |----->| |<-->| | | |
|
|||
|
| | | a | : | | | |--->| Memoria |
|
|||
|
| | | memoria| : | | | | | |
|
|||
|
+--------+ +--------+ : +--------+ | Mecanismo | | |
|
|||
|
: | de | +---------+
|
|||
|
: | Coherencia|
|
|||
|
: | de la | +--------+
|
|||
|
+--------+ +--------+ : +--------+ | cache | | |
|
|||
|
| Core | | Cola | : | Cache | | | | |
|
|||
|
| CPU | | de | : | CPU | | |--->| Dispos |
|
|||
|
| |--->| acceso |----->| |<-->| | | itivo |
|
|||
|
| | | a | : | | | | | |
|
|||
|
| | | memoria| : | | | | +--------+
|
|||
|
+--------+ +--------+ : +--------+ +-----------+
|
|||
|
:
|
|||
|
:
|
|||
|
|
|||
|
Aunque es posible que una carga o store en particular no aparezca fuera de
|
|||
|
la CPU que lo emitió, ya que puede haber sido satisfecha dentro del propio
|
|||
|
caché de la CPU, seguirá pareciendo como si el acceso total a la memoria
|
|||
|
hubiera tenido lugar para las otras CPUs, ya que los mecanismos de
|
|||
|
coherencia de caché migrarán la cacheline sobre la CPU que accede y se
|
|||
|
propagarán los efectos en caso de conflicto.
|
|||
|
|
|||
|
El núcleo de la CPU puede ejecutar instrucciones en el orden que considere
|
|||
|
adecuado, siempre que parezca mantenerse la causalidad esperada del
|
|||
|
programa. Algunas de las instrucciones generan operaciones de carga y
|
|||
|
almacenamiento que luego van a la cola de accesos a memoria a realizar. El
|
|||
|
núcleo puede colocarlos en la cola en cualquier orden que desee, y
|
|||
|
continuar su ejecución hasta que se vea obligado a esperar que una
|
|||
|
instrucción sea completada.
|
|||
|
|
|||
|
De lo que se ocupan las barreras de la memoria es de controlar el orden en
|
|||
|
que los accesos cruzan, desde el lado de la CPU, hasta el lado de memoria,
|
|||
|
y el orden en que los otros observadores perciben los efectos en el sistema
|
|||
|
que sucedan por esto.
|
|||
|
|
|||
|
[!] Las barreras de memoria _no_ son necesarias dentro de una CPU
|
|||
|
determinada, ya que las CPU siempre ven sus propias cargas y stores como si
|
|||
|
hubieran sucedido en el orden del programa.
|
|||
|
|
|||
|
[!] Los accesos a MMIO u otros dispositivos pueden pasar por alto el
|
|||
|
sistema de caché. Esto depende de las propiedades de la ventana de memoria
|
|||
|
a través de la cual se accede a los dispositivos y/o el uso de
|
|||
|
instrucciones especiales de comunicación con dispositivo que pueda tener la
|
|||
|
CPU.
|
|||
|
|
|||
|
|
|||
|
COHERENCIA DE CACHÉ FRENTE A DMA
|
|||
|
---------------------------------
|
|||
|
|
|||
|
No todos los sistemas mantienen coherencia de caché con respecto a los
|
|||
|
dispositivos que realizan DMA. En tales casos, un dispositivo que intente
|
|||
|
DMA puede obtener datos obsoletos de la RAM, porque las líneas de caché
|
|||
|
"sucias" pueden residir en los cachés de varias CPU, y es posible que no
|
|||
|
se hayan vuelto a escribir en la RAM todavía. Para hacer frente a esto, la
|
|||
|
parte apropiada del kernel debe vaciar los bits superpuestos de caché en
|
|||
|
cada CPU (y tal vez también invalidarlos).
|
|||
|
|
|||
|
Además, los datos enviados por DMA a RAM, por un dispositivo, pueden ser
|
|||
|
sobrescritos por líneas de caché sucias que se escriben de nuevo en la RAM
|
|||
|
desde el caché de una CPU, después de que el dispositivo haya puesto sus
|
|||
|
propios datos, o las líneas de caché presentes en el caché de la CPU pueden
|
|||
|
simplemente ocultar el hecho de que la memoria RAM se haya actualizado,
|
|||
|
hasta el momento en que la caché se descarta de la memoria caché de la CPU
|
|||
|
y se vuelve a cargar. Para hacer frente a esto, la parte apropiada del
|
|||
|
kernel debe invalidar los bits superpuestos del caché en cada CPU.
|
|||
|
|
|||
|
Consulte Documentation/core-api/cachetlb.rst para obtener más información
|
|||
|
sobre administración de la memoria caché.
|
|||
|
|
|||
|
|
|||
|
COHERENCIA DE CACHÉ FRENTE A MMIO
|
|||
|
---------------------------------
|
|||
|
|
|||
|
La E/S mapeada en memoria generalmente se lleva a cabo a través de
|
|||
|
ubicaciones de memoria que forman parte de una ventana del espacio de
|
|||
|
memoria de la CPU, que tiene diferentes propiedades asignadas que la
|
|||
|
ventana habitual dirigida a RAM.
|
|||
|
|
|||
|
Entre dichas propiedades, suele existir el hecho de que tales accesos
|
|||
|
eluden el almacenamiento en caché por completo e ir directamente a los
|
|||
|
buses del dispositivo. Esto significa que los accesos MMIO pueden, en
|
|||
|
efecto, superar los accesos a la memoria caché que se emitieron
|
|||
|
anteriormente. Una barrera de memoria no es suficiente en tal caso, sino
|
|||
|
que el caché debe ser vaciado entre la escritura de la memoria caché, y el
|
|||
|
acceso MMIO, si los dos son de cualquier manera dependiente.
|
|||
|
|
|||
|
|
|||
|
=======================
|
|||
|
COSAS QUE HACEN LAS CPU
|
|||
|
=======================
|
|||
|
|
|||
|
Un programador podría dar por sentado que la CPU realizará las operaciones
|
|||
|
de memoria exactamente en el orden especificado, de modo que si a la CPU se
|
|||
|
entrega, por ejemplo, el siguiente fragmento de código a ejecutar:
|
|||
|
|
|||
|
a = READ_ONCE(*A);
|
|||
|
WRITE_ONCE(*B, b);
|
|||
|
c = READ_ONCE(*C);
|
|||
|
d = READ_ONCE(*D);
|
|||
|
WRITE_ONCE(*E, e);
|
|||
|
|
|||
|
esperarían entonces que la CPU complete la operación de memoria para cada
|
|||
|
instrucción antes de pasar a la siguiente, lo que lleva a una definida
|
|||
|
secuencia de operaciones vistas por observadores externos en el sistema:
|
|||
|
|
|||
|
LOAD *A, STORE *B, LOAD *C, LOAD *D, STORE *E.
|
|||
|
|
|||
|
La realidad es, por supuesto, mucho más intrincada. Para muchas CPU y
|
|||
|
compiladores, la anterior suposición no se sostiene porque:
|
|||
|
|
|||
|
(*) es más probable que las cargas deban completarse de inmediato para
|
|||
|
permitir progreso en la ejecución, mientras que los stores a menudo se
|
|||
|
pueden aplazar sin problema;
|
|||
|
|
|||
|
(*) las cargas se pueden hacer especulativamente, y el resultado es
|
|||
|
descartado si resulta innecesario;
|
|||
|
|
|||
|
(*) las cargas se pueden hacer de forma especulativa, lo que lleva a que
|
|||
|
se haya obtenido el resultado en el momento equivocado de la secuencia
|
|||
|
de eventos esperada;
|
|||
|
|
|||
|
(*) el orden de los accesos a memoria se puede reorganizar para promover
|
|||
|
un mejor uso de los buses y cachés de la CPU;
|
|||
|
|
|||
|
(*) las cargas y los stores se pueden combinar para mejorar el rendimiento
|
|||
|
cuando se habla con memoria o hardware de E/S, que puede realizar
|
|||
|
accesos por lotes a ubicaciones adyacentes, reduciendo así los costes
|
|||
|
de configuración de transacciones (la memoria y los dispositivos PCI
|
|||
|
pueden ambos pueden hacer esto); y
|
|||
|
|
|||
|
(*) la caché de datos de la CPU puede afectar al orden, y mientras sus
|
|||
|
mecanismos de coherencia pueden aliviar esto, una vez que el store
|
|||
|
haya accedido al caché- no hay garantía de que la gestión de la
|
|||
|
coherencia se propague en orden a otras CPU.
|
|||
|
|
|||
|
Entonces, digamos que lo que otra CPU podría observar en el fragmento de
|
|||
|
código anterior es:
|
|||
|
|
|||
|
LOAD *A, ..., LOAD {*C,*D}, STORE *E, STORE *B
|
|||
|
|
|||
|
(Donde "LOAD {*C,*D}" es una carga combinada)
|
|||
|
|
|||
|
|
|||
|
Sin embargo, se garantiza que una CPU es autoconsistente: verá que sus
|
|||
|
_propios_ accesos parecen estar correctamente ordenados, sin necesidad de
|
|||
|
barrera de memoria. Por ejemplo con el siguiente código:
|
|||
|
|
|||
|
U = READ_ONCE(*A);
|
|||
|
WRITE_ONCE(*A, V);
|
|||
|
WRITE_ONCE(*A, W);
|
|||
|
X = READ_ONCE(*A);
|
|||
|
WRITE_ONCE(*A, Y);
|
|||
|
Z = READ_ONCE(*A);
|
|||
|
|
|||
|
y asumiendo que no hay intervención de una influencia externa, se puede
|
|||
|
suponer que el resultado final se parecerá a:
|
|||
|
|
|||
|
U == el valor original de *A
|
|||
|
X == W
|
|||
|
Z == Y
|
|||
|
*A == Y
|
|||
|
|
|||
|
El código anterior puede hacer que la CPU genere la secuencia completa de
|
|||
|
accesos de memoria:
|
|||
|
|
|||
|
U=LOAD *A, STORE *A=V, STORE *A=W, X=LOAD *A, STORE *A=Y, Z=LOAD *A
|
|||
|
|
|||
|
en ese orden, pero, sin intervención, la secuencia puede contener casi
|
|||
|
cualquier combinación de elementos combinados o descartados, siempre que la
|
|||
|
perspectiva del programa del mundo siga siendo consistente. Tenga en cuenta
|
|||
|
que READ_ONCE() y WRITE_ONCE() -no- son opcionales en el ejemplo anterior,
|
|||
|
ya que hay arquitecturas donde una CPU determinada podría reordenar cargas
|
|||
|
sucesivas en la misma ubicación. En tales arquitecturas, READ_ONCE() y
|
|||
|
WRITE_ONCE() hacen lo que sea necesario para evitar esto, por ejemplo, en
|
|||
|
Itanium los casts volátiles utilizados por READ_ONCE() y WRITE_ONCE() hacen
|
|||
|
que GCC emita las instrucciones especiales ld.acq y st.rel
|
|||
|
(respectivamente) que impiden dicha reordenación.
|
|||
|
|
|||
|
El compilador también puede combinar, descartar o diferir elementos de la
|
|||
|
secuencia antes incluso de que la CPU los vea.
|
|||
|
|
|||
|
Por ejemplo:
|
|||
|
|
|||
|
*A = V;
|
|||
|
*A = W;
|
|||
|
|
|||
|
puede reducirse a:
|
|||
|
|
|||
|
*A = W;
|
|||
|
|
|||
|
ya que, sin una barrera de escritura o WRITE_ONCE(), puede que se asuma
|
|||
|
que el efecto del almacenamiento de V a *A se pierde. Similarmente:
|
|||
|
|
|||
|
*A = Y;
|
|||
|
Z = *A;
|
|||
|
|
|||
|
puede, sin una barrera de memoria o un READ_ONCE() y WRITE_ONCE(), esto
|
|||
|
sea reducido a:
|
|||
|
|
|||
|
*A = Y;
|
|||
|
Z = Y;
|
|||
|
|
|||
|
y la operación LOAD nunca aparezca fuera de la CPU.
|
|||
|
|
|||
|
|
|||
|
Y LUEGO ESTÁ EL ALFA
|
|||
|
--------------------
|
|||
|
|
|||
|
La CPU DEC Alpha es una de las CPU más relajadas que existen. No solo eso,
|
|||
|
algunas versiones de la CPU Alpha tienen un caché de datos dividido, lo que
|
|||
|
les permite tener dos líneas de caché relacionadas semánticamente,
|
|||
|
actualizadas en momentos separados. Aquí es donde la barrera de dependencia
|
|||
|
de dirección realmente se vuelve necesaria, ya que se sincronizan ambos
|
|||
|
cachés con el sistema de coherencia de memoria, lo que hace que parezca un
|
|||
|
cambio en el puntero, frente a que los nuevos datos se produzcan en el
|
|||
|
orden correcto.
|
|||
|
|
|||
|
Alpha define el modelo de memoria del kernel Linux, aunque a partir de
|
|||
|
v4.15, la adición al kernel de Linux de smp_mb() a READ_ONCE() en Alpha
|
|||
|
redujo en gran medida su impacto en el modelo de memoria.
|
|||
|
|
|||
|
|
|||
|
GUESTS DE MÁQUINAS VIRTUALES
|
|||
|
-----------------------------
|
|||
|
|
|||
|
Los "guests" (invitados) que se ejecutan en máquinas virtuales pueden verse
|
|||
|
afectados por los efectos de SMP incluso si el "host" (huésped) en sí se
|
|||
|
compila sin compatibilidad con SMP. Este es un efecto de la interacción con
|
|||
|
un host SMP mientras ejecuta un kernel UP. El uso obligatorio de barreras
|
|||
|
para este caso de uso sería posible, pero a menudo no son óptimas.
|
|||
|
|
|||
|
Para hacer frente a este caso de manera óptima, están disponibles macros de
|
|||
|
bajo nivel virt_mb() etc. Estas tienen el mismo efecto que smp_mb(), etc.
|
|||
|
cuando SMP está habilitado, pero generan código idéntico para sistemas SMP
|
|||
|
y no SMP. Por ejemplo, los invitados de máquinas virtuales debería usar
|
|||
|
virt_mb() en lugar de smp_mb() al sincronizar contra un (posiblemente SMP)
|
|||
|
anfitrión.
|
|||
|
|
|||
|
Estos son equivalentes a sus contrapartes smp_mb() etc. en todos los demás
|
|||
|
aspectos, en particular, no controlan los efectos MMIO: para controlar los
|
|||
|
efectos MMIO, utilice barreras obligatorias.
|
|||
|
|
|||
|
|
|||
|
================
|
|||
|
EJEMPLOS DE USOS
|
|||
|
================
|
|||
|
|
|||
|
BUFFERS CIRCULARES
|
|||
|
------------------
|
|||
|
|
|||
|
Las barreras de memoria se pueden utilizar para implementar almacenamiento
|
|||
|
en búfer circular, sin necesidad de un cerrojo para serializar al productor
|
|||
|
con el consumidor. Vea:
|
|||
|
|
|||
|
Documentation/core-api/circular-buffers.rst
|
|||
|
|
|||
|
para más detalles.
|
|||
|
|
|||
|
|
|||
|
===========
|
|||
|
REFERENCIAS
|
|||
|
===========
|
|||
|
|
|||
|
Alpha AXP Architecture Reference Manual, Segunda Edición (por Sites & Witek,
|
|||
|
Digital Press)
|
|||
|
Capítulo 5.2: Physical Address Space Characteristics
|
|||
|
Capítulo 5.4: Caches and Write Buffers
|
|||
|
Capítulo 5.5: Data Sharing
|
|||
|
Capítulo 5.6: Read/Write Ordering
|
|||
|
|
|||
|
AMD64 Architecture Programmer's Manual Volumen 2: System Programming
|
|||
|
Capítulo 7.1: Memory-Access Ordering
|
|||
|
Capítulo 7.4: Buffering and Combining Memory Writes
|
|||
|
|
|||
|
ARM Architecture Reference Manual (ARMv8, for ARMv8-A architecture profile)
|
|||
|
Capítulo B2: The AArch64 Application Level Memory Model
|
|||
|
|
|||
|
IA-32 Intel Architecture Software Developer's Manual, Volumen 3:
|
|||
|
System Programming Guide
|
|||
|
Capítulo 7.1: Locked Atomic Operations
|
|||
|
Capítulo 7.2: Memory Ordering
|
|||
|
Capítulo 7.4: Serializing Instructions
|
|||
|
|
|||
|
The SPARC Architecture Manual, Version 9
|
|||
|
Capítulo 8: Memory Models
|
|||
|
Appendix D: Formal Specification of the Memory Models
|
|||
|
Appendix J: Programming with the Memory Models
|
|||
|
|
|||
|
Storage in the PowerPC (por Stone and Fitzgerald)
|
|||
|
|
|||
|
UltraSPARC Programmer Reference Manual
|
|||
|
Capítulo 5: Memory Accesses and Cacheability
|
|||
|
Capítulo 15: Sparc-V9 Memory Models
|
|||
|
|
|||
|
UltraSPARC III Cu User's Manual
|
|||
|
Capítulo 9: Memory Models
|
|||
|
|
|||
|
UltraSPARC IIIi Processor User's Manual
|
|||
|
Capítulo 8: Memory Models
|
|||
|
|
|||
|
UltraSPARC Architecture 2005
|
|||
|
Capítulo 9: Memory
|
|||
|
Appendix D: Formal Specifications of the Memory Models
|
|||
|
|
|||
|
UltraSPARC T1 Supplement to the UltraSPARC Architecture 2005
|
|||
|
Capítulo 8: Memory Models
|
|||
|
Appendix F: Caches and Cache Coherency
|
|||
|
|
|||
|
Solaris Internals, Core Kernel Architecture, p63-68:
|
|||
|
Capítulo 3.3: Hardware Considerations for Locks and
|
|||
|
Synchronization
|
|||
|
|
|||
|
Unix Systems for Modern Architectures, Symmetric Multiprocessing and Caching
|
|||
|
for Kernel Programmers:
|
|||
|
Capítulo 13: Other Memory Models
|
|||
|
|
|||
|
Intel Itanium Architecture Software Developer's Manual: Volumen 1:
|
|||
|
Sección 2.6: Speculation
|
|||
|
Sección 4.4: Memory Access
|