El lado lector: rcu_read_lock, rcu_dereference y sus reglas
El lado barato de RCU: qué hacen de verdad rcu_read_lock y rcu_read_unlock, cómo rcu_dereference carga un puntero con seguridad, y la regla inviolable de no dormir dentro de una sección RCU clásica.
El lado lector es donde RCU cobra su fama: casi gratis. Pero “casi gratis” viene con reglas estrictas que, si las rompes, producen los bugs más difíciles de diagnosticar del kernel. Aquí desmontamos rcu_read_lock, rcu_dereference y la disciplina que exigen.
- Qué hace (y qué no hace)
rcu_read_locken cada configuración. - Usar
rcu_dereferencey su barrera de dependencia. - La regla inviolable: no dormir en una sección RCU clásica.
- Anidamiento, contexto y errores sutiles del lado lector.
rcu_read_lock: casi gratis, de verdad
Lo que hace rcu_read_lock() depende de la configuración, pero nunca toca memoria compartida en escritura:
- Con
CONFIG_PREEMPT_RCU=n(kernels de servidor sin expropiación forzosa),rcu_read_lock()es esencialmentepreempt_disable(), y en algunas rutas ni eso: una simple barrera de compilador. Cero operaciones atómicas. - Con
CONFIG_PREEMPT_RCU=y, incrementa un contador por-tarea,current->rcu_read_lock_nesting. Sigue sin ser una línea de caché compartida: cada tarea toca la suya.
En ambos casos: ninguna coordinación entre núcleos. Ese es todo el punto, y la razón por la que el lado lector escala de forma perfectamente lineal.
Su pareja rcu_read_unlock() es igual de barata: decrementa el contador o levanta la barrera. Bajo PREEMPT_RCU, si te expropiaron mientras leías y quedó pendiente un estado quiescente, es aquí donde se reporta; en el caso común, coste cero.
struct config *p;
rcu_read_lock();
p = rcu_dereference(cfg);
if (p)
consumir(p->valor);
rcu_read_unlock();
/* A partir de aqui, 'p' ya NO es valido: podria liberarse */
El ciclo de vida completo del lector cabe en cuatro pasos, y el puntero muere con la sección:
flowchart LR A[rcu_read_lock] --> B[rcu_dereference carga el puntero] B --> C[Usar el objeto solo dentro de la seccion] C --> D[rcu_read_unlock] D --> E[El puntero deja de ser valido] style A fill:#a6e3a1,color:#11111b style D fill:#a6e3a1,color:#11111b style E fill:#f38ba8,color:#11111b
rcu_dereference: cargar el puntero con seguridad
rcu_dereference() es la suscripción del modelo publicar/suscribir. Hace tres cosas:
- Un
READ_ONCE()sobre el puntero, para que el compilador no lo recargue ni lo invente. - Impone orden de dependencia: la CPU no puede especular la carga de
p->campoantes de la carga dep. Es la contraparte delsmp_store_releaseque pusorcu_assign_pointer. - Comprueba, con sparse y el anotado
__rcu, que respetas el protocolo, y con lockdep que estás dentro de una sección RCU.
El valor devuelto solo es válido dentro de la misma sección crítica. Sacarlo fuera de rcu_read_unlock() y usarlo es un use-after-free en potencia.
struct config __rcu *cfg; /* anotado para sparse */
/* Dentro de rcu_read_lock(): */
p = rcu_dereference(cfg);
/* Cuando tienes el lock de escritura, no hace falta la barrera de lectura: */
p = rcu_dereference_protected(cfg, lockdep_is_held(&cfg_lock));
/* Variante que documenta bajo que condicion es seguro: */
p = rcu_dereference_check(cfg, lockdep_is_held(&cfg_lock));
/* En codigo de inicializacion sin lectores concurrentes: */
p = rcu_dereference_raw(cfg);
Rara vez llamarás a rcu_dereference a mano: list_for_each_entry_rcu, hlist_for_each_entry_rcu y compañía (nivel 17.4) lo invocan por ti en cada paso del recorrido. La regla no cambia: todos exigen estar dentro de rcu_read_lock(), y su resultado solo vale hasta rcu_read_unlock().
Marca los punteros gestionados por RCU con __rcu y pasa make C=1 (sparse). El analizador te obliga a usar rcu_dereference/rcu_assign_pointer en vez de accesos crudos, y CONFIG_PROVE_RCU (lockdep) verifica en runtime que dereferencias siempre dentro de una sección. La mayoría de los bugs de RCU se cazan así, gratis, antes de llegar a producción.
La regla de oro: no dormir
Una sección crítica de lectura clásica no puede bloquearse ni dormir. La razón es exacta y hermosa: el fin de un lector se detecta por el estado quiescente, y un cambio de contexto es un estado quiescente. Si durmieras dentro de la sección, harías schedule(), la CPU registraría un punto quiescente, y el grace period podría terminar mientras todavía sostienes una referencia. El escritor liberaría el objeto bajo tus pies. Use-after-free.
Prohibido, por tanto, dentro de rcu_read_lock() clásico:
rcu_read_lock();
p = rcu_dereference(cfg);
mutex_lock(&m); /* PROHIBIDO: puede dormir */
kmalloc(n, GFP_KERNEL); /* PROHIBIDO: GFP_KERNEL puede dormir */
copy_to_user(u, p, n); /* PROHIBIDO: puede provocar fallo de pagina */
schedule(); /* PROHIBIDO: cede la CPU */
rcu_read_unlock();
Si necesitas reservar memoria dentro de la sección, usa GFP_ATOMIC. Si necesitas dormir, no es trabajo para la RCU clásica: es para SRCU (nivel 17.5).
Con CONFIG_PREEMPT_RCU=y una sección lectora sí puede ser expropiada: el planificador puede quitarte la CPU involuntariamente y RCU lo rastrea para no confundirlo con un estado quiescente. Pero eso no te autoriza a dormir voluntariamente. Expropiación involuntaria: permitida y rastreada. Bloqueo voluntario (mutex_lock, schedule, GFP_KERNEL): prohibido. Confundir ambas cosas es un error clásico.
Anidamiento, contexto y errores sutiles
rcu_read_lock() se puede anidar: el nesting se cuenta, y solo el rcu_read_unlock() más externo cierra la sección. Puedes leer en contexto de proceso y en contexto de softirq. Históricamente existían sabores separados con sus propias primitivas de lectura, que en Linux 7.x están consolidados en un único grace period aunque las APIs sigan existiendo por compatibilidad y semántica:
rcu_read_lock
El sabor general. Protege contra el grace period normal; el 90% de los lectores del kernel.
rcu_read_lock_bh
Además desactiva las bottom halves. Pensado para rutas de red donde los softirqs son el escritor.
rcu_read_lock_sched
Desactiva la expropiación. Equivale a marcar una región donde el planificador no te quita la CPU.
Para depurar, rcu_read_lock_held() y rcu_read_lock_any_held() te dicen en un aserto si estás dentro de una sección; combinados con CONFIG_PROVE_RCU cazan dereferencias fuera de contexto.
Y hay más red de seguridad: con CONFIG_DEBUG_OBJECTS_RCU_HEAD el kernel detecta un struct rcu_head reutilizado o encolado dos veces antes de que corrompa memoria. El error sutil más común, sin embargo, es escapar el puntero de la sección:
/* MAL: 'p' usado fuera de la seccion */
rcu_read_lock();
p = rcu_dereference(cfg);
rcu_read_unlock();
consumir(p->valor); /* p pudo liberarse: use-after-free */
/* BIEN: consumir dentro, o anclar con un refcount antes de salir */
rcu_read_lock();
p = rcu_dereference(cfg);
if (p && kref_get_unless_zero(&p->ref))
guardar_para_mas_tarde(p); /* ahora 'p' esta anclado por refcount */
rcu_read_unlock();
El lado lector de RCU es la cosa más cercana a la magia que ofrece el kernel: un fragmento de código que accede a estructuras compartidas y mutables, en paralelo con escritores, en miles de núcleos a la vez, sin ejecutar una sola operación atómica, sin escribir una sola línea de caché compartida, sin esperar a nadie —y aun así es correcto. El lector RCU es un fantasma: entra, lee, sale y no deja rastro que otro núcleo tenga que observar. Todo el peso de la corrección recae en dos barreras casi invisibles (rcu_dereference al entrar, la disciplina de no dormir mientras estás dentro) y en la promesa del escritor de esperar un grace period antes de liberar. Esta es la razón última por la que las rutas más calientes del kernel —el enrutado de cada paquete, la resolución de cada ruta de fichero en el dcache— pueden correr a la velocidad del hardware. Domina estas reglas y habrás entendido por qué RCU no tiene rival en cargas read-mostly.
- Explica qué compila
rcu_read_lock()con y sinCONFIG_PREEMPT_RCU, y por qué ninguna toca memoria compartida. - Enumera cuatro operaciones prohibidas dentro de una sección RCU clásica y por qué cada una viola la regla.
- Distingue expropiación involuntaria (permitida bajo PREEMPT_RCU) de bloqueo voluntario (prohibido).
- Corrige un fragmento que usa el puntero tras
rcu_read_unlock()con el patrónkref_get_unless_zero. - Activa
CONFIG_PROVE_RCUy provoca a propósito una violación para ver el aviso de lockdep.