Comprensión profunda del mecanismo RCU en el kernel de Linux

RCU (Read-Copy Update) es un mecanismo de sincronización importante en Linux. Como su nombre lo indica, es "leer, copiar y actualizar", y el punto contundente es "leer a voluntad, pero al actualizar los datos, primero debe hacer una copia, completar la modificación en la copia y luego reemplazar la antigua todos los datos a la vez ". Este es un mecanismo de sincronización implementado por el kernel de Linux para "leer más y escribir menos" datos compartidos.

A diferencia de otros mecanismos de sincronización, permite que varios lectores accedan a datos compartidos al mismo tiempo, y el rendimiento del lector no se verá afectado ("leer a voluntad"), y no hay necesidad de un mecanismo de sincronización entre lectores y escritores (pero necesita "copiar de nuevo después de copiar"). Escribir "), pero si hay varios escritores, cuando el escritor sobrescribe la" copia "actualizada a los datos originales, el escritor y el escritor deben utilizar otros mecanismos de sincronización para garantizar la sincronización .

Un escenario de aplicación típico de RCU es una lista vinculada. Se proporciona un archivo de encabezado (include / linux / rculist.h) en el kernel de Linux, que proporciona una interfaz para agregar, eliminar, verificar y modificar la lista vinculada mediante el mecanismo RCU. Este artículo usará un ejemplo para usar la interfaz proporcionada por rculist.h para agregar, eliminar, verificar y modificar la lista vinculada para describir el principio de RCU e introducir las API relevantes en el kernel de Linux (basado en el código fuente de Linux v3 .4.0).

Agregar elemento de lista

El código fuente para agregar elementos a la lista vinculada usando RCU en el kernel de Linux es el siguiente:

#define list_next_rcu(list)     (*((struct list_head __rcu **)(&(list)->next)))

static inline void __list_add_rcu(struct list_head *new,
                struct list_head *prev, struct list_head *next)
{
        new->next = next;
        new->prev = prev;
        rcu_assign_pointer(list_next_rcu(prev), new);
        next->prev = new;
}

El rcu en la función list_next_rcu () es una opción de compilación utilizada por la herramienta de análisis de código Sparse. Se estipula que el puntero con la etiqueta rcu no se puede usar directamente, y rcu_dereference () debe usarse para devolver un puntero protegido por RCU antes puede ser usado. El conocimiento relevante de la interfaz rcu_dereference () se presentará más adelante, esta sección se enfoca en la interfaz rcu_assign_pointer (). Primero mire el código fuente de rcu_assign_pointer ():

#define __rcu_assign_pointer(p, v, space) \
    ({ \
        smp_wmb(); \
        (p) = (typeof(*v) __force space *)(v); \
    })

El efecto final del código anterior es asignar el valor de v a p. El punto clave es la barrera de memoria en la línea 3. ¿Qué es una barrera de memoria (barrera de memoria)? Cuando la CPU utiliza tecnología de canalización para ejecutar instrucciones, solo garantiza el orden de ejecución de las instrucciones con dependencias de memoria, como p = v; a = * p;, porque la memoria apuntada por el puntero p a la que accede la segunda instrucción depende de la primera instrucción, por lo que la CPU se asegurará de que la primera instrucción se ejecute antes de que se ejecute la segunda instrucción. Pero para instrucciones sin dependencia de memoria, como la interfaz __list_add_rcu () anterior, si la línea 8 se escribe como anterior-> siguiente = nuevo;, debido a que esta operación de asignación no implica el acceso a la memoria apuntada por el nuevo puntero, es considerado No confíe en la asignación de new-> next y new-> prev en las líneas 6,7, la CPU puede ejecutar prev-> next = new; y luego ejecutar new-> prev = prev ;, lo que causará que The new El puntero (es decir, el elemento de la lista vinculada recién agregado) se agrega a la lista vinculada antes de que se complete la inicialización. Si en este momento un lector atraviesa y accede al nuevo elemento de la lista vinculada (porque una característica importante de RCU es que se puede leer a voluntad Operación), se accederá a un elemento de la lista enlazada que no se ha inicializado. Este problema se puede resolver estableciendo una barrera de memoria, que asegura que las instrucciones antes de la barrera de memoria se ejecutarán antes que las instrucciones después de la barrera de memoria. Esto asegura que los elementos agregados a la lista vinculada deben haberse inicializado.

