Antecedentes
Read the fucking source code!
--Por Lu XunA picture is worth a thousand words.
--Por Gorki
Descripción:
- Versión de Kernel: 4.14
- Procesador ARM64, Contex-A53, doble núcleo
- Usar herramientas: Source Insight 3.5, Visio
1. Descripción general
RCU, Read-Copy-Update
, Es un mecanismo de sincronización en el kernel de Linux.
RCU
A menudo se describe como un reemplazo para los bloqueos de lectura-escritura. Su característica es que el lector no necesita sincronizarse directamente con el escritor, y el lector y el escritor también pueden ejecutar simultáneamente. RCU
El objetivo es minimizar la sobrecarga en el lado del lector, por lo que también se usa comúnmente en escenarios que requieren un alto rendimiento del lector.
-
Ventajas:
- El lado del lector tiene muy poca sobrecarga, no es necesario adquirir ningún bloqueo, no es necesario ejecutar instrucciones atómicas o barreras de memoria;
- No hay problema de punto muerto;
- No hay problema de inversión prioritaria;
- No hay peligro de pérdida de memoria;
- Muy buen retraso en tiempo real;
-
Desventajas
- La sobrecarga de sincronización del escritor es relativamente grande, y los escritores deben ser mutuamente excluyentes.
- Es más complicado de usar que otros mecanismos de sincronización;
Tomemos una foto para describir la operación general:
- Múltiples lectores pueden acceder a recursos críticos al mismo tiempo y usarlos simultáneamente
rcu_read_lock/rcu_read_unlock
para calibrar secciones críticas; - El escritor (
updater
) copia una copia como base para la modificación al actualizar recursos críticos. Cuando todos los lectores abandonan la sección crítica, señalan el puntero al recurso crítico antiguo a la copia actualizada y reciclan el recurso antiguo; - Solo se muestra un escritor en la figura. Cuando hay varios escritores, se requiere exclusión mutua entre escritores;
La descripción anterior es relativamente simple, y la implementación de RCU es muy complicada. Este artículo primero da una primera impresión de RCU y analiza el ejemplo con la interfaz. Los artículos siguientes profundizarán en el principio de implementación subyacente capa por capa. Vamos!
2. Conceptos básicos de RCU
2.1 Elementos básicos de la UCR
RCU
La idea básica es dividir la Update
operación de actualización en dos partes: 1) Removal
eliminar; 2) Reclamation
reciclar.
Para decirlo sin rodeos, el recurso crítico es leído por varios lectores. Cuando el escritor actualiza la copia después de la modificación, el primer paso debe eliminar los datos de recursos críticos antiguos (los puntos de puntero de modificación), y el segundo paso requiere Reciclar datos antiguos (por ejemplo kfree
).
Por lo tanto, se divide funcionalmente en los siguientes tres elementos básicos: Reader/Updater/Reclaimer
La interacción entre los tres es la siguiente:
-
Lector
- Utilice
rcu_read_lock
yrcu_read_unlock
para definir el área crítica del lector. Al acceder a losRCU
datos protegidos, siempre debe acceder a ellos en el área crítica; - Antes de acceder a los datos protegidos, debe usar
rcu_dereference
para obtener elRCU-protected
puntero; - Cuando se usa no preventivo,
RCU
norcu_read_lock/rcu_read_unlock
puede usar el código que puede dormir;
- Utilice
-
Updater
- Cuando varios Actualizadores actualizan datos, necesitan usar un mecanismo de exclusión mutua para protección;
- Updater se utiliza
rcu_assign_pointer
para eliminar el puntero antiguo para apuntar a los recursos críticos actualizados; - Updater usa
synchronize_rcu
ocall_rcu
para comenzarReclaimer
, para reciclar los viejos recursos críticos, lo quesynchronize_rcu
significa esperar sincrónicamente el reciclaje, lo quecall_rcu
significa reciclaje asincrónico;
-
Reclamador
- Reclaimer recicla viejos recursos críticos;
- Para garantizar que ningún lector acceda a los recursos críticos que se recuperarán, Reclaimer debe esperar a que todos los lectores salgan de la sección crítica. Este tiempo de espera se llama período de gracia (
Grace Period
);
2.2 Tres mecanismos básicos de UCR
Se utiliza para proporcionar las funciones descritas anteriormente, RCU
basadas en tres mecanismos.
2.2.1 Publish-Subscribe Mechanism
¿Cuál es el concepto del mecanismo de suscripción? Ven a la imagen:
Updater
Y elReader
comoPublisher
ySubsriber
las relaciones;Updater
Después de actualizar el contenido, llame a la interfaz para publicar yReader
llame a la interfaz para leer el contenido publicado;
Entonces, ¿qué se debe hacer para garantizar este mecanismo de suscripción? Veamos un pseudocódigo:
/* Definiton of global structure */
1 struct foo {
2 int a;
3 int b;
4 int c;
5 };
6 struct foo *gp = NULL;
7
8 /* . . . */
9 /* =========Updater======== */
10 p = kmalloc(sizeof(*p), GFP_KERNEL);
11 p->a = 1;
12 p->b = 2;
13 p->c = 3;
14 gp = p;
15
16 /* =========Reader======== */
17 p = gp;
18 if (p != NULL) {
19 do_something_with(p->a, p->b, p->c);
20 }
A primera vista, parece que el problema no es demasiado grande: el actualizador realiza la asignación y la actualización, y el lector realiza la lectura y otros procesos. Sin embargo, debido a los problemas de compilación y ejecución fuera de orden, el orden de ejecución del código anterior puede no ser necesariamente el orden del código. Por ejemplo, en algunas arquitecturas ( DEC Alpha
), la parte de operación del lector puede operarse antes de asignar p do_something_with()
.
Para resolver este problema, Linux ofrece rcu_assign_pointer/rcu_dereference
macro para asegurar el orden de ejecución, núcleo de Linux se basa en rcu_assign_pointer/rcu_dereference
las macros a los paquetes de más alto nivel, como por ejemplo list
, hlist
por lo tanto, hay tres protegida kernel escena UCR: 1) apuntador; 2) la lista de la lista ; 3) hlist lista enlazada hash.
Para estos tres escenarios, la Publish-Subscribe
interfaz es la siguiente:
2.2.2 Wait For Pre-Existing RCU Readers to Complete
Reclaimer necesita reciclar los viejos recursos críticos, entonces surge la pregunta, ¿cuándo sucederá? Por lo tanto, es RCU
necesario proporcionar un mecanismo para garantizar que se hayan completado todos los lectores de RCU anteriores, es decir rcu_read_lock/rcu_read_unlock
, que solo se puedan reciclar después de salir de la sección crítica calibrada.
- Los lectores y el actualizador de la figura se ejecutan simultáneamente;
- Cuando el Actualizador realiza la
Removal
operación, se llamasynchronize_rcu
, marca el final de la actualización y comienza a entrar en la fase de recuperación; - Después de la
synchronize_rcu
llamada, puede haber nuevos lectores para leer recursos críticos (contenido actualizado) en este momento, pero los lectores queGrace Period
soloPre-Existing
están esperando están en la figuraReader-4, Reader-5
. Mientras estos lectores de la UCR que existían antes salieran de la sección crítica, significa el final del período de gracia, por lo que el proceso de reciclaje se lleva a cabo; synchronize_rcu
No es que el últimoPre-Existing
lector de RCU se vaya inmediatamente después de abandonar la sección crítica, puede tener un retraso de programación;
2.2.3 Maintain Multiple Versions of Recently Updated Objects
Se 2.2.2节
puede ver que después de las actualizaciones del Actualizador, antes de que el Reclamador se recicle, habrá dos versiones nuevas y antiguas de los recursos críticos. Solo después de synchronize_rcu
regresar, el Reclamador recicla los viejos recursos críticos y queda la última versión. Obviamente, cuando hay múltiples Actualizadores, habrá versiones de recursos más críticas.
Tomemos una foto, tomando punteros y listas vinculadas como ejemplos:
- El
synchronize_rcu
inicio de la llamada es un punto crítico, que mantiene diferentes versiones de recursos críticos; - Después de que Reclaimer reclama la versión anterior de los recursos, finalmente se unifica;
3. Análisis de ejemplo de RCU
Es hora de una ola fucking sample code
.
- La lógica general del código:
- Construya cuatro subprocesos del núcleo, dos subprocesos del núcleo para probar las operaciones de protección de la RCU del puntero y dos subprocesos del núcleo para probar las operaciones de protección de la RCU de la lista vinculada;
- En el momento del reciclaje, se utilizaron dos mecanismos de
synchronize_rcu
reciclaje sincrónico ycall_rcu
reciclado asincrónico; - Para simplificar el código, se ha omitido el juicio básico de tolerancia a fallas;
- El mecanismo de múltiples Actualizadores no se considera, por lo tanto, también se omite la operación mutuamente excluyente entre Actualizadores;
#include <linux/module.h>
#include <linux/init.h>
#include <linux/slab.h>
#include <linux/kthread.h>
#include <linux/rcupdate.h>
#include <linux/delay.h>
struct foo {
int a;
int b;
int c;
struct rcu_head rcu;
struct list_head list;
};
static struct foo *g_pfoo = NULL;
LIST_HEAD(g_rcu_list);
struct task_struct *rcu_reader_t;
struct task_struct *rcu_updater_t;
struct task_struct *rcu_reader_list_t;
struct task_struct *rcu_updater_list_t;
/* 指针的Reader操作 */
static int rcu_reader(void *data)
{
struct foo *p = NULL;
int cnt = 100;
while (cnt--) {
msleep(100);
rcu_read_lock();
p = rcu_dereference(g_pfoo);
pr_info("%s: a = %d, b = %d, c = %d\n",
__func__, p->a, p->b, p->c);
rcu_read_unlock();
}
return 0;
}
/* 回收处理操作 */
static void rcu_reclaimer(struct rcu_head *rh)
{
struct foo *p = container_of(rh, struct foo, rcu);
pr_info("%s: a = %d, b = %d, c = %d\n",
__func__, p->a, p->b, p->c);
kfree(p);
}
/* 指针的Updater操作 */
static int rcu_updater(void *data)
{
int value = 1;
int cnt = 100;
while (cnt--) {
struct foo *old;
struct foo *new = (struct foo *)kzalloc(sizeof(struct foo), GFP_KERNEL);
msleep(200);
old = g_pfoo;
*new = *g_pfoo;
new->a = value;
new->b = value + 1;
new->c = value + 2;
rcu_assign_pointer(g_pfoo, new);
pr_info("%s: a = %d, b = %d, c = %d\n",
__func__, new->a, new->b, new->c);
call_rcu(&old->rcu, rcu_reclaimer);
value++;
}
return 0;
}
/* 链表的Reader操作 */
static int rcu_reader_list(void *data)
{
struct foo *p = NULL;
int cnt = 100;
while (cnt--) {
msleep(100);
rcu_read_lock();
list_for_each_entry_rcu(p, &g_rcu_list, list) {
pr_info("%s: a = %d, b = %d, c = %d\n",
__func__, p->a, p->b, p->c);
}
rcu_read_unlock();
}
return 0;
}
/* 链表的Updater操作 */
static int rcu_updater_list(void *data)
{
int cnt = 100;
int value = 1000;
while (cnt--) {
msleep(100);
struct foo *p = list_first_or_null_rcu(&g_rcu_list, struct foo, list);
struct foo *q = (struct foo *)kzalloc(sizeof(struct foo), GFP_KERNEL);
*q = *p;
q->a = value;
q->b = value + 1;
q->c = value + 2;
list_replace_rcu(&p->list, &q->list);
pr_info("%s: a = %d, b = %d, c = %d\n",
__func__, q->a, q->b, q->c);
synchronize_rcu();
kfree(p);
value++;
}
return 0;
}
/* module初始化 */
static int rcu_test_init(void)
{
struct foo *p;
rcu_reader_t = kthread_run(rcu_reader, NULL, "rcu_reader");
rcu_updater_t = kthread_run(rcu_updater, NULL, "rcu_updater");
rcu_reader_list_t = kthread_run(rcu_reader_list, NULL, "rcu_reader_list");
rcu_updater_list_t = kthread_run(rcu_updater_list, NULL, "rcu_updater_list");
g_pfoo = (struct foo *)kzalloc(sizeof(struct foo), GFP_KERNEL);
p = (struct foo *)kzalloc(sizeof(struct foo), GFP_KERNEL);
list_add_rcu(&p->list, &g_rcu_list);
return 0;
}
/* module清理工作 */
static void rcu_test_exit(void)
{
kfree(g_pfoo);
kfree(list_first_or_null_rcu(&g_rcu_list, struct foo, list));
kthread_stop(rcu_reader_t);
kthread_stop(rcu_updater_t);
kthread_stop(rcu_reader_list_t);
kthread_stop(rcu_updater_list_t);
}
module_init(rcu_test_init);
module_exit(rcu_test_exit);
MODULE_AUTHOR("Loyen");
MODULE_LICENSE("GPL");
Para demostrar que no hay engaño, se publica el registro de salida que se ejecuta en la placa de desarrollo, como se muestra a continuación:
4. Introducción a la API
4.1 API principal
Las siguientes interfaces no pueden ser más básicas.
a. rcu_read_lock() //标记读者临界区的开始
b. rcu_read_unlock() //标记读者临界区的结束
c. synchronize_rcu() / call_rcu() //等待Grace period结束后进行资源回收
d. rcu_assign_pointer() //Updater使用这个宏对受RCU保护的指针进行赋值
e. rcu_dereference() //Reader使用这个宏来获取受RCU保护的指针
4.2 Otras API relacionadas
Según la API principal, se han ampliado otras API relacionadas, de la siguiente manera, sin más detalles:
RCU list traversal::
list_entry_rcu
list_entry_lockless
list_first_entry_rcu
list_next_rcu
list_for_each_entry_rcu
list_for_each_entry_continue_rcu
list_for_each_entry_from_rcu
list_first_or_null_rcu
list_next_or_null_rcu
hlist_first_rcu
hlist_next_rcu
hlist_pprev_rcu
hlist_for_each_entry_rcu
hlist_for_each_entry_rcu_bh
hlist_for_each_entry_from_rcu
hlist_for_each_entry_continue_rcu
hlist_for_each_entry_continue_rcu_bh
hlist_nulls_first_rcu
hlist_nulls_for_each_entry_rcu
hlist_bl_first_rcu
hlist_bl_for_each_entry_rcu
RCU pointer/list update::
rcu_assign_pointer
list_add_rcu
list_add_tail_rcu
list_del_rcu
list_replace_rcu
hlist_add_behind_rcu
hlist_add_before_rcu
hlist_add_head_rcu
hlist_add_tail_rcu
hlist_del_rcu
hlist_del_init_rcu
hlist_replace_rcu
list_splice_init_rcu
list_splice_tail_init_rcu
hlist_nulls_del_init_rcu
hlist_nulls_del_rcu
hlist_nulls_add_head_rcu
hlist_bl_add_head_rcu
hlist_bl_del_init_rcu
hlist_bl_del_rcu
hlist_bl_set_first_rcu
RCU::
Critical sections Grace period Barrier
rcu_read_lock synchronize_net rcu_barrier
rcu_read_unlock synchronize_rcu
rcu_dereference synchronize_rcu_expedited
rcu_read_lock_held call_rcu
rcu_dereference_check kfree_rcu
rcu_dereference_protected
bh::
Critical sections Grace period Barrier
rcu_read_lock_bh call_rcu rcu_barrier
rcu_read_unlock_bh synchronize_rcu
[local_bh_disable] synchronize_rcu_expedited
[and friends]
rcu_dereference_bh
rcu_dereference_bh_check
rcu_dereference_bh_protected
rcu_read_lock_bh_held
sched::
Critical sections Grace period Barrier
rcu_read_lock_sched call_rcu rcu_barrier
rcu_read_unlock_sched synchronize_rcu
[preempt_disable] synchronize_rcu_expedited
[and friends]
rcu_read_lock_sched_notrace
rcu_read_unlock_sched_notrace
rcu_dereference_sched
rcu_dereference_sched_check
rcu_dereference_sched_protected
rcu_read_lock_sched_held
SRCU::
Critical sections Grace period Barrier
srcu_read_lock call_srcu srcu_barrier
srcu_read_unlock synchronize_srcu
srcu_dereference synchronize_srcu_expedited
srcu_dereference_check
srcu_read_lock_held
SRCU: Initialization/cleanup::
DEFINE_SRCU
DEFINE_STATIC_SRCU
init_srcu_struct
cleanup_srcu_struct
All: lockdep-checked RCU-protected pointer access::
rcu_access_pointer
rcu_dereference_raw
RCU_LOCKDEP_WARN
rcu_sleep_check
RCU_NONIDLE
Bien, enumerar estas API es un poco confuso.
El velo misterioso de RCU se revela inicialmente, y luego será un poco difícil recoger la ropa dentro. Después de todo, el mecanismo de implementación detrás de RCU es realmente difícil. Entonces, la pregunta viene, ¿quieres ser un hombre que ve al rey? Presta atención.
Referencia
Documentación / RCU
¿Qué es RCU, fundamentalmente?
¿Qué es la UCR? Parte 2: Uso de
RCU parte 3: Introducción a API
de RCU para RCU
Bienvenido a prestar atención al número público y seguir compartiendo los artículos del mecanismo central en forma gráfica