Contexte
Read the fucking source code!
--Par Lu XunA picture is worth a thousand words.
--Par Gorky
Description:
- Version du noyau: 4.14
- Processeur ARM64, Contex-A53, double cœur
- Utiliser des outils: Source Insight 3.5, Visio
1. Présentation
RCU, Read-Copy-Update
, Est un mécanisme de synchronisation dans le noyau Linux.
RCU
Il est souvent décrit comme remplaçant les verrous en lecture-écriture. Sa caractéristique est que le lecteur n'a pas besoin de se synchroniser directement avec le graveur, et le lecteur et le graveur peuvent également s'exécuter simultanément. RCU
L'objectif est de minimiser les frais généraux du côté du lecteur, de sorte qu'il est également couramment utilisé dans les scénarios qui nécessitent des performances élevées du lecteur.
-
Avantages:
- Le côté lecteur a très peu de surcharge, pas besoin d'acquérir de verrous, pas besoin d'exécuter des instructions atomiques ou des barrières mémoire;
- Aucun problème de blocage;
- Pas de problème d'inversion de priorité;
- Aucun danger de fuite de mémoire;
- Très bon délai en temps réel;
-
Inconvénients:
- Les frais généraux de synchronisation de l'auteur sont relativement importants et les auteurs doivent s'exclure mutuellement.
- Il est plus compliqué à utiliser que les autres mécanismes de synchronisation;
Prenons une photo pour décrire le fonctionnement général:
- Plusieurs lecteurs peuvent accéder simultanément aux ressources critiques et les utiliser simultanément
rcu_read_lock/rcu_read_unlock
pour calibrer les sections critiques; - L'auteur (
updater
) copie une copie comme base de modification lors de la mise à jour des ressources critiques. Lorsque tous les lecteurs quittent la section critique, ils pointent le pointeur vers l'ancienne ressource critique vers la copie mise à jour et recyclent l'ancienne ressource; - Un seul écrivain est indiqué sur la figure. Lorsqu'il y a plusieurs écrivains, l'exclusion mutuelle est requise entre les écrivains;
La description ci-dessus est relativement simple et la mise en œuvre de RCU est très compliquée. Cet article donne d'abord une première impression de RCU et analyse l'exemple avec l'interface.Les articles suivants approfondiront le principe d'implémentation sous-jacent couche par couche. C'est parti!
2. Bases RCU
2.1 Éléments de base du RCU
RCU
L'idée de base est de diviser l' Update
opération de mise à jour en deux parties: 1) Removal
supprimer; 2) Reclamation
recycler.
Pour le dire franchement, la ressource critique est lue par plusieurs lecteurs. Lorsque le rédacteur met à jour la copie après modification, la première étape doit supprimer les anciennes données de ressource critique (les points du pointeur de modification), et la deuxième étape Recyclez les anciennes données (par exemple kfree
).
Par conséquent, il est fonctionnellement divisé en trois éléments de base suivants: Reader/Updater/Reclaimer
L'interaction entre les trois est la suivante:
-
Lecteur
- Utilisez
rcu_read_lock
etrcu_read_unlock
pour définir la zone critique du lecteur LorsqueRCU
vous accédez aux données protégées, vous devez toujours y accéder dans la zone critique; - Avant d'accéder aux données protégées, vous devez utiliser
rcu_dereference
pour obtenir leRCU-protected
pointeur; - Lorsque vous utilisez non préemptif,
RCU
vousrcu_read_lock/rcu_read_unlock
ne pouvez pas utiliser le code qui peut dormir;
- Utilisez
-
Updater
- Lorsque plusieurs Updaters mettent à jour les données, ils doivent utiliser un mécanisme d'exclusion mutuelle pour la protection;
- Updater est utilisé
rcu_assign_pointer
pour supprimer l'ancien pointeur pour pointer vers les ressources critiques mises à jour; - Updater utilise
synchronize_rcu
oucall_rcu
pour démarrerReclaimer
, pour recycler les anciennes ressources critiques, ce quisynchronize_rcu
signifie attendre de manière synchrone le recyclage, ce quicall_rcu
signifie le recyclage asynchrone;
-
Reclaimer
- Reclaimer recycle les anciennes ressources critiques;
- Afin de garantir qu'aucun lecteur n'accède aux ressources critiques à récupérer, Reclaimer doit attendre que tous les lecteurs quittent la section critique. Ce temps d'attente est appelé période de grâce (
Grace Period
);
2.2 Trois mécanismes de base de RCU
Utilisé pour fournir les fonctions décrites ci-dessus, RCU
basées sur trois mécanismes.
2.2.1 Publish-Subscribe Mechanism
Quel est le concept du mécanisme d'abonnement, venez à l'image:
Updater
EtReader
commePublisher
et lesSubsriber
relations;Updater
Après avoir mis à jour le contenu, appelez l'interface pour publier etReader
appelez l'interface pour lire le contenu publié;
Alors, que faut-il faire pour assurer ce mécanisme d'abonnement? Regardons un pseudo code:
/* 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 }
À première vue, il semble que le problème ne soit pas trop important. Le programme de mise à jour effectue l'affectation et la mise à jour, et le lecteur effectue la lecture et d'autres traitements. Cependant, en raison des problèmes de compilation et d'exécution dans le désordre, l'ordre d'exécution du code ci-dessus n'est pas nécessairement l'ordre du code. Par exemple, dans certaines architectures ( DEC Alpha
), la partie opération du lecteur peut être exploitée avant l'affectation de p do_something_with()
.
Pour résoudre ce problème, Linux offre rcu_assign_pointer/rcu_dereference
macro pour assurer l'ordre d'exécution, le noyau Linux est également basée sur des rcu_assign_pointer/rcu_dereference
macros un packages de niveau supérieur, tels que list
, par hlist
conséquent, il y a trois noyaux de scène RCU protégé: 1) pointeur; 2) liste la liste ; 3) liste de liens de hachage hlist.
Pour ces trois scénarios, l' Publish-Subscribe
interface est la suivante:
2.2.2 Wait For Pre-Existing RCU Readers to Complete
Reclaimer doit recycler les anciennes ressources critiques, donc la question se pose, quand cela se produira-t-il? Par conséquent, il est RCU
nécessaire de fournir un mécanisme pour garantir que tous les lecteurs RCU précédents ont été terminés, c'est-à- rcu_read_lock/rcu_read_unlock
dire qu'ils ne peuvent être recyclés qu'après avoir quitté la section critique étalonnée.
- Les lecteurs et le programme de mise à jour de la figure sont exécutés simultanément;
- Lorsque le programme de mise à jour effectue l'
Removal
opération, il est appelésynchronize_rcu
, marquant la fin de la mise à jour et commençant à entrer dans la phase de récupération; - Après l'
synchronize_rcu
appel, il peut y avoir de nouveaux lecteurs pour lire les ressources critiques (contenu mis à jour) en ce moment, mais les lecteurs quiGrace Period
n'attendent que lePre-Existing
sont dans la figureReader-4, Reader-5
. Tant que ces lecteurs RCU qui existaient avant ont quitté la section critique, cela signifie la fin de la période de grâce, donc le processus de recyclage est effectué; synchronize_rcu
Ce n'est pas que le dernierPre-Existing
lecteur RCU quitte immédiatement après avoir quitté la section critique, il peut y avoir un retard de programmation;
2.2.3 Maintain Multiple Versions of Recently Updated Objects
On 2.2.2节
peut voir qu'après la mise à jour du programme de mise à jour, avant le recyclage du reclaimer, il y aura deux nouvelles et anciennes versions des ressources critiques. Ce n'est qu'après synchronize_rcu
son retour que le reclaimer recycle les anciennes ressources critiques et la dernière version reste. Évidemment, lorsqu'il y a plusieurs mises à jour, il y aura des versions de ressources plus critiques.
Prenons une photo, en prenant des pointeurs et des listes liées comme exemples:
- Le
synchronize_rcu
début de l' appel est un point critique, maintenant différentes versions des ressources critiques; - Après que Reclaimer récupère l'ancienne version des ressources, il est finalement unifié;
3. Exemple d'analyse RCU
Il est temps pour une vague fucking sample code
.
- La logique de code globale:
- Construire quatre threads du noyau, deux threads du noyau pour tester l'opération de protection RCU du pointeur et deux threads du noyau pour tester l'opération de protection RCU de la liste liée;
- Au moment du recyclage, deux mécanismes de
synchronize_rcu
recyclage synchrone et decall_rcu
recyclage asynchrone ont été utilisés ; - Afin de simplifier le code, le jugement de base sur la tolérance aux pannes a été omis;
- Le mécanisme de plusieurs Updaters n'est pas considéré, par conséquent, l'opération mutuellement exclusive entre Updaters est également omise;
#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");
Afin de prouver qu'il n'y a pas de tromperie, le journal de sortie exécuté sur la carte de développement est publié, comme indiqué ci-dessous:
4. Introduction à l'API
4.1 API principale
Les interfaces suivantes ne peuvent pas être plus centrales.
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 Autres API associées
Sur la base de l'API principale, d'autres API connexes ont été étendues, comme suit, sans plus de détails:
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
D'accord, répertorier ces API est un peu déroutant.
Le voile mystérieux de RCU est d'abord dévoilé, puis il sera un peu difficile de ramasser des vêtements à l'intérieur. Après tout, le mécanisme de mise en œuvre derrière RCU est vraiment difficile. Donc, la question vient, voulez-vous être un homme qui voit le roi? Faites attention.
Référence
Documentation / RCU
Qu'est-ce que RCU, fondamentalement?
Qu'est-ce que RCU? Partie 2: Utilisation
RCU partie 3: l'API RCU
Introduction à RCU
Bienvenue à prêter attention au numéro public et à continuer de partager les articles du mécanisme de base sous forme graphique