Como recordatorio final, debe tenerse en cuenta aquí que si puede haber varios subprocesos realizando la operación de agregar un elemento de lista vinculado al mismo tiempo, la operación de agregar un elemento de lista vinculado debe protegerse mediante otros mecanismos de sincronización (como spin_lock, etc.).

Materiales de video de aprendizaje relacionados con el desarrollo del kernel de Linux, haga clic en: materiales de aprendizaje para obtener

 

Elemento de la lista de acceso

Los patrones de código comunes para acceder a los elementos de la lista enlazada RCU en el kernel de Linux son:

rcu_read_lock();
list_for_each_entry_rcu(pos, head, member) {
    // do something with `pos`
}
rcu_read_unlock();

El rcu_read_lock () y rcu_read_unlock () mencionados aquí son la clave para RCU "leer a voluntad" Su efecto es declarar una sección crítica del lado de lectura. Antes de hablar sobre la sección crítica del final de la lectura, echemos un vistazo a la función de macro list_for_each_entry_rcu que lee los elementos de la lista enlazada. Rastrear hasta el código fuente, obtener un puntero a un elemento de lista vinculado principalmente llama a una función macro llamada rcu_dereference (), y la implementación principal de esta función macro es la siguiente:

#define __rcu_dereference_check(p, c, space) \
    ({ \
        typeof(*p) *_________p1 = (typeof(*p)*__force )ACCESS_ONCE(p); \
        rcu_lockdep_assert(c, "suspicious rcu_dereference_check()" \
                      " usage"); \
        rcu_dereference_sparse(p, space); \
        smp_read_barrier_depends(); \
        ((typeof(*p) __force __kernel *)(_________p1)); \
    })
第 3 行:声明指针 _p1 = p;
第 7 行:smp_read_barrier_depends();
第 8 行:返回 _p1;

Las dos piezas de código anteriores en realidad se pueden considerar como un patrón de este tipo:

rcu_read_lock();
p1 = rcu_dereference(p);
if (p1 != NULL) {
    // do something with p1, such as:
    printk("%d\n", p1->field);
}
rcu_read_unlock();

Según la implementación de rcu_dereference (), el efecto final es asignar un puntero a otro. ¿Qué pasa si rcu_dereference () en la línea 2 anterior se escribe directamente como p1 = p? En la arquitectura general del procesador, no hay ningún problema. Pero en alfa, se dice que la opción de optimización de especulación de valor del compilador "adivina" el valor de p1, y luego reorganiza la instrucción para tomar primero el valor p1-> campo ~ Por lo tanto, en el kernel de Linux, la implementación de smp_read_barrier_depends () Arm, x86 y otras arquitecturas son implementaciones vacías, y se agrega una barrera de memoria a alpha para garantizar que la dirección real de p se obtenga primero y luego se desreferencia. Por lo tanto, la opción de compilación "__rcu" mencionada en la sección anterior "Agregar elementos de listas vinculadas" obliga a verificar si rcu_dereference () se usa para acceder a datos protegidos por RCU, lo que en realidad es para hacer que el código sea más portátil.

Ahora regrese al tema de la sección crítica al final de la lectura. Varias secciones críticas de extremo de lectura no son mutuamente excluyentes, es decir, varios lectores pueden estar en la sección crítica de extremo de lectura al mismo tiempo, pero una vez que se puede hacer referencia a un fragmento de datos de la memoria mediante punteros en la sección crítica de extremo de lectura, la liberación de estos datos del bloque de memoria debe esperar hasta la lectura. Cuando finaliza la sección crítica final, la API del kernel de Linux que espera el final de la sección crítica final de lectura es synchronize_rcu (). La verificación de la sección crítica del extremo de lectura es global. Si algún código en el sistema está en la sección crítica del extremo de lectura, synchronize_rcu () se bloqueará y solo regresará cuando todas las secciones críticas del extremo de lectura estén finalizado. Para comprender este problema de manera intuitiva, proporcione el siguiente ejemplo de código:

/* `p` 指向一块受 RCU 保护的共享数据 */

/* reader */
rcu_read_lock();
p1 = rcu_dereference(p);
if (p1 != NULL) {
    printk("%d\n", p1->field);
}
rcu_read_unlock();

