wandres.dev
RCU · Read-Copy-Update

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.

⏱ 14 min

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.

🎯 Al terminar esta lección sabrás
  • Qué hace (y qué no hace) rcu_read_lock en cada configuración.
  • Usar rcu_dereference y 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 esencialmente preempt_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:

  1. Un READ_ONCE() sobre el puntero, para que el compilador no lo recargue ni lo invente.
  2. Impone orden de dependencia: la CPU no puede especular la carga de p->campo antes de la carga de p. Es la contraparte del smp_store_release que puso rcu_assign_pointer.
  3. 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);
📝
rcu_dereference vive en muchos disfraces

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().

💡
Anota con __rcu y ejecuta sparse

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).

⚠️
Expropiación no es lo mismo que dormir

Con CONFIG_PREEMPT_RCU=y una sección lectora 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 lector fantasma

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.

⚔️ Disciplina de lector
  1. Explica qué compila rcu_read_lock() con y sin CONFIG_PREEMPT_RCU, y por qué ninguna toca memoria compartida.
  2. Enumera cuatro operaciones prohibidas dentro de una sección RCU clásica y por qué cada una viola la regla.
  3. Distingue expropiación involuntaria (permitida bajo PREEMPT_RCU) de bloqueo voluntario (prohibido).
  4. Corrige un fragmento que usa el puntero tras rcu_read_unlock() con el patrón kref_get_unless_zero.
  5. Activa CONFIG_PROVE_RCU y provoca a propósito una violación para ver el aviso de lockdep.