Guía Informal al Bloqueo - Técnicas Comunes
Tutorial creado por Paul Rusty Russell. Extraido de: http://es.tldp.org/Manuales-LuCAS/linux-bloqueo/multiple-html/
02 de Marzo de 2006
Linux
4 - Técnicas Comunes
Esta sección lista algunos dilemas comunes y las soluciones estándar usadas en el código del núcleo Linux. Si usas estas, la gente encontrará tu código más fácil de entender.
Si pudiera darte una parte de un aviso sería: nunca duermas con alguien más loco/a que tú. Pero si tuviera que darte un aviso en el bloqueo: mantente sólo.
Bloquea a los datos, no al código.
Se reacio a introducir nuevos bloqueos.
Suficientemente ajeno, esto es justo lo contrario de mi aviso cuando tienes que dormir con alguien más loco/a que tú.
Hay un caso bastante común donde un manejador de interrupciones necesita acceder a la región crítica, pero no necesita acceso de escritura. En este caso, no necesitas usar read_lock_irq(), únicamente read_lock() en todos los sitios (desde que ocurre una interrupción, el manejador irq sólo intentará coger el bloqueo, que no hará deadlock). Todavía necesitas usar write_lock_irq().
Una lógica similar se aplica al bloqueo entre softirqs/tasklets/BHs que nunca necesitan un bloqueo de escritura, y al contexto de usuario: read_lock() y write_lock_bh().
Hay un fallo de codificación donde un pedazo de código intenta obtener un spinlock dos veces: él esperará siempre, esperando a que el bloqueo sea liberado (spinlocks, rwlocks y semáforos no son recursivos en Linux). Esto es trivial de diagnosticar: no es un tipo de problema de estar-cinco-noches-despierto-hablando-con-los-suaves-conejitos-del-código.
Para un caso ligeramente más complejo, imagínate que tienes una región compartida por un bottom half y un contexto de usuario. Si usas una llamada spin_lock() para protegerla, es posible que el contexto de usuario sea interrumpido por el bottom half mientras mantiene el bloqueo, y el bottom half entonces esperará para siempre para obtener el mismo bloqueo.
Ambas son llamadas deadlock (bloqueo muerto), y como se mostró antes, puede ocurrir con una CPU simple (aunque no en compilaciones para UP, ya que los spinlocks se desvanecen en la compilación del núcleo con CONFIG_SMP=n. Aún tendrás corrupción de datos en el segundo ejemplo).
Este bloqueo completo es fácil de diagnosticar: en equipos SMP el cronómetro guardián o compilado con DEBUG_SPINLOCKS establecido (include/linux/spinlock.h) nos mostrará esto inmediatamente cuando suceda.
Un problema más complejo es el también llamado `abrazo mortal', involucrando a dos o más bloqueos. Digamos que tienes una tabla hash: cada entrada en la tabla es un spinlock, y una cadena de objetos ordenados. Dentro de un manejador softirq, algunas veces quieres alterar un objeto de un lugar de la tabla hash a otro: coges el spinlock de la vieja cadena hash y el spinlock de la nueva cadena hash, y borras el objeto de la vieja y lo insertas en la nueva.
Aquí hay dos problemas. El primero es que si tu código siempre intenta mover el objeto a la misma cadena, él se hará un deadlock cuando se intente bloquear dos veces. El segundo es que si la misma softirq u otra CPU está intentando mover otro objeto en la dirección inversa podría pasar lo siguiente:
Tabla 4-1. Consecuencias
|| CPU 1 || CPU 2 ||
|| Pilla bloqueo A -> OK || Pilla bloqueo B -> OK ||
|| Pilla bloqueo B -> spin || Pilla bloqueo A -> spin ||
Las dos CPUs esperarán para siempre, esperando a que el otro libere su bloqueo. Él parecerá, olerá, y se sentirá como si cayera el sistema.
Preveniendo los Deadlocks
Los libros de texto te dirán que si siempre bloqueas en el mismo orden, nunca obtendrás esta clase de deadlock. La práctica te dirá que este tipo de aproximación no escala bien: cuando creo un nuevo bloqueo, no entiendo suficientemente el núcleo para imaginarme dónde está él en la jerarquía de los 5000 bloqueos.
Los mejores bloqueos están encapsulados; nunca estarán expuestos en las cabeceras, y nunca se mantendrán a través de llamadas a funciones no triviales fuera del mismo archivo. Puedes leer a través de este código y ver que nunca hará deadlock, porque nunca intenta tener otro bloqueo mientras tiene el uso. La gente usando tu código no necesita saber nunca que estás usando un bloqueo.
Un problema clásico aquí es cuando suministras retrollamadas o trampas: si las llamas con el bloqueo mantenido, arriesgas un deadlock simple, o un abrazo mortal (¿quién sabe lo que hará la llamada?). Recuerda, los otros programadores andan detrás de ti, por lo tanto no hagas esto.
Sobreentusiasmo en la Prevención de Deadlocks
Los deadlocks son problemáticos, pero no son tan malos como la corrupción de datos. El código que obtiene un bloqueo de lectura, busca una lista, falla al encontrar lo que quiere, tira el bloqueo de lectura, obtiene un bloqueo de escritura e inserta el objeto tiene una condición de carrera.
Si no ves porqué, por favor permanece jodidamente lejos de mi código.
Una gran técnica usada ampliamente para eliminar el bloqueo es duplicar la información para cada CPU. Por ejemplo, si quieres mantener una cuenta de una condición común, puedes usar un spinlock y un contador simple. Bonito y simple.
Si esto era muy lento [probablemente no], puedes en vez de esto usar un contador para cada CPU [no lo hagas], entonces ninguno de ellos necesitarán un bloqueo exclusivo [estás gastando tu tiempo aquí]. Para asegurarte de que las CPUs no tienen que sincronizar las cachés todo el tiempo, alinea los contadores al límite de las cachés añadiendo `cacheline_aligned' a la declaración (include/linux/cache.h). [¿No puedes pensar en alguna cosa mejor que hacer?]
De cualquier forma necesitarán un bloqueo de lectura para acceder a sus propios contadores. De esta forma puedes usar un bloqueo de escritura para garantizar acceso exclusivo a todos ellos a la vez, para llevar cuenta de ellos.
Un ejemplo clásico de información para cada CPU son los bloqueos `gran lector' de Ingo (linux/include/brlock.h). Estos usan técnicas de datos de cada CPU descritas más adelante para crear un bloqueo que es muy rápido en obtener un bloqueo de lectura, pero agonizantemente lento para un bloqueo de escritura.
Afortunadamente, hay un número limitado disponible de estos bloqueos: tienes que ir a través de un proceso de entrevista estricta para obtener uno.
Algunas veces es posible eliminar el bloqueo. Considera el siguiente caso del código del cortafuegos 2.2, que inserta un elemento en una lista simplemente enlazada en el contexto de usuario:
new->next = i->next;
i->next = new;
Aquí el autor (Alan Cox, que sabía lo que estaba haciendo) asume que el asignamiento de punteros es atómico. Esto es importante, porque los paquetes de red atravesarían esta lista en bottom halves sin un bloqueo. Dependiendo del tiempo exacto, ellos verían el nuevo elemento en las lista con un puntero next válido, o no verían la lista todavía. Aún se requiere un bloqueo contra otras CPUs insertando o borrando de la lista, por supuesto.
Por supuesto, las escrituras deben estar en este orden, en otro caso el nuevo elemento aparece en la lista con un puntero next inválido, y alguna otra CPU iterando en el tiempo equivocado saltará a través de él a la basura. Porque las modernas CPUs reordenan, el código de Alan actualmente se lee como sigue:
new->next = i->next;
wmb();
i->next = new;
La función wmb() es una barrera de escritura de memoria (include/asm/system.h): ni el compilador ni la CPU permitirán alguna escritura a memoria después de que wmb() sea visible a otro hardware antes de que alguna otra escritura se encuentre antes de wmb().
Como i386 no realiza reordenamiento de escritura, este bug nunca fue mostrada en esta plataforma. Es otras plataformas SMP, de cualquier forma, si que fue mostrado.
También hay rmb() para ordenamiento de lectura: para asegurar que cualquier lectura previa de una variable ocurre antes de la siguiente lectura. La macro simple mb() combina rmb() y wmb().
Algunas operaciones atómicas están definidas para actuar como una barrera de memoria (esto es, como la macro mb(), pero si dudas, se explícito. También, las operaciones de spinlock actuan como barreras parciales: las operaciones después de obtener un spinlock nunca serán movidas para preceder a la llamada spin_lock(), y las operaciones antes de liberar un spinlock nunca serán movidas después de la llamada spin_unlock().
Hay un número de operaciones atómicas definidas en include/asm/atomic.h: estas están garantizadas que serán atómicas para todas las CPUs en el sistema, entonces eliminando las carreras. Si tus datos compartidos consisten, digamos, en un simple contador, estas operaciones quizás sean más simples que usar spinlocks (aunque para algo no trivial el uso de spinlocks es más claro).
Destacar que las operaciones atómicas están definidas para actuar como barreras de escritura y lectura en todas las plataformas.
Bloqueando una colección de objetos es bastante fácil: coges un spinlock simple, y te aseguras de obtenerlo antes de buscar, añadir o borrar un objeto.
El propósito de este bloqueo no es proteger los objetos individuales: quizás tengas un bloqueo separado dentro de cada uno de ellos. Es para proteger la estructura de datos conteniendo el objeto de las condiciones de carrera. Frecuentemente el mismo bloqueo es usado también para proteger los contenidos de todos los objetos, por simplicidad, pero ellos son inherentemente ortogonales (y muchas otras grandes palabras diseñadas para confundir).
Cambiando esto a un bloqueo de lectura-escritura frecuentemente ayudará notablemente si las lecturas son más frecuentes que las escrituras. Si no, hay otra aproximación que puedes usar para reducir el tiempo que es mantenido el bloqueo: las cuentas de referencia.
En esta aproximación, un objeto tiene un dueño, quien establece la cuenta de referencia a uno. Cuando obtienes un puntero al objeto, incrementas la cuenta de referencia (una operación 'obtener'). Cuando abandonas un puntero, decrementas la cuenta de referencia (una operación 'poner'). Cuando el dueño quiere destruirlo, lo marca como muerto y hace una operación poner.
Cualquiera que ponga la cuenta de referencia a cero (usualmente implementado con atomic_dec_and_test()) limpia y libera el objeto.
Esto significa que se garantiza que el objeto no se desvanecerá debajo de ti, incluso aunque no tengas más un bloqueo para la colección.
Aquí hay algún código esqueleto:
void create_foo(struct foo *x)
{
atomic_set(&x->use, 1);
spin_lock_bh(&list_lock);
... inserta en la lista ...
spin_unlock_bh(&list_lock);
}
struct foo *get_foo(int desc)
{
struct foo *ret;
spin_lock_bh(&list_lock);
... encuentra en la lista ...
if (ret) atomic_inc(&ret->use);
spin_unlock_bh(&list_lock);
return ret;
}
void put_foo(struct foo *x)
{
if (atomic_dec_and_test(&x->use))
kfree(foo);
}
void destroy_foo(struct foo *x)
{
spin_lock_bh(&list_lock);
... borra de la lista ...
spin_unlock_bh(&list_lock);
put_foo(x);
}
Macros Para Ayudarte
Hay un conjunto de macros de depuración recogidas dentro de include/linux/netfilter_ipv4/lockhelp.h y listhelp.h: estas son muy útiles para asegurarnos de que los bloqueos son mantenidos en los sitios correctos para proteger la infraestructura.
Nunca puedes llamar a las siguientes rutinas mientras estás manteniendo un spinlock, porque ellas quizás se vayan a dormir. Esto también significa que necesitas estar en el contexto de usuario.
printk() puede ser llamada en cualquier contexto, suficientemente interesante.
Alan Cox dice "la deshabilitación/habilitación de una sparc es en la ventana registrada". Andi Kleen dice "cuando restore_flags (restauras las banderas) en una función diferente ensucias todas las ventanas de registros".
Por lo tanto nunca pases el conjunto de palabras de flags por spin_lock_irqsave() y hermanos a otra función (a menos que sea declarada inline). Usualmente nadie hace esto, pero ahora ya estás advertido. Dave Miller nunca puede hacer nada de una forma directa (Puedo decir esto porque tengo fotos de él y de cierto defensor de PowerPC en una posición comprometida).
Los cronómetros pueden producir sus propios problemas con las carreras. Considera una colección de objeros (listas, hash, etc) donde cada objeto tiene un cronómetro que lo va a destruir.
Si quieres destruir la colección entera (digamos en el borrado de un módulo), quizás realices lo siguiente:
/* ESTE CÓDIGO ES MALO MALO MALO MALO: SI HUBIERA ALGO PEOR
USUARÍA NOTACIÓN HÚNGARA */
spin_lock_bh(&list_lock);
while (list) {
struct foo *next = list->next;
del_timer(&list->timer);
kfree(list);
list = next;
}
spin_unlock_bh(&list_lock);
Tarde o temprano, esto rompería en SMP, porque un cronómetro puede acabar antes que spin_lock_bh(), y sólo obtendría el bloqueo después de spin_unlock_bh(), y entonces intentaría liberar el elemento (¡el cual ya ha sido liberado!).
Esto puede ser eliminado comprobando el resultado de del_timer(): si retorna 1, el cronómetro ha sido borrado. Si , significa (en este caso) que está actualmente ejecutándose, por lo tanto lo que podemos hacer es:
retry:
spin_lock_bh(&list_lock);
while (list) {
struct foo *next = list->next;
if (!del_timer(&list->timer)) {
/* Le da al cronómetro una oportunidad para borrarlo */
spin_unlock_bh(&list_lock);
goto retry;
}
kfree(list);
list = next;
}
spin_unlock_bh(&list_lock);
Otro problema común es el borrando de cronómetros que se reinician a ellos mismos (llamando a add_timer() al final de su función cronómetro). Porque este es un caso bastante común que es propenso a carreras, puedes poner una llamada a timer_exit() muy al funal de tu función cronómetro, y usar del_timer_sync() para manejar este caso. Él retorna el número de veces que el cronómetro tuvo que ser borrado antes de que finalmente lo paráramos añadiéndolo otra vez.
Si pudiera darte una parte de un aviso sería: nunca duermas con alguien más loco/a que tú. Pero si tuviera que darte un aviso en el bloqueo: mantente sólo.
Bloquea a los datos, no al código.
Se reacio a introducir nuevos bloqueos.
Suficientemente ajeno, esto es justo lo contrario de mi aviso cuando tienes que dormir con alguien más loco/a que tú.
En un Contexto de Interrupciones No Escritores
Hay un caso bastante común donde un manejador de interrupciones necesita acceder a la región crítica, pero no necesita acceso de escritura. En este caso, no necesitas usar read_lock_irq(), únicamente read_lock() en todos los sitios (desde que ocurre una interrupción, el manejador irq sólo intentará coger el bloqueo, que no hará deadlock). Todavía necesitas usar write_lock_irq().
Una lógica similar se aplica al bloqueo entre softirqs/tasklets/BHs que nunca necesitan un bloqueo de escritura, y al contexto de usuario: read_lock() y write_lock_bh().
Deadlock: Simple y Avanzado
Hay un fallo de codificación donde un pedazo de código intenta obtener un spinlock dos veces: él esperará siempre, esperando a que el bloqueo sea liberado (spinlocks, rwlocks y semáforos no son recursivos en Linux). Esto es trivial de diagnosticar: no es un tipo de problema de estar-cinco-noches-despierto-hablando-con-los-suaves-conejitos-del-código.
Para un caso ligeramente más complejo, imagínate que tienes una región compartida por un bottom half y un contexto de usuario. Si usas una llamada spin_lock() para protegerla, es posible que el contexto de usuario sea interrumpido por el bottom half mientras mantiene el bloqueo, y el bottom half entonces esperará para siempre para obtener el mismo bloqueo.
Ambas son llamadas deadlock (bloqueo muerto), y como se mostró antes, puede ocurrir con una CPU simple (aunque no en compilaciones para UP, ya que los spinlocks se desvanecen en la compilación del núcleo con CONFIG_SMP=n. Aún tendrás corrupción de datos en el segundo ejemplo).
Este bloqueo completo es fácil de diagnosticar: en equipos SMP el cronómetro guardián o compilado con DEBUG_SPINLOCKS establecido (include/linux/spinlock.h) nos mostrará esto inmediatamente cuando suceda.
Un problema más complejo es el también llamado `abrazo mortal', involucrando a dos o más bloqueos. Digamos que tienes una tabla hash: cada entrada en la tabla es un spinlock, y una cadena de objetos ordenados. Dentro de un manejador softirq, algunas veces quieres alterar un objeto de un lugar de la tabla hash a otro: coges el spinlock de la vieja cadena hash y el spinlock de la nueva cadena hash, y borras el objeto de la vieja y lo insertas en la nueva.
Aquí hay dos problemas. El primero es que si tu código siempre intenta mover el objeto a la misma cadena, él se hará un deadlock cuando se intente bloquear dos veces. El segundo es que si la misma softirq u otra CPU está intentando mover otro objeto en la dirección inversa podría pasar lo siguiente:
Tabla 4-1. Consecuencias
|| CPU 1 || CPU 2 ||
|| Pilla bloqueo A -> OK || Pilla bloqueo B -> OK ||
|| Pilla bloqueo B -> spin || Pilla bloqueo A -> spin ||
Las dos CPUs esperarán para siempre, esperando a que el otro libere su bloqueo. Él parecerá, olerá, y se sentirá como si cayera el sistema.
Preveniendo los Deadlocks
Los libros de texto te dirán que si siempre bloqueas en el mismo orden, nunca obtendrás esta clase de deadlock. La práctica te dirá que este tipo de aproximación no escala bien: cuando creo un nuevo bloqueo, no entiendo suficientemente el núcleo para imaginarme dónde está él en la jerarquía de los 5000 bloqueos.
Los mejores bloqueos están encapsulados; nunca estarán expuestos en las cabeceras, y nunca se mantendrán a través de llamadas a funciones no triviales fuera del mismo archivo. Puedes leer a través de este código y ver que nunca hará deadlock, porque nunca intenta tener otro bloqueo mientras tiene el uso. La gente usando tu código no necesita saber nunca que estás usando un bloqueo.
Un problema clásico aquí es cuando suministras retrollamadas o trampas: si las llamas con el bloqueo mantenido, arriesgas un deadlock simple, o un abrazo mortal (¿quién sabe lo que hará la llamada?). Recuerda, los otros programadores andan detrás de ti, por lo tanto no hagas esto.
Sobreentusiasmo en la Prevención de Deadlocks
Los deadlocks son problemáticos, pero no son tan malos como la corrupción de datos. El código que obtiene un bloqueo de lectura, busca una lista, falla al encontrar lo que quiere, tira el bloqueo de lectura, obtiene un bloqueo de escritura e inserta el objeto tiene una condición de carrera.
Si no ves porqué, por favor permanece jodidamente lejos de mi código.
Datos por cada CPU
Una gran técnica usada ampliamente para eliminar el bloqueo es duplicar la información para cada CPU. Por ejemplo, si quieres mantener una cuenta de una condición común, puedes usar un spinlock y un contador simple. Bonito y simple.
Si esto era muy lento [probablemente no], puedes en vez de esto usar un contador para cada CPU [no lo hagas], entonces ninguno de ellos necesitarán un bloqueo exclusivo [estás gastando tu tiempo aquí]. Para asegurarte de que las CPUs no tienen que sincronizar las cachés todo el tiempo, alinea los contadores al límite de las cachés añadiendo `cacheline_aligned' a la declaración (include/linux/cache.h). [¿No puedes pensar en alguna cosa mejor que hacer?]
De cualquier forma necesitarán un bloqueo de lectura para acceder a sus propios contadores. De esta forma puedes usar un bloqueo de escritura para garantizar acceso exclusivo a todos ellos a la vez, para llevar cuenta de ellos.
Bloqueos Gran Lector
Un ejemplo clásico de información para cada CPU son los bloqueos `gran lector' de Ingo (linux/include/brlock.h). Estos usan técnicas de datos de cada CPU descritas más adelante para crear un bloqueo que es muy rápido en obtener un bloqueo de lectura, pero agonizantemente lento para un bloqueo de escritura.
Afortunadamente, hay un número limitado disponible de estos bloqueos: tienes que ir a través de un proceso de entrevista estricta para obtener uno.
Eliminando los bloqueos: Ordenamiento de Lecturas y Escrituras
Algunas veces es posible eliminar el bloqueo. Considera el siguiente caso del código del cortafuegos 2.2, que inserta un elemento en una lista simplemente enlazada en el contexto de usuario:
new->next = i->next;
i->next = new;
Aquí el autor (Alan Cox, que sabía lo que estaba haciendo) asume que el asignamiento de punteros es atómico. Esto es importante, porque los paquetes de red atravesarían esta lista en bottom halves sin un bloqueo. Dependiendo del tiempo exacto, ellos verían el nuevo elemento en las lista con un puntero next válido, o no verían la lista todavía. Aún se requiere un bloqueo contra otras CPUs insertando o borrando de la lista, por supuesto.
Por supuesto, las escrituras deben estar en este orden, en otro caso el nuevo elemento aparece en la lista con un puntero next inválido, y alguna otra CPU iterando en el tiempo equivocado saltará a través de él a la basura. Porque las modernas CPUs reordenan, el código de Alan actualmente se lee como sigue:
new->next = i->next;
wmb();
i->next = new;
La función wmb() es una barrera de escritura de memoria (include/asm/system.h): ni el compilador ni la CPU permitirán alguna escritura a memoria después de que wmb() sea visible a otro hardware antes de que alguna otra escritura se encuentre antes de wmb().
Como i386 no realiza reordenamiento de escritura, este bug nunca fue mostrada en esta plataforma. Es otras plataformas SMP, de cualquier forma, si que fue mostrado.
También hay rmb() para ordenamiento de lectura: para asegurar que cualquier lectura previa de una variable ocurre antes de la siguiente lectura. La macro simple mb() combina rmb() y wmb().
Algunas operaciones atómicas están definidas para actuar como una barrera de memoria (esto es, como la macro mb(), pero si dudas, se explícito. También, las operaciones de spinlock actuan como barreras parciales: las operaciones después de obtener un spinlock nunca serán movidas para preceder a la llamada spin_lock(), y las operaciones antes de liberar un spinlock nunca serán movidas después de la llamada spin_unlock().
Eliminando los Bloqueos: Operaciones Atómicas
Hay un número de operaciones atómicas definidas en include/asm/atomic.h: estas están garantizadas que serán atómicas para todas las CPUs en el sistema, entonces eliminando las carreras. Si tus datos compartidos consisten, digamos, en un simple contador, estas operaciones quizás sean más simples que usar spinlocks (aunque para algo no trivial el uso de spinlocks es más claro).
Destacar que las operaciones atómicas están definidas para actuar como barreras de escritura y lectura en todas las plataformas.
Protegiendo Una Colección de Objetos: Cuentas de Referencia
Bloqueando una colección de objetos es bastante fácil: coges un spinlock simple, y te aseguras de obtenerlo antes de buscar, añadir o borrar un objeto.
El propósito de este bloqueo no es proteger los objetos individuales: quizás tengas un bloqueo separado dentro de cada uno de ellos. Es para proteger la estructura de datos conteniendo el objeto de las condiciones de carrera. Frecuentemente el mismo bloqueo es usado también para proteger los contenidos de todos los objetos, por simplicidad, pero ellos son inherentemente ortogonales (y muchas otras grandes palabras diseñadas para confundir).
Cambiando esto a un bloqueo de lectura-escritura frecuentemente ayudará notablemente si las lecturas son más frecuentes que las escrituras. Si no, hay otra aproximación que puedes usar para reducir el tiempo que es mantenido el bloqueo: las cuentas de referencia.
En esta aproximación, un objeto tiene un dueño, quien establece la cuenta de referencia a uno. Cuando obtienes un puntero al objeto, incrementas la cuenta de referencia (una operación 'obtener'). Cuando abandonas un puntero, decrementas la cuenta de referencia (una operación 'poner'). Cuando el dueño quiere destruirlo, lo marca como muerto y hace una operación poner.
Cualquiera que ponga la cuenta de referencia a cero (usualmente implementado con atomic_dec_and_test()) limpia y libera el objeto.
Esto significa que se garantiza que el objeto no se desvanecerá debajo de ti, incluso aunque no tengas más un bloqueo para la colección.
Aquí hay algún código esqueleto:
void create_foo(struct foo *x)
{
atomic_set(&x->use, 1);
spin_lock_bh(&list_lock);
... inserta en la lista ...
spin_unlock_bh(&list_lock);
}
struct foo *get_foo(int desc)
{
struct foo *ret;
spin_lock_bh(&list_lock);
... encuentra en la lista ...
if (ret) atomic_inc(&ret->use);
spin_unlock_bh(&list_lock);
return ret;
}
void put_foo(struct foo *x)
{
if (atomic_dec_and_test(&x->use))
kfree(foo);
}
void destroy_foo(struct foo *x)
{
spin_lock_bh(&list_lock);
... borra de la lista ...
spin_unlock_bh(&list_lock);
put_foo(x);
}
Macros Para Ayudarte
Hay un conjunto de macros de depuración recogidas dentro de include/linux/netfilter_ipv4/lockhelp.h y listhelp.h: estas son muy útiles para asegurarnos de que los bloqueos son mantenidos en los sitios correctos para proteger la infraestructura.
Cosas Que Duermen
Nunca puedes llamar a las siguientes rutinas mientras estás manteniendo un spinlock, porque ellas quizás se vayan a dormir. Esto también significa que necesitas estar en el contexto de usuario.
- Accesos a userspace:
- copy_from_user()
- copy_to_user()
- get_user()
- put_user()
- kmalloc(GFP_KERNEL)
- down_interruptible() y down()
printk() puede ser llamada en cualquier contexto, suficientemente interesante.
La Follonera Sparc
Alan Cox dice "la deshabilitación/habilitación de una sparc es en la ventana registrada". Andi Kleen dice "cuando restore_flags (restauras las banderas) en una función diferente ensucias todas las ventanas de registros".
Por lo tanto nunca pases el conjunto de palabras de flags por spin_lock_irqsave() y hermanos a otra función (a menos que sea declarada inline). Usualmente nadie hace esto, pero ahora ya estás advertido. Dave Miller nunca puede hacer nada de una forma directa (Puedo decir esto porque tengo fotos de él y de cierto defensor de PowerPC en una posición comprometida).
Cronómetros de Carreras: Un Pasatiempo del Núcleo
Los cronómetros pueden producir sus propios problemas con las carreras. Considera una colección de objeros (listas, hash, etc) donde cada objeto tiene un cronómetro que lo va a destruir.
Si quieres destruir la colección entera (digamos en el borrado de un módulo), quizás realices lo siguiente:
/* ESTE CÓDIGO ES MALO MALO MALO MALO: SI HUBIERA ALGO PEOR
USUARÍA NOTACIÓN HÚNGARA */
spin_lock_bh(&list_lock);
while (list) {
struct foo *next = list->next;
del_timer(&list->timer);
kfree(list);
list = next;
}
spin_unlock_bh(&list_lock);
Tarde o temprano, esto rompería en SMP, porque un cronómetro puede acabar antes que spin_lock_bh(), y sólo obtendría el bloqueo después de spin_unlock_bh(), y entonces intentaría liberar el elemento (¡el cual ya ha sido liberado!).
Esto puede ser eliminado comprobando el resultado de del_timer(): si retorna 1, el cronómetro ha sido borrado. Si , significa (en este caso) que está actualmente ejecutándose, por lo tanto lo que podemos hacer es:
retry:
spin_lock_bh(&list_lock);
while (list) {
struct foo *next = list->next;
if (!del_timer(&list->timer)) {
/* Le da al cronómetro una oportunidad para borrarlo */
spin_unlock_bh(&list_lock);
goto retry;
}
kfree(list);
list = next;
}
spin_unlock_bh(&list_lock);
Otro problema común es el borrando de cronómetros que se reinician a ellos mismos (llamando a add_timer() al final de su función cronómetro). Porque este es un caso bastante común que es propenso a carreras, puedes poner una llamada a timer_exit() muy al funal de tu función cronómetro, y usar del_timer_sync() para manejar este caso. Él retorna el número de veces que el cronómetro tuvo que ser borrado antes de que finalmente lo paráramos añadiéndolo otra vez.
Valora este capítulo:
Autor y licencia de 'Guía Informal al Bloqueo - Técnicas Comunes'
|
Opiniona sobre 'Guía Informal al Bloqueo - Técnicas Comunes' (0)
Tu nombre debe tener tres caracteres como mínimo.
Es necesario que te des de alta con una cuenta de correo válida.
Es necesario que te des de alta con una cuenta de correo válida.
El contenido del título de tu opinión debe tener tres caracteres como mínimo.
Es obligatorio que selecciones una valoración del recurso.
El contenido del comentario de tu opinión debe tener tres caracteres como mínimo.
Opina sobre este tutorial |
Wikis relacionados con 'Guía Informal al Bloqueo - Técnicas Comunes'
Es una lista de frases que contiene las más comunes y utilizadas. This is a...
Más »
Es una lista de frases que contiene las más comunes y utilizadas.
És una llista de...
Más »
Es una lista de frases que contiene las más comunes y utilizadas. This is a...
Más »
Es una lista de frases que contiene las más comunes y utilizadas. This is a...
Más »
Es una lista de frases que contiene las más comunes y utilizadas.
This is a...
Más »

