torvalds - WRITE_ONCE en las listas del kernel de Linux
una de las tareas del kernel es: (1)
La primera definición a la que te refieres es parte del validador de bloqueo del núcleo , también conocido como "lockdep". WRITE_ONCE
(y otros) no necesitan un tratamiento especial, pero el motivo es el tema de otra pregunta.
La definición relevante estaría aquí , y un comentario muy conciso declara que su propósito es:
Evita que el compilador fusione o recupere lecturas o escrituras.
...
Asegurarse de que el compilador no pliegue, modifique o no mutile los accesos que no requieran ordenamiento o que interactúen con una barrera de memoria explícita o instrucción atómica que proporcione el ordenamiento requerido.
Pero, ¿qué significan esas palabras?
El problema
El problema es en realidad plural:
Lectura / escritura "tearing": reemplazando un solo acceso de memoria por muchos más pequeños. GCC puede (y lo hace) en ciertas situaciones reemplazar algo como
p = 0x01020304;
con dos instrucciones inmediatas de almacenamiento de 16 bits, en lugar de presumiblemente colocar la constante en un registro y luego un acceso a la memoria, y así sucesivamente.WRITE_ONCE
nos permitiría decirle a GCC, "no hagas eso", así:WRITE_ONCE(p, 0x01020304);
Los compiladores de C han dejado de garantizar que una palabra de acceso es atómica. Cualquier programa que no sea libre de carreras puede ser miscompiled con resultados espectaculares. No solo eso, sino que un compilador puede decidir no mantener ciertos valores en los registros dentro de un bucle, lo que lleva a múltiples referencias que pueden desordenar el código de esta manera:
for(;;) { owner = lock->owner; if (owner && !mutex_spin_on_owner(lock, owner)) break; /* ... */ }
- En ausencia de accesos de "etiquetado" a la memoria compartida, no podemos detectar automáticamente accesos no deseados de ese tipo. Las herramientas automatizadas que intentan encontrar este tipo de errores no pueden distinguirlos de accesos intencionalmente delicados.
La solución
Comenzamos notando que el kernel de Linux requiere ser construido con GCC. Por lo tanto, solo hay un compilador que debemos cuidar con la solución, y podemos usar su documentation como la única guía.
Para una solución genérica, necesitamos manejar accesos de memoria de todos los tamaños. Tenemos todos los diferentes tipos de anchos específicos, y todo lo demás. También notamos que no necesitamos etiquetar específicamente los accesos a la memoria que ya están en las secciones críticas ( ¿por qué no? ).
Para tamaños de 1, 2, 4 y 8 bytes, existen tipos apropiados, y volatile
específicamente impide que GCC aplique la optimización a la que nos referimos en (1), además de atender otros casos (la última viñeta en "COMPILER BARRERAS "). También no permite que GCC compile mal el bucle en (2), ya que movería el acceso volatile
a través de un punto de secuencia, y el estándar C no lo permite. Linux uses lo que llamamos un "acceso volátil" (ver más abajo) en lugar de etiquetar un objeto como volátil. Podríamos resolver nuestro problema marcando el objeto específico como volatile
, pero esto es (casi?) Nunca una buena opción. Hay many reasons que podría ser perjudicial.
Así es como se implementa un acceso volátil (escritura) en el kernel para un tipo ancho de 8 bits:
*(volatile __u8_alias_t *) p = *(__u8_alias_t *) res;
Supongamos que no sabíamos exactamente qué hace volatile
, ¡y descubrirlo no es fácil! (ver # 5) - otra forma de lograr esto sería colocar barreras de memoria: esto es exactamente lo que hace Linux en caso de que el tamaño sea diferente a 1,2,4 u 8, recurriendo a memcpy
y colocando barreras de memoria antes y después de la llamada. Las barreras de memoria también resuelven fácilmente el problema (2), pero incurren en grandes penalizaciones de rendimiento.
Espero haber cubierto una descripción general sin profundizar en las interpretaciones del estándar C, pero si lo desea, puedo tomarme el tiempo para hacerlo.
Estoy leyendo la implementación del kernel de Linux de la lista de enlaces duplicados. No entiendo el uso de la macro WRITE_ONCE(x, val)
. Se define como sigue en compiler.h:
#define WRITE_ONCE(x, val) x=(val)
Se usa siete veces en el archivo, como
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
WRITE_ONCE(prev->next, new);
}
He leído que se utiliza para evitar condiciones de carrera.
Tengo dos preguntas:
1 / Pensé que la macro fue reemplazada por código en tiempo de compilación. Entonces, ¿cómo difiere este código al siguiente? ¿Cómo esta macro puede evitar condiciones de carrera?
static inline void __list_add(struct list_head *new,
struct list_head *prev,
struct list_head *next)
{
next->prev = new;
new->next = next;
new->prev = prev;
prev->next = new;
}
2 / ¿Cómo saber cuándo debemos usarlo? Por ejemplo, se usa para __lst_add()
pero no para __lst_splice()
:
static inline void __list_splice(const struct list_head *list,
struct list_head *prev,
struct list_head *next)
{
struct list_head *first = list->next;
struct list_head *last = list->prev;
first->prev = prev;
prev->next = first;
last->next = next;
next->prev = last;
}
editar:
Aquí hay un mensaje de confirmación sobre este archivo y WRITE_ONCE
, pero no me ayuda a entender nada ...
lista: use WRITE_ONCE () al inicializar las estructuras list_head
El código que realiza las pruebas de vacío sin bloqueo de listas que no son RCU se basa en INIT_LIST_HEAD () para escribir el siguiente puntero del encabezado de lista atómicamente, especialmente cuando se invoca INIT_LIST_HEAD () desde list_del_init (). Por lo tanto, esta confirmación agrega WRITE_ONCE () a los almacenes de punteros de esta función que podrían afectar al siguiente puntero de la cabeza.