El lado escritor: publicar, synchronize_rcu, call_rcu y listas RCU
El lado caro de RCU: el patrón copy-update con rcu_assign_pointer, esperar el grace period con synchronize_rcu o diferir con call_rcu, y las primitivas de lista RCU que hacen todo esto seguro.
Si el lector es un fantasma, el escritor es un contable meticuloso: copia, publica de forma atómica y luego espera —o difiere la espera— hasta que sea seguro liberar la versión vieja. Aquí está el patrón que da nombre a RCU: Read, Copy, Update.
- Publicar con
rcu_assign_pointery el patrón copy-update completo. synchronize_rcu(bloqueante) frente acall_rcu(asíncrono).- Recorrer y mutar listas RCU sin romper a los lectores.
- Elegir entre esperar el grace period y diferir la liberación.
El patrón copy-update
El nombre lo dice todo. Para modificar una estructura protegida por RCU, el escritor no la edita in situ (eso rompería a los lectores en vuelo). En su lugar: lee el objeto actual (Read), lo copia en uno nuevo (Copy), actualiza la copia (Update), publica la copia y difiere la liberación del viejo.
Un detalle crucial: RCU coordina lectores-contra-escritores, no escritores-contra-escritores. Si hay varios escritores, siguen necesitando exclusión mutua entre ellos, típicamente un spinlock o un mutex.
static DEFINE_SPINLOCK(cfg_lock); /* serializa escritores entre si */
static struct config __rcu *cfg;
static int actualizar(int nuevo_valor)
{
struct config *viejo, *nuevo;
nuevo = kmalloc(sizeof(*nuevo), GFP_KERNEL);
if (!nuevo)
return -ENOMEM;
spin_lock(&cfg_lock);
viejo = rcu_dereference_protected(cfg, lockdep_is_held(&cfg_lock));
*nuevo = *viejo; /* Copy */
nuevo->valor = nuevo_valor; /* Update */
rcu_assign_pointer(cfg, nuevo); /* publica de forma atomica */
spin_unlock(&cfg_lock);
synchronize_rcu(); /* espera a los lectores del viejo */
kfree(viejo); /* seguro: nadie lo mira ya */
return 0;
}
Los lectores en vuelo cuando se ejecutó rcu_assign_pointer siguen usando viejo; los que llegan después ven nuevo. Nunca hay un estado intermedio visible.
synchronize_rcu vs call_rcu
Hay dos formas de esperar el grace period, y elegir bien define el rendimiento.
synchronize_rcu() bloquea al llamante hasta que transcurre un grace period completo —que puede durar milisegundos. Es simple y correcto, pero exige un contexto donde puedas dormir: nada de spinlocks tomados, nada de contexto de interrupción.
call_rcu() no bloquea: registra un callback que se ejecutará tras el próximo grace period y devuelve el control de inmediato. El objeto debe embeber un struct rcu_head. El callback corre más tarde, en softirq. Úsalo cuando no puedes dormir o cuando el throughput importa.
struct nodo {
u32 clave;
int valor;
struct list_head lista;
struct rcu_head rcu; /* necesario para call_rcu/kfree_rcu */
};
static void liberar_nodo(struct rcu_head *head)
{
struct nodo *n = container_of(head, struct nodo, rcu);
kfree(n);
}
/* ...tras desenlazar n bajo el lock de escritura... */
call_rcu(&n->rcu, liberar_nodo); /* difiere la liberacion, no bloquea */
/* Si el callback solo hace kfree, hay un atajo sin escribir funcion: */
kfree_rcu(n, rcu); /* equivalente idiomatico */
Hay más azúcar en esta familia. kvfree_rcu() cubre memoria reservada con kvmalloc; y existe una forma de un solo argumento, kfree_rcu(ptr), que espera el grace period sin necesidad de un struct rcu_head embebido, a costa de poder bloquear brevemente. Como regla: usa synchronize_rcu() cuando actualizas rara vez y puedes permitirte esperar, y call_rcu/kfree_rcu cuando el contexto es atómico o el throughput manda.
Cuando publicas un puntero cuyo destino aún no es visible para ningún lector —típico en la inicialización, o al asignar NULL— no necesitas la barrera de rcu_assign_pointer. RCU_INIT_POINTER() hace la asignación sin barrera y ahorra ese coste. Úsalo solo cuando de verdad no hay lectores concurrentes o el valor es NULL; en cualquier otro caso, rcu_assign_pointer es lo correcto.
Nunca llames a synchronize_rcu() sosteniendo el lock de escritura: alargas la sección crítica milisegundos y estrangulas a los demás escritores. En rutas calientes, prefiere call_rcu/kfree_rcu. Y cuidado con dos peligros: una avalancha de call_rcu puede acumular callbacks más rápido de lo que se drenan (usa rcu_barrier() para forzar el drenado). Y al descargar un módulo que registró callbacks, llama a rcu_barrier() antes de liberar el código: si un callback pendiente ejecuta una función de tu módulo ya descargado, es un crash inmediato.
Listas RCU
Casi nunca proteges un solo puntero: proteges colecciones. linux/rculist.h ofrece variantes RCU de las listas del nivel 10, y su diseño esconde una sutileza preciosa.
- Lado lector:
list_for_each_entry_rcu(), siempre dentro dercu_read_lock(). - Insertar:
list_add_rcu(),list_add_tail_rcu()— publican el nodo conrcu_assign_pointerpor debajo. - Borrar:
list_del_rcu()— desenlaza para que ningún lector nuevo alcance el nodo, pero conserva su punteronextválido. - Reemplazar:
list_replace_rcu().
La sutileza: list_del_rcu() no envenena el puntero next como hace list_del(). ¿Por qué? Porque un lector concurrente puede estar parado sobre ese nodo justo ahora, y necesita seguir next para continuar el recorrido. Envenenarlo lo mandaría a LIST_POISON y provocaría un crash. El nodo sigue apuntando hacia adelante hasta que el grace period garantiza que nadie lo pisa.
static LIST_HEAD(lista);
static DEFINE_SPINLOCK(lista_lock);
/* Lector: sin bloqueo */
rcu_read_lock();
list_for_each_entry_rcu(n, &lista, lista) {
if (n->clave == k)
usar(n->valor);
}
rcu_read_unlock();
/* Escritor: insertar */
spin_lock(&lista_lock);
list_add_rcu(&nuevo->lista, &lista);
spin_unlock(&lista_lock);
/* Escritor: borrar y diferir liberacion */
spin_lock(&lista_lock);
list_del_rcu(&n->lista);
spin_unlock(&lista_lock);
kfree_rcu(n, rcu);
flowchart LR W[Escritor copia y actualiza] --> P[rcu_assign_pointer publica el nuevo] P --> O[Lectores en vuelo siguen en la version vieja] P --> N[Lectores nuevos ven la version nueva] O --> G[Grace period] G --> F[call_rcu o kfree libera la version vieja] style P fill:#f9e2af,color:#11111b style G fill:#cba6f7,color:#11111b style F fill:#89b4fa,color:#11111b
La modificación in situ de una estructura compartida es, bajo concurrencia, una fuente inagotable de carreras: un lector puede observar el punto medio entre dos escrituras y ver un estado que nunca fue válido. RCU disuelve el problema cambiando la granularidad de la atomicidad. En lugar de intentar que N campos se actualicen “a la vez” —imposible sin un candado que serialice a los lectores—, RCU actualiza un solo puntero, y esa escritura de puntero es naturalmente atómica en el hardware. El truco es que ese puntero representa la versión entera del objeto: al cambiarlo, el mundo salta de la versión vieja a la nueva de golpe, sin estados intermedios observables. Copy-update es, en el fondo, control de versiones aplicado a la memoria: nunca editas la copia que otros están leyendo; publicas una revisión nueva y dejas que las lecturas viejas terminen contra la revisión vieja antes de reciclarla. Esta es la idea que permite mutar el enrutado, las reglas de firewall o el dcache de un kernel en producción sin detener ni un solo lector.
- Implementa
actualizarcon el patrón copy-update completo, incluyendo el spinlock que serializa escritores. - Convierte una liberación con
synchronize_rcu()+kfree()akfree_rcu()y explica cuándo prefieres cada una. - Explica por qué
list_del_rcuno envenenanextmientraslist_delsí lo hace. - Añade
rcu_barrier()en la salida de un módulo que usacall_rcuy justifica por qué evita un crash. - Diagrama la línea temporal copy-update marcando dónde exactamente ocurre la atomicidad.