/* free the memory */
p2 = p;
if (p2 != NULL) {
    p = NULL;
    synchronize_rcu();
    kfree(p2);
}

Utilice el siguiente diagrama para mostrar la relación de tiempo entre varios lectores y subprocesos de liberación de memoria:

 

En la figura anterior, el cuadrado de cada lector representa el período de tiempo desde que se obtiene la referencia de p (código de la línea 5) hasta el final de la sección crítica del final de la lectura; t1 representa el tiempo cuando p = NULL; t2 representa el tiempo cuando comienza la llamada sincronizar_rcu (); t3 representa el tiempo devuelto por sincronizar_rcu (). Primero veamos los lectores 1, 2 y 3. Aunque los tiempos de finalización de estos 3 lectores son diferentes, todos obtienen una referencia a la dirección p antes de t1. Synchronize_rcu () se llama en t2. En este momento, la sección crítica de Reader1 ha finalizado, pero Reader2 y Reader 3 todavía están en la sección crítica del final de lectura, por lo que debe esperar hasta que finalice la sección crítica de Reader2 y 3, es decir, después de t3, t3, puede ejecutar kfree (p2) para liberar la memoria. Este período de tiempo en el que synchronize_rcu () está bloqueado tiene un nombre llamado Período de gracia. Y Lector 4, 5 y 6, independientemente de la relación de tiempo con el período de gracia, ya que el tiempo para obtener la referencia es posterior a t1, no se puede obtener la referencia del puntero p, por lo que no entrará en la rama de p1. = NULO.

Eliminar elemento de la lista

Al conocer el período de gracia mencionado anteriormente, es fácil comprender la eliminación de elementos de lista vinculados. Los patrones de código comunes son:

p = seach_the_entry_to_delete();
list_del_rcu(p->list);
synchronize_rcu();
kfree(p);
其中 list_del_rcu() 的源码如下,把某一项移出链表:

/* list.h */
static inline void __list_del(struct list_head * prev, struct list_head * next)
{
    next->prev = prev;
    prev->next = next;
}

/* rculist.h */
static inline void list_del_rcu(struct list_head *entry)
{
    __list_del(entry->prev, entry->next);
    entry->prev = LIST_POISON2;
}

De acuerdo con el ejemplo de "Acceder a los elementos de la lista vinculada" en la sección anterior, si un lector puede obtener el elemento de la lista vinculada que vamos a eliminar de la lista vinculada, debe ingresar al área crítica del final de lectura antes de sincronizar_rcu ( ), y synchronize_rcu () garantizará el final de la lectura. La memoria del elemento de la lista vinculada se liberará cuando finalice la sección crítica, y el elemento de la lista vinculada al que el lector está accediendo no se liberará.

Actualizar elemento de lista

Como se mencionó anteriormente, el mecanismo de actualización de RCU es "Copiar actualización", y la actualización de los elementos de la lista enlazada de RCU también es este mecanismo. El patrón de código típico es:

p = search_the_entry_to_update();
q = kmalloc(sizeof(*p), GFP_KERNEL);
*q = *p;
q->field = new_value;
list_replace_rcu(&p->list, &q->list);
synchronize_rcu();
kfree(p);

La tercera y cuarta líneas son para hacer una copia, completar la actualización de la copia y luego llamar a list_replace_rcu () para reemplazar el nodo antiguo con el nuevo nodo. El código fuente es el siguiente:
las líneas 3 y 4 son para hacer una copia y completar la actualización en la copia, luego llamar a list_replace_rcu () para reemplazar el nodo antiguo con el nuevo nodo y finalmente liberar la memoria del nodo antiguo. El código fuente de list_replace_rcu () es el siguiente:

static inline void list_replace_rcu(struct list_head *old,
                struct list_head *new)
{
    new->next = old->next;
    new->prev = old->prev;
    rcu_assign_pointer(list_next_rcu(new->prev), new);
    new->next->prev = new;
    old->prev = LIST_POISON2;
}

Materiales de video de aprendizaje relacionados con el desarrollo del kernel de Linux, haga clic en: materiales de aprendizaje para obtener

Esquema del estudio sistemático del kernel de Linux, adquisición de mapas mentales

 

Supongo que te gusta

Origin blog.csdn.net/Linuxhus/article/details/114549668
Recomendado
Clasificación