| NOTE: |
| This is a version of Documentation/memory-barriers.txt translated into |
| Spanish by Carlos Bilbao <carlos.bilbao.osdev@gmail.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.osdev@gmail.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 |